RT-Thread线程管理

  1. 1. 线程控制块
    1. 1.1. 一些重要属性
    2. 1.2. 线程状态切换
    3. 1.3. 系统线程
      1. 1.3.1. 空闲线程
      2. 1.3.2. 主线程
  2. 2. 线程的管理方式
    1. 2.1. 创建和删除线程
    2. 2.2. 初始化和脱离线程
    3. 2.3. 启动线程
    4. 2.4. 获得当前线程
    5. 2.5. 让出CPU
    6. 2.6. 线程睡眠
    7. 2.7. 控制线程
    8. 2.8. 设置和删除空闲钩子
    9. 2.9. 设置调度器钩子

RT-Thread的线程调度为优先级抢占式调度。当调度器进行线程切换时,会切换至就绪任务中优先级最高的那个任务,如果中断服务函数(ISR)让一个更高优先级的任务满足运行条件,中断完成时,被中断的线程挂起,优先级高的线程开始运行。对于同优先级的任务,RT-Thread采用时间片轮转调度。

线程控制块

RT-Thread中,线程控制块是操作系统用于管理线程的数据结构,它会存放线程的一些信息,例如优先级、线程名称、线程堆栈指针等,也包含线程与线程之间连接用的链表结构,详细定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/* 线程控制块 */
struct rt_thread
{
/* rt 对象 */
char name[RT_NAME_MAX]; /* 线程名称 */
rt_uint8_t type; /* 对象类型 */
rt_uint8_t flags; /* 标志位 */

rt_list_t list; /* 对象列表 */
rt_list_t tlist; /* 线程列表 */

/* 栈指针与入口指针 */
void *sp; /* 栈指针 */
void *entry; /* 入口函数指针 */
void *parameter; /* 参数 */
void *stack_addr; /* 栈地址指针 */
rt_uint32_t stack_size; /* 栈大小 */

/* 错误代码 */
rt_err_t error; /* 线程错误代码 */
rt_uint8_t stat; /* 线程状态 */

/* 优先级 */
rt_uint8_t current_priority; /* 当前优先级 */
rt_uint8_t init_priority; /* 初始优先级 */
rt_uint32_t number_mask;

......

rt_ubase_t init_tick; /* 线程初始化计数值 */
rt_ubase_t remaining_tick; /* 线程剩余计数值 */

struct rt_timer thread_timer; /* 内置线程定时器 */

void (*cleanup)(struct rt_thread *tid); /* 线程退出清除函数 */
rt_uint32_t user_data; /* 用户数据 */
};

一些重要属性

  • 线程栈:用于储存线程的上下文信息以及局部变量。

  • 线程状态:

    • 初始状态 RT_THREAD_INIT:当线程初始化完毕时处于初始状态,不参与调度
    • 就绪状态 RT_THREAD_READY:在就绪状态下,线程会参与调度,按照优先级排队,等待被执行
    • 运行状态 RT_THREAD_RUNNING:当前线程正在运行
    • 挂起状态 RT_THREAD_SUSPEND:也称阻塞态。因不可用而挂起等待,或因线程主动延时而挂起,在此状态下,线程不参与调度。
    • 关闭状态 RT_THREAD_CLOSE:当线程运行结束时处于关闭状态,不参与调度
  • 线程优先级:RT-Thread最高支持256个线程优先级,数值越小的优先级越高,0为最高优先级,可选择的优先级个数有8、32、256,对于Cortex-M系列,一般采用32个优先级

  • 时间片:时间片仅对优先级相同的就绪线程有效,时间片决定了任务单次运行的时长,单位为系统节拍(OS Tick)

  • 线程的入口函数:是实现线程功能的函数,一般有两种形式:

    • 无限循环模式,类似裸机的函数形式:需注意,RT-Thread采用了优先级,如果一个线程陷入了死循环,那么优先级比它低的函数都不能被执行,所以一个线程不能陷入死循环,必须要有主动让出CPU的操作,如调用延时函数或者主动挂起。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      void thread_entry(void* paramenter)
      {
      while (1)
      {
      /* 等待事件的发生 */

      /* 对事件进行服务、进行处理 */
      }
      }
    • 顺序执行或有限次循环模式:执行完毕后,线程会被系统自动删除

      1
      2
      3
      4
      5
      6
      7
      8
      static void thread_entry(void* parameter)
      {
      /* 处理事务 #1 */

      /* 处理事务 #2 */

      /* 处理事务 #3 */
      }
  • 线程错误码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #define RT_EOK           0 /* 无错误     */
    #define RT_ERROR 1 /* 普通错误 */
    #define RT_ETIMEOUT 2 /* 超时错误 */
    #define RT_EFULL 3 /* 资源已满 */
    #define RT_EEMPTY 4 /* 无资源 */
    #define RT_ENOMEM 5 /* 无内存 */
    #define RT_ENOSYS 6 /* 系统不支持 */
    #define RT_EBUSY 7 /* 系统忙 */
    #define RT_EIO 8 /* IO 错误 */
    #define RT_EINTR 9 /* 中断系统调用 */
    #define RT_EINVAL 10 /* 非法参数 */

线程状态切换

线程状态转换图

系统线程

系统线程是由系统创建的线程,用户线程是由用户调用函数创建的线程,在RT-Thread中,系统线程有空闲线程主线程

空闲线程

空闲线程是系统创建的优先级最低的线程,永远为就绪态。当系统中无其他就绪线程存在时,调度器将调度到空闲线程,通常为一个死循环,且永远不能被挂起。另外,空闲线程在RT-Thread中还有另一个作用:

若某线程运行完毕,系统会自动删除线程,自动执行rt_thread_exit()函数,将线程从系统就绪队列中删除,再将线程的状态更改为关闭状态,不再参与系统调度,然后挂入rt_thread_defunct僵尸队列(资源未回收,处于关闭状态的线程队列),最后空闲线程会回收被删除线程的资源。

空闲线程也提供了接口来运行用户设置的钩子函数,在空闲线程运行时会调用该钩子函数,适合钩入功耗管理、看门狗喂狗等工作。

主线程

在系统启动时,系统会创建main线程,它的入口函数为main_thread_entry(),用户的应用入口函数main()就是从这里真正开始的,系统调度器启动后,main 线程就开始运行,在 main() 函数里添加自己的应用程序初始化代码。

线程的管理方式

线程相关操作

创建和删除线程

该函数用于创建一个动态线程:

1
2
3
4
5
6
7
// 创建成功返回线程句柄,失败返回RT_NULL
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_thread_delete()删除:

1
2
// 成功返回RT_EOK,失败返回RT_ERROR
rt_err_t rt_thread_delete(rt_thread_t thread);

值得注意的是,rt_thread_delete()函数只是让线程从调度队列中脱离,放入僵尸队列中,并没有真正的释放资源,在空闲线程运行时,才会释放资源。

初始化和脱离线程

该函数用于初始化一个静态线程:

1
2
3
4
5
6
7
8
9
// 成功返回RT_EOK,失败返回RT_ERROR
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_thread_detach()使线程对象在调度队列和内核对象管理器中脱离:

1
2
// 成功返回RT_EOK,失败返回RT_ERROR
rt_err_t rt_thread_detach (rt_thread_t thread);

启动线程

当一个线程处于初始状态,需调用rt_thread_startup()函数把线程放入调度队列,让线程进入就绪状态:

1
2
// 成功返回RT_EOK,失败返回RT_ERROR
rt_err_t rt_thread_startup(rt_thread_t thread);

如果新启动的线程比当前线程优先级高,将立即切换到这个线程。

获得当前线程

在系统运行的过程中,一个函数有可能被多个线程运行,可使用以下函数获得当前执行函数的线程句柄:

1
2
// 返回当前运行的线程句柄,如返回RT_NULL代表调度器还未启动
rt_thread_t rt_thread_self(void);

让出CPU

当线程的时间片用完或主动让出CPU时,它将不再占用CPU,调度器会选择相同优先级的下一个线程运行。当函数调用这个函数时,会主动让出CPU:

1
rt_err_t rt_thread_yield(void);

如当前优先级只有这一个线程,则这个线程继续运行。

线程睡眠

1
2
3
rt_err_t rt_thread_sleep(rt_tick_t tick);
rt_err_t rt_thread_delay(rt_tick_t tick); // 以上两个单位为OS Tick
rt_err_t rt_thread_mdelay(rt_int32_t ms); // 单位为毫秒

这三个函数接口的作用相同,可以使当前线程挂起一定时间,当时间结束后,线程会恢复就绪状态。

控制线程

可以通过以下函数动态修改线程的优先级:

1
rt_err_t rt_thread_control(rt_thread_t thread, rt_uint8_t cmd, void* arg);
CMD说明
RT_THREAD_CTRL_CHANGE_PRIORITY动态改变线程优先级
RT_THREAD_CTRL_STARTUP运行一个线程,等同于rt_thread_startup()
RT_THREAD_CTRL_CLOSE关闭一个线程,等同于rt_thread_delete()

设置和删除空闲钩子

当系统执行空闲线程时,指定的钩子函数会被调用:

1
2
rt_err_t rt_thread_idle_sethook(void (*hook)(void));
rt_err_t rt_thread_idle_delhook(void (*hook)(void));

注意:空闲线程是一个永远为就绪态的线程,因此钩子函数不能使空闲线程处于挂起状态,例如rt_thread_sleep()rt_sem_take()等函数都不能使用。

设置调度器钩子

当系统发生上下文切换时会调用设置的钩子函数:

1
2
// from为要挂起的线程控制块指针,to为要运行的线程控制块指针
void rt_scheduler_sethook(void (*hook)(struct rt_thread* from, struct rt_thread* to));
本网站所有文章除特别声明外,均采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。