【学习笔记】RT-Thread内核视频学习笔记[1-16章]

写在前面的话:

这个文章很长,主要是对RT-Threadd的教学视频 (视频链接,)的总结和回顾。是一个比较个人化的东西,其实比较专业的内核指导在RTT官网上也有总结(内核链接)。只是视频中有一些比较基础的官网上没提到的地方做下总结。

文章目录

    • 写在前面的话:
    • 第一章 初识RT-Thread
      • 1.1嵌入式系统
      • 1.2RT-Thread
      • 1.3目录结构
      • 1.4启动分析
    • 第二章 动态内存堆的使用
      • 2.1 回顾堆栈的概念
      • 2.2 裸机系统动态内存分配
      • 2.3 RTT中动态内存
        • 起始地址
        • 结束地址
      • 2.4 RTT动态内存的使用
      • 2.5 动态内存注意事项
      • 2.6 其他相关API
    • 第三章 线程的概念
      • 3.1线程的概念
      • 3.2线程组成
        • 3.2.1线程控制块
        • 3.2.2线程栈
      • 3.3线程创建
      • 3.4创建线程事例
      • 3.5静态线程和动态线程的区别
        • 3.5.1.相关资源分配
        • 3.5.2.运行效率
    • 第四章 简单的线程实例
      • 4.1线程状态切换
      • 4.2系统滴答时钟
      • 4.3GPIO驱动架构操作IO
      • 4.4代码示例
      • 4.5线程栈的大小分配
    • 第五章 线程的时间片轮询调度
      • 5.1线程优先级
      • 5.2线程时间片
      • 5.3线程调度规则
        • 5.3.1优先级抢占调度
        • 5.3.2时间片轮询调度
      • 6示例
    • 第六章 空闲线程及两个常用的钩子函数
      • 6.1空闲线程
      • 6.2空闲线程狗子函数
      • 6.3空闲线程钩子函数使用注意
        • 6.4系统调度钩子函数
    • 第七章 临界区保护
      • 7.1临界区和临界资源
      • 7.2临界区保护
        • 1.==禁止调度==
        • 2.==关闭中断==
    • 第八章 IPC-信号量的使用
      • 8.1IPC简介
      • 8.2信号量工作机制
      • 8.3信号量控制块
      • 8.4信号量的操作
      • 8.5信号量使用事例
    • 第九章 生产者消费者模型
      • 9.1生产者消费者问题模型
      • 9.2生产者消费者问题本质
      • 9.3生产者消费者解决模型
      • 9.4示例代码
    • 第十章 IPC-互斥量
      • 10.1互斥量工作机制
      • 10.2互斥量控制块
      • 10.3互斥量的操作[API]
      • 10.4信号量VS互斥量
      • 10.5示例
    • 第十一章 优先级翻转与优先级继承
      • 11.1优先级翻转
      • 11.2优先级继承
      • 11.3示例
      • 11.4启发
    • 第十二章 IPC-事件集,事件集的使用
      • 12.1事件集工作机制
      • 12.2事件集控制块
      • 12.3事件集的操作
      • 12.4示例
    • 第十三章 IPC-邮箱的使用
      • 13.1邮箱工作机制
      • 13.2邮箱控制块
      • 13.3邮箱操作API
      • 13.4示例
    • 第十四章 IPC-消息队列的使用
      • 14.1消息队列的工作机制
      • 14.2消息队列控制块
      • 12.3消息队列的操作
      • 12.4示例
    • 第十五章 软件定时器的使用
      • 15.1软件定时器
      • 15.2定时器模式
      • 15.3软件定时器控制块
      • 15.4软件定时器的操作
      • 15.5示例
    • 第十六章 内存池的使用
      • 16.1内存池介绍
      • 16.2内存池工作机制
      • 16.3内存池控制块
      • 16.4内存池的操作

第一章 初识RT-Thread

1.1嵌入式系统

  • 嵌入式系统是一种完全嵌入在装置或设备内部、为满足特定需求而设计的计算机系统
  • 嵌入式操作系统是应用于嵌入式系统的软件,用来对接嵌入式底层硬件和上层应用
    • 多任务管理
    • 任务间通信
    • 内存管理
    • 定时器管理
    • 设备管理

1.2RT-Thread

  • 实时操作系统
  • 开源、免费;许可证类似FreeRTOS
  • 国产嵌入式操作系统,不仅是一个RTOS,还包含网络、文件系统、GUI界面等组件的中间件平台,具有极强的扩展性
  • 支持市面上所有的主流编译工具,如IAR、GCC、Keil
  • 完成了超过50款MCU上和所有主流CPU架构上的移植工作

1.3目录结构

  • RT-Thread内核源码目录
|----+ RT-Thread:
|------src:RT-Thread内核代码文件
|------libcpu:各类芯片/内核移植代码
|------include:RT-Thread内核头文件
|------components:RT-Thread外部组件代码
  • 工程源码目录
|-------+ project
|----------application:用户应用代码
|----------drivers:RT-Thread的驱动,不同平台底层驱动的具体实现
|----------kernel-sample:内核例程
|----------Libraries:STM32芯片固件库
|----------rt-thread:RT-Thread源代码
  • 工程目录
|-------+ project
|----------Application:用户应用代码
|----------Drivers:RT-Thread的底层驱动代码
|----------STM32_HAL:存放STM32固件库文件【Libraries】
|----------Kernel:存放RT-Thread内核核心代码【src】
|----------CORTEX-M3:存放CORTEX-M3移植代码【libcpu】
|----------DeviceDrives:驱动框架源码【components->d】
|----------finsh:RT-Thread命令行和finsh组件【components->f】
|----------kernel-sample:RT-Thread内核例程

1.4启动分析

int main(void)
{
    return 0;
}

无法从main函数中看到RT-Thread的启动过程

  • 启动流程
  • kernel -->components.c -->$sub$$main
int $Sub$$main(void)
{
    rt_hw_interrupt_disable();//关中断
    rtthread_startup();//RTT启动函数
    return 0;
}

关于$Sub$$main和main之间的说法,我之前有解释过,可以看链接。

  • RTT启动入口
int rtthread_startup(void)
{
    rt_hw_interrupt_disable();//关中断

    /* board level initalization
     * NOTE: please initialize heap inside board initialization.
     */
    rt_hw_board_init();//硬件平台初始化

    /* show RT-Thread version */
    rt_show_version();//显示版本号码

    /* timer system initialization */
    rt_system_timer_init();//系统时钟初始化

    /* scheduler system initialization */
    rt_system_scheduler_init();//系统调度器的初始化

#ifdef RT_USING_SIGNALS
    /* signal system initialization */
    rt_system_signal_init();//系统信号机制的初始化
#endif

    /* create init_thread */
    rt_application_init();//很重要,因为用户的任务在这里面创建的

    /* timer thread initialization */
    rt_system_timer_thread_init();

    /* idle thread initialization */
    rt_thread_idle_init();

    /* start scheduler */
    rt_system_scheduler_start();//使rtt跑起来

    /* never reach here */
    return 0;
}
  • main就是在app中创建的
void rt_application_init(void)
{
    rt_thread_t tid;

#ifdef RT_USING_HEAP
    tid = rt_thread_create("main", main_thread_entry, RT_NULL,
                           RT_MAIN_THREAD_STACK_SIZE, RT_MAIN_THREAD_PRIORITY, 20);
    RT_ASSERT(tid != RT_NULL);
#else
    rt_err_t result;

    tid = &main_thread;
    result = rt_thread_init(tid, "main", main_thread_entry, RT_NULL,
                            main_stack, sizeof(main_stack), RT_MAIN_THREAD_PRIORITY, 20);
    RT_ASSERT(result == RT_EOK);
	
    /* if not define RT_USING_HEAP, using to eliminate the warning */
    (void)result;
#endif

    rt_thread_startup(tid);
}

【学习笔记】RT-Thread内核视频学习笔记[1-16章]_第1张图片

第二章 动态内存堆的使用

2.1 回顾堆栈的概念

  • 栈(stack):由编译器自动分配释放
  • 堆(heap) :一般由程序员分配和释放
int a = 0char *p1
int main()
{
    int b;
    char s[] = "abc";
    char *p2;
    char *p3 = "123456";
    static int c = 0;
    p1 = (char *)malloc(10);
    p2 = (char *)malloc(20);
}
  • 定义的局部变量都来源于栈空间栈是由编译器自动分配和释放的,当函数结束,栈空间就释放掉
  • malloc函数申请的都来源于堆空间

2.2 裸机系统动态内存分配

【学习笔记】RT-Thread内核视频学习笔记[1-16章]_第2张图片

2.3 RTT中动态内存

在board.c中,需要一个函数事先帮我们配置好动态内存的api
在board.c中,也就是进行相关内存初始化的时候,会去配置动态内存。会根据实际芯片,或者是板卡上的内存去配置。

rt_system_heap_init((void *)HEAP_BEGIN,(void *)HEAP_END)//从系统中取到一块内存设置为动态内存区,然后可以通过RTT特定的rt_malloc和rt_free,申请和释放内存
//一般在board.c中调用,在进行板级初始化的时候,配置动态内存,根据芯片或板卡的实际情况配置

函数需要提供一个起始地址HEAP_BEGIN结束地址HEAP_END,中间的这段空间会被系统当作动态内存空间使用。
本节示例使用的芯片,具有64kram空间。
在这里插入图片描述

  • 从系统中取到一块内存设置为动态内存区,然后可以通过RTT特定的rt_mallocrt_free申请和释放内存
  • 一般在board.c中调用,在进行板级初始化的时候,配置动态内存,根据芯片或板卡的实际情况配置
    函数原型在men.c
    【学习笔记】RT-Thread内核视频学习笔记[1-16章]_第3张图片

起始地址

#define HEAP_BEGIN  ((void *)&Image$$RW_IRAM$$Limit)
HEAP_BEGIN = (void *)&Image$$RW_IRAM$$Limit
  • Image$$RW_IRAM$$Limit连接器导出的符号,代表ZI段的结束,也就是程序执行去的RAM结束后的地址,反过来就是我们执行去的RAM未使用的区域的起始地址
  • 也就是,定义全局变量,或者静态全局变量,所剩下的空间分配给动态内存堆中
    通过查看工程中的map文件
    ,可以看到整个工程所用到的静态ram空间用到了22.05k,也就是说,除了RW和ZI外,整个加起来用到了22.05k。那么整个芯片64k的空间,除去22.05k,剩下的这段空间,肯定是有一个起始地址的。这个地址,就是通过宏定义Image$$RW_IRAM$$Limit表示的。
    【学习笔记】RT-Thread内核视频学习笔记[1-16章]_第4张图片

结束地址

我们一般以片内ram的结束地址,作为结束地址。

Total RW+heap size = MCU total RAM size

2.4 RTT动态内存的使用

  • 首先创建一个任务
int dynmem_sample(void)
{
    rt_thread_t tid;

    /* 创建线程1 */
    tid = rt_thread_create("thread1",
                           thread1_entry, RT_NULL,
                           THREAD_STACK_SIZE,
                           THREAD_PRIORITY,
                           THREAD_TIMESLICE);
    if (tid != RT_NULL)
        rt_thread_startup(tid);

    return 0;
}
  • thread1_entry:任务的入口函数
/* 线程入口 */
void thread1_entry(void *parameter)
{
    int i;
    char *ptr = RT_NULL; /* 内存块的指针 */

    for (i = 0; ; i++)
    {
        /* 每次分配 (1 << i) 大小字节数的内存空间 */
        ptr = rt_malloc(1 << i);

        /* 如果分配成功 */
        if (ptr != RT_NULL)
        {
            rt_kprintf("get memory :%d byte\n", (1 << i));
            /* 释放内存块 */
            rt_free(ptr);
            rt_kprintf("free memory :%d byte\n", (1 << i));
            ptr = RT_NULL;
        }
        else
        {
            rt_kprintf("try to get %d byte memory failed!\n", (1 << i));
            return;
        }
    }
}

使用rt_malloc申请内存,如果申请内存失败,会返回RT_NULL[常量],申请的空间要少于系统空闲的空间才能成功
通过命令行的方式,导出特定的符号
【学习笔记】RT-Thread内核视频学习笔记[1-16章]_第5张图片

2.5 动态内存注意事项

  • 内存复位
    当我们每次申请到新的内存块之后,建议对申请到的内存块进行清零操作
    因为整个动态内存空间是公用的,申请到的空间内可能有原来的应用留下来的数据,初始值可能不为零。
p = rt_malloc(10);
if(p != RT_NULL)
{
rt_memset(p,0,10);
}
  • 内存泄漏
    是指程序中已动态分配的堆内存由于某种原因程序未释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
    rt_malloc和rt_free配套使用

2.6 其他相关API

void *rt_realloc(void *tmen,rt_size_t newsize)
  • 在已分配内存块的基础上重新分配内存块的大小(增加或者缩小)
  • 在进行重新分配内存块时,原来的内存块数据保持不变(缩小的情况下,后面的数据被自动截断)

如果前面分配了50个字节的内存空间,后面发现不够,想加到100,或者原本分配了50,太大了,需要20就够了

void *rt_calloc(rt_size_t count, rt_size_t size)
  • 从内存堆中分配连续内存地址的多个内存块
  • malloc的形参只有size 的大小
    calloc的形参有count 和size,分配到的空间为count * size ,而且空间连续

第三章 线程的概念

3.1线程的概念

  • 在设计一个较为复杂的应用程序时,也通常把一个大型任务分解成多个小任务,然后通过运行这些小任务,最终达到完成大人物的目的
  • 在rtt中,与上述小问题对应的程序实体就叫做“线程”(任务),rtt就是一个能对这些小“线程”进行管理和调度多“线程”操作系统
  • 线程是实现任务的载体,是rtt中最基本的调度单位,她描述了一个任务执行的运行环境,也描述了这个任务的优先等级。

3.2线程组成

RTT中,线程有三部分组成:线程代码(入口函数)、线程控制块线程堆栈

  • 无限循环结构
void thread_entry(void *parameter)
{
    whlie(1)
    {
        /* 等待事件的发生 */
        
        /* 处理事件 */
    }
}

while中往往要加入让出CPU使用权的API函数,如果不加,其他线程得不到执行。

  • 顺序执行结构
void thread_entry(void *parameter)
{
    /* 事务1处理 */
    /* 事务2处理 */
}

3.2.1线程控制块

线程控制块是操作系统用于管理线程的一个数据结构,他会存放线程的一些信息,例如,优先级、线程名称、线程状态等,也包含线程与线程之间连接用的链表结构、线程等待时间集合等。

/**
 * Thread structure
 */
struct rt_thread
{
    /* rt object */
    char        name[RT_NAME_MAX];                      /**< the name of thread */
    rt_uint8_t  type;                                   /**< type of object */
    rt_uint8_t  flags;                                  /**< thread's flags */

#ifdef RT_USING_MODULE
    void       *module_id;                              /**< id of application module */
#endif

    rt_list_t   list;                                   /**< the object list */
    rt_list_t   tlist;                                  /**< the thread list */

    /* stack point and entry */
    void       *sp;                                     /**< stack point */
    void       *entry;                                  /**< entry */
    void       *parameter;                              /**< parameter */
    void       *stack_addr;                             /**< stack address */
    rt_uint32_t stack_size;                             /**< stack size */

    /* error code */
    rt_err_t    error;                                  /**< error code */

    rt_uint8_t  stat;                                   /**< thread status */

    /* priority */
    rt_uint8_t  current_priority;                       /**< current priority */
    rt_uint8_t  init_priority;                          /**< initialized priority */
#if RT_THREAD_PRIORITY_MAX > 32
    rt_uint8_t  number;
    rt_uint8_t  high_mask;
#endif
    rt_uint32_t number_mask;

#if defined(RT_USING_EVENT)
    /* thread event */
    rt_uint32_t event_set;
    rt_uint8_t  event_info;
#endif

#if defined(RT_USING_SIGNALS)
    rt_sigset_t     sig_pending;                        /**< the pending signals */
    rt_sigset_t     sig_mask;                           /**< the mask bits of signal */

    void            *sig_ret;                           /**< the return stack pointer from signal */
    rt_sighandler_t *sig_vectors;                       /**< vectors of signal handler */
    void            *si_list;                           /**< the signal infor list */
#endif

    rt_ubase_t  init_tick;                              /**< thread's initialized tick */
    rt_ubase_t  remaining_tick;                         /**< remaining tick */

    struct rt_timer thread_timer;                       /**< built-in thread timer */

    void (*cleanup)(struct rt_thread *tid);             /**< cleanup function when thread exit */

    /* light weight process if present */
#ifdef RT_USING_LWP
    void        *lwp;
#endif

    rt_uint32_t user_data;                             /**< private user data beyond this thread */
};
typedef struct rt_thread *rt_thread_t;

3.2.2线程栈

  • RTT每个线程都具有独立的栈空间,当线程切换时,系统会将当前线程的上下文保存在线程栈中,当线程要恢复运行时,再从线程栈中读取上下文信息,恢复线程的运行。
  • 线程上下文是指线程执行时的环境,具体来说就是【各个变量和数据】,包括所有寄存器变量、堆栈信息、内存信息等。
  • 线程栈在形式上是一段连续的内存空间,我们可以通过定义一个数组或者申请一段动态内存来作为线程的栈。

3.3线程创建

  • 创建线程
  • 创建静态线程
rt_thread_t rt_thread_create(const char* name, 
                                    void (*entry)(void* parameter), 
                                    void* parameter, 
                                    rt_uint32_t stack_size, 
                                    rt_uint8_t priority, 
                                    rt_uint32_t tick);
  • 创建动态线程
rt_err_t rt_thread_init(struct rt_thread* thread, 
                            const char* name, 
                            void (*entry)(void* parameter), 
                            void* parameter, 
                            void* stack_start, 
                            rt_uint32_t stack_size, 
                            rt_uint8_t priority, 
                            rt_uint32_t tick);
  • 静态线程需要实现定义好线程控制块与栈空间
  • 动态线程的堆栈 是动态分配的
  • 启动线程
rt_err_t rt_thread_startup(rt_thread_t thread);

调用此函数后创建的线程会被加入到线程的就绪队列,执行调度。

3.4创建线程事例

  • 动态线程
#include 

#define THREAD_PRIORITY         25
#define THREAD_STACK_SIZE       512
#define THREAD_TIMESLICE        5

static rt_thread_t tid1 = RT_NULL;

/* 线程1的入口函数 */
static void thread1_entry(void *parameter)

/* 创建线程1,名称是thread1,入口是thread1_entry*/
    tid1 = rt_thread_create("thread1",
                            thread1_entry, RT_NULL,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE);
    
    /* 如果获得线程控制块,启动这个线程 */
    if (tid1 != RT_NULL)
        rt_thread_startup(tid1);
  • 创建动态线程
static char thread2_stack[1024];
static struct rt_thread thread2;
/* 线程2入口 */
static void thread2_entry(void *param)
{
}

/* 初始化线程2,名称是thread2,入口是thread2_entry */
    rt_thread_init(&thread2,
                   "thread2",
                   thread2_entry,
                   RT_NULL,
                   &thread2_stack[0],
                   sizeof(thread2_stack),
                   THREAD_PRIORITY - 1, THREAD_TIMESLICE);
    rt_thread_startup(&thread2);

3.5静态线程和动态线程的区别

3.5.1.相关资源分配

  • 静态的线程控制栈和启动块,都需要先定义出来
  • 动态的是运行的时候自动分配的

3.5.2.运行效率

  • 都处于ram上的时候,没有区别
  • 使用外扩ram,动态的线程栈和线程控制款使用外扩的话,效率比较低下

第四章 简单的线程实例

4.1线程状态切换

【学习笔记】RT-Thread内核视频学习笔记[1-16章]_第6张图片

4.2系统滴答时钟

  • 每一个操作系统中都存在一个==“系统心跳”时钟==,是操作系统中最小的时钟单位。这个时钟负责系统和时间相关的一些操作。作为操作系统运行的时间尺度,心跳时钟是由硬件定时器的定时中断产生
  • 系统的心跳时钟我们也常称之为系统滴答或时钟节拍,系统滴答的频率我们根据处理器cpu 的处理能力来决定。
  • 时钟节拍使得内核可以将线程延时若干个整数时钟节拍,以及线程等待事件发生时,提供等待超时的依据。
  • 频率越快,内核函数介入系统运行大的几率就越大,内核占用的处理器时间就越长,系统的负荷就变大
  • 频率越小,时间处理精度不够
  • 在stm32平台上一般设置系统滴频率为100hz,即每个滴答时间是10ms。

然后再示例中,rt_thread_delay(50),由于系统时钟是100HZ,一个滴答时间是4ms,50个滴答是0.5秒

/**
 * This is the timer interrupt service routine.
 *
 */
void SysTick_Handler(void)
{
    /* enter interrupt */
    rt_interrupt_enter();
    HAL_IncTick();
    rt_tick_increase();
    /* leave interrupt */
    rt_interrupt_leave();
}

4.3GPIO驱动架构操作IO

  • IO初始化
void rt_pin_mode(rt_base_t pin, rt_base_t mode)
{
    RT_ASSERT(_hw_pin.ops != RT_NULL);
    _hw_pin.ops->pin_mode(&_hw_pin.parent, pin, mode);
}
//IOmode
#define PIN_MODE_OUTPUT         0x00
#define PIN_MODE_INPUT          0x01
#define PIN_MODE_INPUT_PULLUP   0x02
#define PIN_MODE_INPUT_PULLDOWN 0x03
#define PIN_MODE_OUTPUT_OD      0x04
#define STM32F10X_PIN_NUMBERS 144
#if (STM32F10X_PIN_NUMBERS == 144)
    __STM32_PIN_DEFAULT,
    __STM32_PIN(1, E, 2),
    __STM32_PIN(2, E, 3),
    __STM32_PIN(3, E, 4),
    __STM32_PIN(4, E, 5),
    __STM32_PIN(5, E, 6),
    __STM32_PIN_DEFAULT,
    __STM32_PIN(7, C, 13),
    __STM32_PIN(8, C, 14),
    __STM32_PIN(9, C, 15), // 如果要使用C15管教,设置PIN为9即可
    ...
  • IO写入
void rt_pin_write(rt_base_t pin, rt_base_t value)
PIN_HIGH
PIN_LOW
void rt_pin_write(rt_base_t pin, rt_base_t value)
{
    RT_ASSERT(_hw_pin.ops != RT_NULL);
    _hw_pin.ops->pin_write(&_hw_pin.parent, pin, value);
}
  • IO读出
int  rt_pin_read(rt_base_t pin)
{
    RT_ASSERT(_hw_pin.ops != RT_NULL);
    return _hw_pin.ops->pin_read(&_hw_pin.parent, pin);
}

4.4代码示例

#include 


/* 线程1的入口函数 */
static void thread1_entry(void *parameter)
{
    rt_pin_mode(14,PIN_MODE_OUTPUT);
    while(1)
    {
        rt_pin_write(14,PIN_LOW);
        rt_thread_delay(50);//rt_thread_mdelay(500);rt_tjread_sleep(50);
        rt_pin_write(14,PIN_HIGH);
        rt_thread_delay(50);
    }
}

void led_test()
{

    rt_thread_t tid1;

    /* 创建线程1,名称是thread1,入口是thread1_entry*/
    tid1 = rt_thread_create("led",
                            led_entry, RT_NULL,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE);
    
    /* 如果获得线程控制块,启动这个线程 */
    if (tid1 != RT_NULL)
        rt_thread_startup(tid1);
 }
 MSH_CMD_EXP。。。

4.5线程栈的大小分配

先将线程大小设置一个固定值(比如2048),在线程运行时通过查看线程栈的使用情况,了解线程栈的实际使用情况,根据情况设置合理的栈大小。
【学习笔记】RT-Thread内核视频学习笔记[1-16章]_第7张图片

一般将线程栈最大使用量设置为70%


第五章 线程的时间片轮询调度

5.1线程优先级

  • 优先级时间片是线程的两个重要参数,分别描述线程竞争处理器资源的能力和持处理时间长短的能力。
  • RT_Thread最大支持256个优先级数值越小的优先级越高,0为最高优先级,最低优先级预留给空闲线程)。
  • 用户可以通过rt_config.h中的RT_THREAD_PRIORITY_MAX宏来修改最大支持的优先级;
  • 针对STM32,默认最大支持32个优先级
  • 具体应用中,线程总数不受限制,能创建的线程总数之和具体硬件平台的内存有关

5.2线程时间片

时间片仅对优先级相同的就绪态线程有效。系统对优先级相同的就绪态线程采用时间片轮转的调度方式进行调度时,时间片起到约束线程单次运行时长的作用,其单位是一个系统节拍(OS Tick),详见第五章。

假设有 2 个优先级相同的就绪态线程 A 与 B,A 线程的时间片设置为 10,B 线程的时间片设置为 5,那么当系统中不存在比 A 优先级高的就绪态线程时系统会在 A、B 线程间来回切换执行,并且每次对 A 线程执行 10 个节拍的时长,对 B 线程执行 5 个节拍的时长,如下图。
【学习笔记】RT-Thread内核视频学习笔记[1-16章]_第8张图片

5.3线程调度规则

5.3.1优先级抢占调度

操作系统总是让具有最高优先级的就绪任务优先运行:即当有任务的优先级高于当前任务优先级并且处于就绪态后,就一定会发生任务调度

  • 通过优先级抢占机制,最大限度的满足了系统的实时性

5.3.2时间片轮询调度

当操作系统中存在相同优先级的线程时(优先级相同就不会抢占),操作系统会按照设置的时间片大小来轮流调度线程,时间片起到约束线程单次运行时长的作用,其单位是一个系统节拍(OS Tick)

  • 通过时间片轮询,保证优先级相同的任务能够轮流占有处理器。

6示例

在timeslice.c

 /*
 * 程序清单:相同优先级线程按照时间片轮番调度
 *
 * 这个例子中将创建两个线程,每一个线程都在打印信息
 * 
 */

#include 

#define THREAD_STACK_SIZE	1024
#define THREAD_PRIORITY	    20
#define THREAD_TIMESLICE    10

/* 线程入口 */
static void thread_entry(void* parameter)
{
    rt_uint32_t value;
    rt_uint32_t count = 0;

    value = (rt_uint32_t)parameter;
    while (1)
    {
        if(0 == (count % 5))
        {           
            rt_kprintf("thread %d is running ,thread %d count = %d\n", value , value , count);      

            if(count > 200)
                return;            
        }
         count++;
     }  
}

int timeslice_sample(void)
{
    rt_thread_t tid;
    /* 创建线程1 */
    tid = rt_thread_create("thread1", 
                            thread_entry, (void*)1, 
                            THREAD_STACK_SIZE, 
                            THREAD_PRIORITY, THREAD_TIMESLICE); 
    if (tid != RT_NULL) 
        rt_thread_startup(tid);


    /* 创建线程2 */
    tid = rt_thread_create("thread2", 
                            thread_entry, (void*)2,
                            THREAD_STACK_SIZE, 
                            THREAD_PRIORITY, THREAD_TIMESLICE-5);
    if (tid != RT_NULL) 
        rt_thread_startup(tid);
    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(timeslice_sample, timeslice sample);

第六章 空闲线程及两个常用的钩子函数

6.1空闲线程

  • 空闲线程是一个比较特殊的系统线程,它具备最低的优先级。当系统中无其他就绪线程可运行时,调度器将调度到空闲线程。
  • 空闲线程还负责一些系统资源回收以及将一些关闭态的线程从线程调度列表中移除的工作
  • 空闲线程在形式上是一个无限循环结构,永远为就绪态,且永远不被挂起
  • 在RTT中空闲线程向用户提供了钩子函数,空闲线程钩子函数可以让系统在空闲的时候执行一些非紧急事务,例如系统运行指示灯闪烁,CPU使用率统计等等。

空闲线程在 RT-Thread 也有着它的特殊用途:若某线程运行完毕,系统将自动删除线程:自动执行 rt_thread_exit() 函数,先将该线程从系统就绪队列中删除,再将该线程的状态更改为关闭状态,不再参与系统调度,然后挂入 rt_thread_defunct 僵尸队列(资源未回收、处于关闭状态的线程队列)中,最后空闲线程会回收被删除线程的资源。空闲线程也提供了接口来运行用户设置的钩子函数,在空闲线程运行时会调用该钩子函数,适合钩入功耗管理、看门狗喂狗等工作。

6.2空闲线程狗子函数

  • 设置钩子函数
rt_err_t rt_thread_idle_sethook(void (*hook)(void))
  • 删除钩子函数
rt_err_t rt_thread_idle_delhook(void (*hook)(void))

6.3空闲线程钩子函数使用注意

  • 永远为就绪态,所以钩子函数中执行的相关代码必须保证空闲线程在任何时刻都不会被挂起。例如rt_thread_delayrt_sem_take都可能会导致线程挂起的阻塞类函数都不能在钩子函数中使用
  • 可以设置多个钩子函数,由RT_IDEL_HOOK_LIST_ZISE决定可以设置几个钩子函数。

6.4系统调度钩子函数

系统的上下文切换是系统运行时最普遍的事件,有时用户可能会想知道在某一时刻发生了什么样的线程切换,RT-Thread向用户提供了一个系统调度钩子函数,这个钩子函数在系统进行任务切换时运行,通过这个钩子函数,我们可以了解到系统任务调度时一些信息。

rt_scheduler_sethook(void (*hook)(struct rt_thread *form, struct rt_thread *to))

***【学习笔记】RT-Thread内核视频学习笔记[1-16章]_第9张图片


第七章 临界区保护

7.1临界区和临界资源

  • 临界资源
  • 临界资源是指一次仅允许一个线程访问的共享资源。他可以是一个硬件设备,也可以是一个变量。
  • 多线程必须互斥地对他们进行访问
  • 临界区
  • 每个线程中访问(操作)临界资源的那段代码成为临界区[Critical Section],我们每次只允许一个线程进入临界区

7.2临界区保护

  • 关闭系统调度,保护临界区:禁止调度,关闭中断
  • 信号量、互斥量保护临界区。

1.禁止调度

即把调度器锁住不让线程进行切换。这样可以保证当前运行的任务不被换出,直到调度器解锁。

void thrend_entry(void* paremeter)
{
    while(1)
    {
        /* 调度器上锁,仅响应中断 */
        rt_enter_critical();
        /* 进入临界区*/
         ...
        /* 调度器解锁 */
        rt_exit_critical();
    }
}

2.关闭中断

因为所有的线程调度都是建立在中断的基础上的,所以,当我们关闭中断后,系统将不能在进行调度,线程自身也自然不会其他线程抢占了

void thrend_entry(void* paremeter)
{
    rt_base_t level;
    while(1)
    {
        /* 关闭中断 */
        level = rt_hw_interruput_disable();
        /* 以下是临界区 */
        ...
        /* 关闭中断 */
        rt_hw_interruput_enable(level);
    }
}

第八章 IPC-信号量的使用

8.1IPC简介

在嵌入式系统中运行的代码主要包括线程和中断[ISR],在它们的运行过程中,它们的运行步骤有时需要同步(按照预定的先后次序运行),他们访问的资源有时候需要互斥一个时刻只允许一个线程访问资源),它们制键又是也要彼此交换数据。这些需要,有的是因为应用需求,有的是多线程编程模型带来的需求。

操作系统必须提供相应的机制来完成这些功能(同步,互斥,数据交互,我们把这些机制统称为**进程间通信(Internal Process Communication , IPC)RT-Thread中的IPC机制包括信号量、互斥量、事件、邮箱、消息队列。

通过IPC机制,我们可以协调多个线程(包括ISR)“默契”的工作,从而共同完成一个整项工作。

8.2信号量工作机制

信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。

【学习笔记】RT-Thread内核视频学习笔记[1-16章]_第10张图片

每个信号量对象都有一个信号量一个线程等待队列信号量的值对应信号量对象的实例数目(资源数目),加入信号量值N,则便是共有N个信号量实例(资源)可以被使用,当信号量实例数目为0时,再请求信号量的线程就会被挂起在信号量的等待队列上,等待可用的信号量实例。

8.3信号量控制块

在RT-Thread中,信号量控制块是操作系统用于管理信号量的一个数据结构。

struct rt_semaphore
{
    struct rt_ipc_object parent;    /*in herit from ipc_object*/
    rt_uint16_t value;                /*value of semaphore*/
}
  • 定义静态信号量:struct rt_semaphore static_sem
  • 定义动态信号量:rt_sem_t dynamic_sem
    动态信号量后续需要分配内存空间

8.4信号量的操作

  • 信号量的初始化和脱离静态信号量
rt_err_t rt_sem_init(rt_sem_t    sem,   //信号量指针
                     const char *name,    //信号量名称
                     rt_uint32_t value,     //信号量初始值
                     rt_uint8_t  flag)      //信号量标志:RT_IPC_FLAG_FIFO/RT_IPC_FLAG_PRIO 

多个线程等待信号量的排队方式:RT_IPC_FLAG_FIFO/RT_IPC_FLAG_PRIO – 先进先出/优先级

rt_err_t rt_sem_detach(rt_sem_t sem)
  • 创建与删除动态信号量
rt_sem_t rt_sem_create(const char *name, rt_uint32_t value, rt_uint8_t flag)
  • 动态申请,涉及内存申请,要判断返回值
rt_err_t rt_sem_delete(rt_sem_t sem)
  • 获取信号量
rt_err_t rt_sem_take(rt_sem_t sem, rt_int32_t time) //时间参数:RT_WAITING_FOREVER=-1
  • 不能在ISR中调用,因为中断快进快出。这个API只能在线程中等待。
  • 时间参数
    • =0:立即返回
    • >0:以系统tick时间为单位继续等待
    • <0:RT_WAITING_FOREVER
rt_err_t rt_sem_trytake(rt_sem_t sem)
  • 时间参数为0的take,若没有得到信号量,返回-RT_ETIMEOUT
  • 释放信号量
rt_err_t rt_sem_release(rt_sem_t sem)

8.5信号量使用事例

static void rt_thread1_entry(void *parameter)
{
    static rt_uint8_t count = 0;
  
    while(1)
    {
        if(count <= 100)
        {
            count++;           
        }
        else
            return; 
        
        /* count每计数10次,就释放一次信号量 */
         if(0 == (count % 10))
        {
            rt_kprintf("t1 release a dynamic semaphore.\n" ); 
            rt_sem_release(dynamic_sem);            
        }
    }
}

ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
static void rt_thread2_entry(void *parameter)
{
    static rt_err_t result;
    static rt_uint8_t number = 0;
    while(1)
    {
        /* 永久方式等待信号量,获取到信号量,则执行number自加的操作 */
        result = rt_sem_take(dynamic_sem, RT_WAITING_FOREVER);
        if (result != RT_EOK)
        {        
            rt_kprintf("t2 take a dynamic semaphore, failed.\n");
            rt_sem_delete(dynamic_sem);
            return;
        }
        else
        {      
            number++;             
            rt_kprintf("t2 take a dynamic semaphore. number = %d\n" ,number);                        
        }
    }   
}

第九章 生产者消费者模型

9.1生产者消费者问题模型

生产者消费者问题是一个经典的、多线程同步问题。
两个线程:一个生产者线程和一个消费者线程两个线程共享一个初始为空固定大小为n的缓存区

  • 生产者的工作是“生产”一段数据,只有缓冲区没满时,生产者才能把消息放入到缓冲区,否则必须等待,如此反复。
  • 同时,只有缓冲区非空时,消费者才能从中取出数据,一次消费一段数据,否则必须等待,如此反复。
  • 问题的核心
  • 保证不让生产者在缓存还是满的时候仍然要向内写数据
  • 不让消费者试图从空的缓存中取出数据

9.2生产者消费者问题本质

解决生产者消费者问题实际上是要解决线程间互斥关系问题和同步关系问题

由于缓冲区是临界资源它一个时刻只允许一个生产者放入消息,或者一个消费者从中取出消息,所以这里要解决一个互斥访问的问题。
同时生产者和消费者又是一个相互协作的关系,只有生产者生产之后消费者才能消费,所以我们还需要解决一个同步的问题。

9.3生产者消费者解决模型

【学习笔记】RT-Thread内核视频学习笔记[1-16章]_第11张图片

9.4示例代码

  • 两个线程,3个信号量

/* 定义最大5个元素能够被产生 */
#define MAXSEM 5

/* 用于放置生产的整数数组 */
rt_uint32_t array[MAXSEM];

/* 指向生产者、消费者在array数组中的读写位置 */
static rt_uint32_t set, get;

/* 指向线程控制块的指针 */
static rt_thread_t producer_tid = RT_NULL;
static rt_thread_t consumer_tid = RT_NULL;

struct rt_semaphore sem_lock;
struct rt_semaphore sem_empty, sem_full;

/* 生产者线程入口 */
void producer_thread_entry(void *parameter)
{
    int cnt = 0;

    /* 运行10次 */
    while (cnt < 10)
    {
        /* 获取一个空位 */
        rt_sem_take(&sem_empty, RT_WAITING_FOREVER);

        /* 修改array内容,上锁 */
        rt_sem_take(&sem_lock, RT_WAITING_FOREVER);
        array[set % MAXSEM] = cnt + 1;
        rt_kprintf("the producer generates a number: %d\n", array[set % MAXSEM]);
        set++;
        rt_sem_release(&sem_lock);

        /* 发布一个满位 */
        rt_sem_release(&sem_full);
        cnt++;

        /* 暂停一段时间 */
        rt_thread_mdelay(20);
    }

    rt_kprintf("the producer exit!\n");
}

/* 消费者线程入口 */
void consumer_thread_entry(void *parameter)
{
    rt_uint32_t sum = 0;

    while (1)
    {
        /* 获取一个满位 */
        rt_sem_take(&sem_full, RT_WAITING_FOREVER);

        /* 临界区,上锁进行操作 */
        rt_sem_take(&sem_lock, RT_WAITING_FOREVER);
        sum += array[get % MAXSEM];
        rt_kprintf("the consumer[%d] get a number: %d\n", (get % MAXSEM), array[get % MAXSEM]);
        get++;
        rt_sem_release(&sem_lock);

        /* 释放一个空位 */
        rt_sem_release(&sem_empty);

        /* 生产者生产到10个数目,停止,消费者线程相应停止 */
        if (get == 10) break;

        /* 暂停一小会时间 */
        rt_thread_mdelay(50);
    }

    rt_kprintf("the consumer sum is: %d\n", sum);
    rt_kprintf("the consumer exit!\n");
}

int producer_consumer(void)
{
    set = 0;
    get = 0;

    /* 初始化3个信号量 */
    rt_sem_init(&sem_lock, "lock",     1,      RT_IPC_FLAG_FIFO);
    rt_sem_init(&sem_empty, "empty",   MAXSEM, RT_IPC_FLAG_FIFO);
    rt_sem_init(&sem_full, "full",     0,      RT_IPC_FLAG_FIFO);
    ...
}

第十章 IPC-互斥量

10.1互斥量工作机制

互斥量(互斥锁)是用于线程间互斥访问的IPC对象,它是一种特殊的二值信号量,当某个线程访问系统中的共享资源时,通过引入互斥量机制,可以保证其他线程无法获得共享资源的访问权
【学习笔记】RT-Thread内核视频学习笔记[1-16章]_第12张图片

互斥量只有两种状态,LOCKED和UNLOCKED,分别代表加锁和开锁的两种情况。

  • 当线程持有他时,互斥量处于闭锁状态,由这个线程获得它的所有权
  • 当线程释放它时,将对互斥量进行开锁失去对它的所有权
  • 当一个线程持有互斥量时,其他线程将不能够对它进行开锁或持有它。
  • 持有该互斥量的线程也能够再次获得这个“锁”(递归持有)而不被挂起。

10.2互斥量控制块

互斥量控制块是操作系统用于管理互斥量的一个数据结构

struct rt_mutex
{
    struct rt_ipc_object parent;                        /**< inherit from ipc_object */

    rt_uint16_t          value;                         /**< value of mutex 只有两种状态*/

    rt_uint8_t           original_priority;             /**< priority of last thread hold the mutex 上一次拥有这个互斥量的优先级*/
    rt_uint8_t           hold;                          /**< numbers of thread hold the mutex ,某个线程持有互斥量的次数*/

    struct rt_thread    *owner;                         /**< current owner of mutex ,当前持有线程的线程控制块*/
};
  • 静态互斥量:struct rt_mutex static_mutex
  • 动态信号量:rt_mutex_t dynamic_mutex

10.3互斥量的操作[API]

  • 初始化和脱离
  • rt_err_t rt_mutex_init(rt_mutex_t mutex, const char * name, rt_uint8_t flag)
    //RT_IPC_FLAG_FIFO/RT_IPC_FLAG_PRIO
  • rt_err_t rt_mutex_detach(rt_mutex_t mutex)
  • 创建与删除
  • rt_mutex_t rt_mutex_create(const char * name, rt_uint8_t flag)
  • rt_err_t rt_mutex_delete(rt_mutex_t mutex)
  • 获取互斥量
  • rt_err_t rt_mutex_take(rt_mutex_t mutex, rt_int32_t time)
    //RT_WAITING_FOREVER = -1
  • 规定时间内没有得到互斥量会返回-RT_TIME_OUT
  • 当一个线程已经take了一个互斥量之后,再次take不用等待,并且hold+1
  • 释放互斥量
  • rt_err_t rt_mutex_release(rt_mutex_t mutex)

不能在中断中释放。

10.4信号量VS互斥量

  • 信号量可以由 任何线程(以及中断)释放,线程只有在获得许可的时候才可以运行,强调的是运行步骤
    互斥量只能由持有它的线程释放,即只有“锁上”它的那个线程才有“钥匙”打开它。它用于互斥的时候就像一把钥匙,只有获得钥匙的线程才可以运行,强调的是许可和权限
  • 使用信号量可能导致线程优先级反转
    互斥量可通过优先级继承的方法解决优先级反转问题。

10.5示例

tatic void rt_thread_entry1(void *parameter)
{
      while(1)
      {
          /* 线程1获取到互斥量后,先后对number1、number2进行加1操作,然后释放互斥量 */
          rt_mutex_take(dynamic_mutex, RT_WAITING_FOREVER);          
          number1++;
          rt_thread_mdelay(10);
          number2++;          
          rt_mutex_release(dynamic_mutex);
       }	    
}

ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
static void rt_thread_entry2(void *parameter)
{     
      while(1)
      {
          /* 线程2获取到互斥量后,检查number1、number2的值是否相同,相同则表示mutex起到了锁的作用 */
          rt_mutex_take(dynamic_mutex, RT_WAITING_FOREVER);
          if(number1 != number2)
          {
            rt_kprintf("not protect.number1 = %d, mumber2 = %d \n",number1 ,number2);
          }
          else
          {
            rt_kprintf("mutex protect ,number1 = mumber2 is %d\n",number1);            
          }
          
           number1++;
           number2++;
           rt_mutex_release(dynamic_mutex);
          
          if(number1 >=50)
              return;      
      }	  
} 

第十一章 优先级翻转与优先级继承

11.1优先级翻转

使用信号量会导致的另一个潜在的问题就是线程优先级翻转问题。

所谓线程优先级翻转,即当一个高优先级线程试图通过某种IPC对象已被一低优先级的线程访问共享资源时,如果该IPC对象已被一低优先级的线程所持有,而这个低优先级线程在运行过程中,可能又被一些中等优先级的线程抢占,因此会造成高优先级线程被许多具有较低优先级的线程阻塞的情况。
优先级翻转会造成高优先级线程的实时性得不到保证
【学习笔记】RT-Thread内核视频学习笔记[1-16章]_第13张图片

11.2优先级继承

在RT-Thread中,通过互斥量的优先级继承算法,可以有效的解决优先级翻转问题。
优先级翻转是指提高某个占有某种共享资源的低优先级线程的优先级,使之与所有等待该资源的线程优先级最高的那个线程的优先级相等,从而得到更快的执行,然后释放贡献资源,而当这个低优先级线程释放该资源时,优先级重新回到初始设定值
【学习笔记】RT-Thread内核视频学习笔记[1-16章]_第14张图片

继承优先级的线程避免了系统共享资源任何中间优先级线程抢占

11.3示例

。。。
就是两个线程去抢一个互斥量,优先级继承是系统中互斥量的特点,不需要做特殊处理

11.4启发

优先级翻转现象提醒编程人员对共享资源进行互斥访问的代码段应尽量短

第十二章 IPC-事件集,事件集的使用

12.1事件集工作机制

信号量主要用于“一对一”的线程同步;当需要“一对多”、“多对一"、”多对多“的同步时,就需要事件集来处理了。
RT-Thread中的事件集用一个32位无符号整型变量来表示,变量中的一个位代表一个事件,[bit如果是0的话,事件没有发生,如果为1,事件发生了]。线程通过”逻辑与“或”逻辑或”与一个或多个事件建立关联形成一个事件组合。

  • 事件的”逻辑“也称独立型同步,指的是线程与任何事件之一发成同步,只要有一个事件发生,即满足条件。
  • 事件的”逻辑“,也称关联型同步,指的是线程与若干事件都发生同步,只有这些事件全部发生,才满足条件。
    【学习笔记】RT-Thread内核视频学习笔记[1-16章]_第15张图片

12.2事件集控制块

在RT-Thread中,事件集控制块是操作系统用于管理事件的一个数据结构。

struct rt_event
{
    struct rt_ipc_object parent;                        /**< inherit from ipc_object */
    rt_uint32_t          set;                           /**< event set */
};
typedef struct rt_event *rt_event_t;
  • 定义静态事件集:struct rt_event static_evt
  • 定义动态事件集:rt_event_t dynamic_evt

12.3事件集的操作

  • 初始化与脱离
  • rt_err_t rt_event_init(rt_event_t event, const char *name, rt_uint8_t flag)
    • //RT_IPC_FLAG_FIFO/RT_IPC_FLAG_PRIO
  • rt_err_t rt_event_detach(rt_event_t event)
  • 创建与删除
  • rt_event_t rt_event_create(const char *name, rt_uint8_t flag)
  • rt_err_t rt_event_delete(rt_event_t event)
  • 发送事件
  • rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set)
  • 接收事件
  • rt_err_t rt_event_recv(rt_event_t event, rt_uint32_t set, rt_uint8_t option, rt_int32_t timeout, rt_uint32_t *recved)
    • option:RT_EVENT_FLAG_AND、RT_EVENT_FLAG_OR、RT_EVENT_FLAG_CLEAR
    • recved:定义一个变量接收event参数

12.4示例


/* 线程1入口函数 */
static void thread1_recv_event(void *param)
{
    rt_uint32_t e;

    /* 第一次接收事件,事件3或事件5任意一个可以触发线程1,接收完后清除事件标志 */
    if (rt_event_recv(&event, (EVENT_FLAG3 | EVENT_FLAG5),
                      RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR,
                      RT_WAITING_FOREVER, &e) == RT_EOK)
    {
        rt_kprintf("thread1: OR recv event 0x%x\n", e);
    }

    rt_kprintf("thread1: delay 1s to prepare the second event\n");
    rt_thread_mdelay(1000);

    /* 第二次接收事件,事件3和事件5均发生时才可以触发线程1,接收完后清除事件标志 */
    if (rt_event_recv(&event, (EVENT_FLAG3 | EVENT_FLAG5),
                      RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR,
                      RT_WAITING_FOREVER, &e) == RT_EOK)
    {
        rt_kprintf("thread1: AND recv event 0x%x\n", e);
    }
    rt_kprintf("thread1 leave.\n");
}

/* 线程2入口 */
static void thread2_send_event(void *param)
{
    rt_kprintf("thread2: send event3\n");
    rt_event_send(&event, EVENT_FLAG3);
    rt_thread_mdelay(200);

    rt_kprintf("thread2: send event5\n");
    rt_event_send(&event, EVENT_FLAG5);
    rt_thread_mdelay(200);

    rt_kprintf("thread2: send event3\n");
    rt_event_send(&event, EVENT_FLAG3);
    rt_kprintf("thread2 leave.\n");
}

【学习笔记】RT-Thread内核视频学习笔记[1-16章]_第16张图片

第十三章 IPC-邮箱的使用

13.1邮箱工作机制

RT-Thread操作系统的邮箱用于线程间通信,特点是开销比较低,效率较高。邮箱中的每一封邮件只能容纳固定的4个字节的内容(针对32位系统指针的大小即为4个字节,所以一封邮件敲好能够容纳一个指针)。
线程或中断服务例程把一封4字节长度的邮件发送到邮箱中,而其他需要的线程可以从邮箱中接收这些邮件并进行处理。
【学习笔记】RT-Thread内核视频学习笔记[1-16章]_第17张图片

13.2邮箱控制块

在RT-Thread中,邮箱控制块是操作系统用于管理邮箱的一个数据结构。

struct rt_mailbox
{
    struct rt_ipc_object parent;                        /**< inherit from ipc_object 从IPC对象中继承*/

    rt_uint32_t         *msg_pool;                      /**< start address of message buffer 有效消息缓冲区的指针*/

    rt_uint16_t          size;                          /**< size of message pool 容量*/

    rt_uint16_t          entry;                         /**< index of messages in msg_pool 邮箱中邮件的数目*/
    rt_uint16_t          in_offset;                     /**< input offset of the message buffer */
    rt_uint16_t          out_offset;                    /**< output offset of the message buffer */
    //邮箱的进出偏移量
    rt_list_t            suspend_sender_thread;         /**< sender thread suspended on this mailbox */
    //邮箱满的时候发邮件会导致线程挂起,这个成员用来记录挂起的线程的
};
typedef struct rt_mailbox *rt_mailbox_t;
  • 定义静态邮箱:struct rt_mailbox static_mb
  • 定义动态邮箱:rt_mailbox_t dynamic_mb

13.3邮箱操作API

  • 初始化和脱离
  • rt_err_t rt_mb_init(rt_mailbox_t mb, const char *name, void *msgpool, rt_size_t size, rt_uint8_t flag)
    • flag:RT_IPC_FLAG_FIFO / RT_IPC_FLASG_PRIO
  • rt_err_t rt_mb_detach(rt_mailbox_t mb)
  • 创建和删除
  • rt_mailbox_t rt_mb_create(const char *name, rt_size_t size, rt_uint8_t flag)
  • rt_err_t rt_mb_delete(rt_mailbox_t mb)
  • 发送邮件
  • rt_err_t rt_mb_send(rt_mailbox_t mb, rt_uint32_t value)
    • value:当大于4个字节,可以用传递地址的方式。
  • rt_err_t rt_mb_send_wait(rt_mailbox_t mb, rt_uint32_t value, rt_int32_t timeout)

send可在中断中使用,因为他不会等待,send_wait不行。

  • 接收邮件
  • rt_err_t rt_mb_recv(rt_mailbox_t mb, rt_uint32_t *value, rt_int32_t timeout)

13.4示例

    result = rt_mb_init(&mb,
                        "mbt",                      /* 名称是mbt */
                        &mb_pool[0],                /* 邮箱用到的内存池是mb_pool */
                        sizeof(mb_pool) / 4,        /* 邮箱中的邮件数目,因为一封邮件占4字节 */
                        RT_IPC_FLAG_FIFO);          /* 采用FIFO方式进行线程等待 */

第十四章 IPC-消息队列的使用

14.1消息队列的工作机制

消息队列是RT-Thread中另一种常用的线程间通信方式,消息队列是对邮箱的扩展。
消息队列能够接收来自线程或中断服务例程中发出的不固定长度的消息,并把消息缓存在自己的内存空间中,而其他线程能够从消息队列中读取相应的消息并进行对应的处理。
【学习笔记】RT-Thread内核视频学习笔记[1-16章]_第18张图片

  • 消息队列内都有内存池,内存池会根据一定的大小划分为消息框,消息队列的消息,都保存在内存框中,内存框以链表的形式组件起来的,
  • 消息队列支持紧急消息的发送,当是紧急消息的时候,会直接链接到链表头,等待线程就可以第一时间获取到
  • 消息队列满的时候,发送消息失败。

14.2消息队列控制块

在RT-Thread中,消息队列控制块是操作系统用于管理消息队列的一个数据结构。

struct rt_messagequeue
{
    struct rt_ipc_object parent;                        /**< inherit from ipc_object ;从IPC对象中继承*/

    void                *msg_pool;                      /**< start address of message queue,指向一段内存空间,保存消息队列的各个消息 */

    rt_uint16_t          msg_size;                      /**< message size of each message 消息队列中每一个消息框,消息的大小*/
    rt_uint16_t          max_msgs;                      /**< max number of messages 消息队列的容量/长度,消息队列所能容纳的数目*/

    rt_uint16_t          entry;                         /**< index of messages in the queue 当消息队列有消息进来,+1,有线程读取,-1*/

    void                *msg_queue_head;                /**< list head 消息头指针,指向第一条*/
    void                *msg_queue_tail;                /**< list tail 消息尾指针,指向最后一条*/
    void                *msg_queue_free;                /**< pointer indicated the free node of queue ,空闲指针指向未被使用的消息框*/
};
typedef struct rt_messagequeue *rt_mq_t;
  • msg_size:消息长度,在定义消息的时候,需要指定一个消息长度,假如消息长度是1,系统会自动按对齐的消息长度进行改写,在RTT的rtconfig中可以看到
#define RT_ALIGN_SIZE 4

定义了4字节对齐假如我们定义为1,对齐为4,定义为5,对齐成8,size必须是4的整数倍

  • max_msgs:消息队列的容量。首先有一个内存池msg_pool,假如初始化1024的大小,那么max_msgs = 1024/(msg_size+1个指针的大小)

指针的原因:消息队列以链表的形式存在,会有一个链表的指针,在32位系统下,一个指针4字节

  • 定义静态消息队列:struct rt_messagequeue static_mq
  • 定义动态消息队列:rt_mq_t dynamic_mq

12.3消息队列的操作

  • 初始化与脱离
  • rt_err_t rt_mq_init(rt_mq_t mq, const char * name, void * msgpool, rt_size_t msg_size, rt_size_t pool_size, rt_uint8_t flag)
  • rt_err_t rt_mq_detach(rt_mq_t mq)
  • 创建与删除
  • rt_mq_t rt_mq_create(const char * name, rt_size_t msg_size, rt_size_t max_msgs, rt_uint8_t flag)
  • rt_err_t rt_mq_delete(rt_mq_t mq)
  • 发动消息
  • rt_err_t rt_mq_send(rt_mq_t mq, void * buffer, rt_size_t size)
  • rt_err_t rt_mq_urgent(rt_mq_t mq, void * buffer, rt_size_t size)

一般消息send紧急消息urgent
紧急消息发送后,直接放到消息队列链表头部接收线程可以直接获取

  • 接收消息
  • rt_err_t rt_mq_recv(rt_mq_t mq, void * buffer, rt_size_t size, rt_int32_t timeout)

12.4示例

逐步发ABCD,和紧急消息I。
【学习笔记】RT-Thread内核视频学习笔记[1-16章]_第19张图片

/* 
 * Copyright (c) 2006-2018, RT-Thread Development Team 
 * 
 * SPDX-License-Identifier: Apache-2.0 
 * 
 * Change Logs: 
 * Date           Author       Notes 
 * 2018-08-24     yangjie      the first version 
 */ 

/*
 * 程序清单:消息队列例程
 *
 * 这个程序会创建2个动态线程,一个线程会从消息队列中收取消息;一个线程会定时给消
 * 息队列发送 普通消息和紧急消息。
 */
#include 

/* 消息队列控制块 */
static struct rt_messagequeue mq;
/* 消息队列中用到的放置消息的内存池 */
static rt_uint8_t msg_pool[2048];

ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;
/* 线程1入口函数 */
static void thread1_entry(void *parameter)
{
    char buf = 0;
    rt_uint8_t cnt = 0;

    while (1)
    {
        /* 从消息队列中接收消息 */
        if (rt_mq_recv(&mq, &buf, sizeof(buf), RT_WAITING_FOREVER) == RT_EOK)
        {
            rt_kprintf("thread1: recv msg from msg queue, the content:%c\n", buf);
            if (cnt == 19)
            {
                break;
            }
        }
        /* 延时50ms */
        cnt++;
        rt_thread_mdelay(50);
    }
    rt_kprintf("thread1: detach mq \n");
    rt_mq_detach(&mq);
}

ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
/* 线程2入口 */
static void thread2_entry(void *parameter)
{
    int result;
    char buf = 'A';    
    rt_uint8_t cnt = 0;
    
    while (1)
    {
        if (cnt == 8)
        {
            /* 发送紧急消息到消息队列中 */
            result = rt_mq_urgent(&mq, &buf, 1);
            if (result != RT_EOK)
            {
                rt_kprintf("rt_mq_urgent ERR\n");
            }
            else
            {
                rt_kprintf("thread2: send urgent message - %c\n", buf);
            }
        }
        else if (cnt >= 20)/* 发送20次消息之后退出 */
        {
            rt_kprintf("message queue stop send, thread2 quit\n");
            break;
        }
        else
        {
            /* 发送消息到消息队列中 */
            result = rt_mq_send(&mq, &buf, 1);
            if (result != RT_EOK)
            {
                rt_kprintf("rt_mq_send ERR\n");
            }

            rt_kprintf("thread2: send message - %c\n", buf);
        }
        buf++;
        cnt++;
        /* 延时5ms */
        rt_thread_mdelay(5);
    }
}

/* 消息队列示例的初始化 */
int msgq_sample(void)
{
    rt_err_t result;

    /* 初始化消息队列 */
    result = rt_mq_init(&mq,
                        "mqt",
                        &msg_pool[0],               /* 内存池指向msg_pool */
                        1,                          /* 每个消息的大小是 1 字节 */
                        sizeof(msg_pool),           /* 内存池的大小是msg_pool的大小 */
                        RT_IPC_FLAG_FIFO);          /* 如果有多个线程等待,按照先来先得到的方法分配消息 */

    if (result != RT_EOK)
    {
        rt_kprintf("init message queue failed.\n");
        return -1;
    }

    rt_thread_init(&thread1,
                   "thread1",
                   thread1_entry,
                   RT_NULL,
                   &thread1_stack[0],
                   sizeof(thread1_stack), 25, 5);
    rt_thread_startup(&thread1);

    rt_thread_init(&thread2,
                   "thread2",
                   thread2_entry,
                   RT_NULL,
                   &thread2_stack[0],
                   sizeof(thread2_stack), 25, 5);
    rt_thread_startup(&thread2);

    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(msgq_sample, msgq sample);

第十五章 软件定时器的使用

15.1软件定时器

软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上(系统滴答定时器)。软件定时器使系统能够提供不受数目限制的软件定时器服务
RT-Thread操作系统的软件定时器,以系统节拍(OS Tick)的时间长度为定时单位,提供了基于系统节拍整数倍的定时能力,即定时数值是OS Tick的整数倍。例如一个OS Tick是10ms,那么上层软件定时器只能提供10ms,20ms,100ms等事件精度的定时服务,而不是定时为15ms、20ms、35ms等。
当软件定时器所设定的定时事件到了后,会调用用户设置的定时器timeout回调函数,用户需要定时运行的程序会在回调函数中得到处理。

15.2定时器模式

根据timeout所在的上下文环境不同定时器分为两种模式

  • HARDTIMER模式
  • HARD_TIMER模式的定时器超时函数在中断上下文环境中执行,此模式在定时器初始化时指定。在中断上下文环境中执行时,对于超时函数的要求与中断服务例程的要求相同,执行事件应该尽量短,执行时不应该导致当前上下文挂起HARD_TIMER模式是RT-Thread软件定时器的默认方式。
  • SOFTTIMER模式
    SOFTTIMER模式的定时器超时函数在系统的timer线程的上下文中执行。通过宏定义RT_USING_SOFT[rtconfig.h]来决定是否启用该模式。当启用SOFTTIMER模式后,我们可以在定时器初始化时指定定时器工作在SOFTTIMER模式

15.3软件定时器控制块

在RT-Thread中,软件定时器控制块时操作系统用于管理软件定时器的一个数据结构

struct rt_timer
{
    struct rt_object parent;                            /**< inherit from rt_object */

    rt_list_t        row[RT_TIMER_SKIP_LIST_LEVEL];     /*定时器链表节点*/

    void (*timeout_func)(void *parameter);              /**超时函数的函数指针 < timeout function */
    void            *parameter;                         /**超时函数的入参< timeout function's parameter */

    rt_tick_t        init_tick;                         /**指定定时器超时时间< timer timeout tick */
    rt_tick_t        timeout_tick;                      /*超时时间到达时的系统时钟节拍计数*< timeout tick */
};
typedef struct rt_timer *rt_timer_t;
  • 定义静态软件定时器:struct rt_timer staric_timer
  • 定义动态软件定时器:rt_timer_t dynamic_timer

15.4软件定时器的操作

  • 初始化与脱离
  • void rt_timer_init(rt_timer_t timer, const char *name, void (*timeout)(void *parameter), void *parameter, rt_tick_t time, rt_uint8_t flag)

flag:

  • RT_TIMER_FLAG_ONE_SHOT:指定超时函数运行的次数定时一次就停止
  • RT_TIMER_FLAG_PERIODIC:周期性调用定时函数
  • RT_TIMER_FLAG_HARD_TIMER:HARD_TIMER模式
  • RT_TIMER_FLAG_SOFT_TIMER:SOFT_TIMER模式
    不指定默认HARD_TIMER模式
  • rt_err_t rt_timer_detach(rt_timer_t timer)
  • 创建与删除
  • rt_timer_t rt_timer_create(const char *name, void (*timeout)(void *parameter), void *parameter, rt_tick_t time, rt_uint8_t flag)

无内存创建时,函数返回0则失败

  • rt_err_t rt_timer_delete(rt_timer_t timer)
  • 启动定时器
  • rt_err_t rt_timer_start(rt_timer_t timer)
  • 停止定时器
  • rt_err_t rt_timer_stop(rt_timer_t timer)

15.5示例

创建了两个定时器,一个周期,一个单次,超时函数里面打印并退出

/* 定时器1超时函数 */
static void timeout1(void *parameter)
{
    rt_kprintf("periodic timer is timeout %d\n", cnt);

    /* 运行第10次,停止周期定时器 */
    if (cnt++ >= 9)
    {
        rt_timer_stop(timer1);
        rt_kprintf("periodic timer was stopped! \n");
    }
}

/* 定时器2超时函数 */
static void timeout2(void *parameter)
{
    rt_kprintf("one shot timer is timeout\n");
}

int timer_sample(void)
{
    /* 创建定时器1  周期定时器 */
    timer1 = rt_timer_create("timer1", timeout1,
                             RT_NULL, 10,
                             RT_TIMER_FLAG_PERIODIC);

    /* 启动定时器1 */
    if (timer1 != RT_NULL) rt_timer_start(timer1);

    /* 创建定时器2 单次定时器 */
    timer2 = rt_timer_create("timer2", timeout2,
                             RT_NULL,  30,
                             RT_TIMER_FLAG_ONE_SHOT);

    /* 启动定时器2 */
    if (timer2 != RT_NULL) rt_timer_start(timer2);
    return 0;
}

第十六章 内存池的使用

16.1内存池介绍

  • 动态内存堆可以分配任意大小的内存块,非常灵活和方便。但其存在明显的缺点:一是分配效率不高,在每次分配时,都要进行空闲内存块查找;二是容易产生内存碎片

为了提高内存分配的效率,并且避免内存碎片,RT-Thread提供了另外一种内存管理方法:内存池(Memory Pool)

  • 内存池时一种内存分配方式,用于分配大量大小相同的小内存块。使用内存池可以极大地加快内存分配与释放的速度,且能尽量避免内存碎片化。

RT-Thread的内存池支持线程挂起功能,当内存池中无空闲内存块时,申请线程会被挂起,直到内存池中有新的可用内存块,再将挂起的线程唤醒。基于这个特点内存池非常适合需要通过内存资源进行同步的场景。

16.2内存池工作机制

内存池在创建时先从系统中获取一大块内存(静态或动态),然后分成相同大小的多个小内存块,这些小内存块通过链表连接起来(此链表也称空闲链表)。线程每次申请分配内存块的时候,系统从空闲链表中取出链表上第一个内存块,提供给申请者。

我们在一个系统中,可以创建多个内存池,它在创建的时候,是从系统中获取一大块内存,获取到的内存可以是静态的形式,比如我们先定义一块数组,将其作为内存池的内存分配资源,也可以通过动态内存堆的方式,申请一个动态的地址当作内存资源。
将一个大的内存块分成大小相同的小的内存块,小内存块在内存池中是通过链表的方式链接起来的,线程可以向内存申请小内存块,取出链表头开始的第一个空闲内存块;当线程使用完内存块以后,将内存块释放,返回给内存池中

【学习笔记】RT-Thread内核视频学习笔记[1-16章]_第20张图片

16.3内存池控制块

在RT-Thread中,内存池控制块是操作系统用于管理内存池的一个数据结构。

struct rt_mempool
{
    struct rt_object parent;                            /**< inherit from rt_object ,继承于rt_object这个对象*/

    void            *start_address;                     /**< memory pool start ,申请内存空间的内存地址*/
    rt_size_t        size;                              /**< size of memory pool ,记录大内存块的大小*/

    rt_size_t        block_size;                        /**< size of memory blocks ,记录小内存块的大小*/
    rt_uint8_t      *block_list;                        /**< memory blocks list ,小内存块列表*/

    rt_size_t        block_total_count;                 /**< numbers of memory block ,整个内存池中一共有多少小内存块*/
    rt_size_t        block_free_count;                  /**< numbers of free memory block ,当前空闲内存块的个数  */

    rt_list_t        suspend_thread;                    /**< threads pended on this resource,由于内存池支持线程挂起,所以此参数用来记录挂起在内存池上的线程列表 */
    rt_size_t        suspend_thread_count;              /**< numbers of thread pended on this resource ,挂起在内存池上的线程数目*/
};
typedef struct rt_mempool *rt_mp_t;

  • 定义静态内存池:struct rt_mempool static_mp;
  • 定义动态内存池:rt_mp_t dynamic_mp

16.4内存池的操作

  • 初始化与脱离
  • rt_err_t rt_mp_init(struct rt_mempool * mp, const char * name, void * start, rt_size_t size, rt_size_t block_size)

    mp:内存控制块的地址。
    name:内存池的名称
    start:内存地址,可以是一个数组,这就是大内存块。
    size:大内存块的大小
    block_size:小内存块大小。
    ps:消息队列中,消息的大小按系统定义的对齐格式(例如4字节对齐)对齐。如果block_size输入1,则系统自动定为4.
    可以计算出初始化的内存池中,有多少内存块。计算公式为:cnt=size/(系统的block_size+4)。 4是因为内存是以链表的方式链接起来的,所以每一个内存块多一个指针的大小

  • rt_err_t rt_mp_detach(struct rt_mempool * mp)
  • 创建与删除
  • rt_mp_t rt_mp_create(const char * name, rt_size_t block_count, rt_size_t block_size)

    实现动态内存的时候,不需要提前定义一个内存块,她所需要的内存块,会通过动态的方式,从系统动态内存堆里面分配。如果系统当前有可用的内存,会返回内存控制块的指针,如果系统中没有可用的内存,那么会返回null空指针

  • rt_err_t rt_mp_delete(rt_mp_t mp)
  • 申请内存块
  • void * rt_mp_alloc(rt_mp_t mp, rt_int32_t time)
  • mp,指明从哪个系统中去申请内存块。如果申请不到,系统挂起
  • 释放内存块
  • void rt_mp_free(void * block)
//线程1申请48次,线程2释放2次,
static rt_uint8_t *ptr[50];
static rt_uint8_t mempool[4096];
static struct rt_mempool mp;
/* 线程1入口 */
static void thread1_mp_alloc(void *parameter)
{
    int i;
    for (i = 0 ; i < 50 ; i++)
    {
        if (ptr[i] == RT_NULL)
        {
            /* 试图申请内存块50次,当申请不到内存块时,
               线程1挂起,转至线程2运行 */
            ptr[i] = rt_mp_alloc(&mp, RT_WAITING_FOREVER);
            if (ptr[i] != RT_NULL)
                rt_kprintf("allocate No.%d\n", i);
        }
    }
}

/* 线程2入口,线程2的优先级比线程1低,应该线程1先获得执行。*/
static void thread2_mp_release(void *parameter)
{
    int i;

    rt_kprintf("thread2 try to release block\n");
    for (i = 0; i < 50 ; i++)
    {
        /* 释放所有分配成功的内存块 */
        if (ptr[i] != RT_NULL)
        {
            rt_kprintf("release block %d\n", i);
            rt_mp_free(ptr[i]);
            ptr[i] = RT_NULL;
        }
    }
}
    rt_mp_init(&mp, "mp1", &mempool[0], sizeof(mempool), 80);       //4096/(80+4)=48个内存块

你可能感兴趣的:(学习笔记)