在之前的学习中,我们对RT-Thread的内核基础作了初步的了解。其中关于线程的部分,介绍了线程调度、线程同步以及线程间通信等内容。这一篇博客将详细介绍在RT-Thread中线程的管理工作。
线程管理
线程是实现任务的载体,也是RT-Thread中最基本的调度单位,它描述了一个任务执行的运行环境(也称作上下文,具体来讲就是各个变量和数据,包括所有的寄存器变量、堆栈、内存信息等)以及该任务所处的优先等级。对线程的管理主要就是对线程进行管理和调度。在线程管理的学习中,心中要保持一些问题,例如:
- 线程有哪些状态?
- 怎么创建一个线程?
- 为什么会存在空闲线程?
- 线程之间的优先级是怎么使用的?
线程管理的功能特点
在RT-Thread的系统中,主要包括两种线程:系统线程(内核创建)和用户线程(应用程序创建)。这两类线程都会从内核对象容器中分配线程对象,当线程被删除时,也会从对象容器中删除。
在之前的博客中,我们也提到了RT-Thread的线程调度方式:基于优先级的全抢占式调度方法,也即当前只要该线程的优先级最高,那么就抢占CPU资源。
线程的工作机制
线程控制块
在RT-Thread中,线程控制块有结构体struct rt_thread表示,是操作系统用于管理线程的一个数据结构。其定义如下:
/* 线 程 控 制 块 */
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; /* 用 户 数 据 可 以 提 供 类 似 线 程 私 有 数 据 的 实 现 */
};
线程重要属性
1 线程栈
RT_Thread线程具有独立的栈,当线程切换的时候,会将当前线程执行的上下文信息存到栈中,当线程恢复的时候,再从栈中取出上下文信息,进行恢复。
线程栈还存放局部变量。函数中的局部变量初始化时从寄存器分配,当该函数再调用另外一个函数的时候,这些局部变量都将放入线程栈中。
线程的增长方向与芯片的架构密切相关,RT-Thread 3.1.0以前的版本,均只支持栈由高地址向低地址增长的方式。对于ARM Cortex-M架构,线程栈的构造如图所示:
对于线程栈的大小,可以根据不同的MCU进行预先设置,也可以通过FinSH命令list_thread列出从线程启动到当前线程所使用栈的大小,然后根据实际情况进行动态调整。
2 线程状态
在RT-Thread中,从运行的过程上划分,线程可以分为5种状态:
- 初始状态:线程刚创建还没参与调度。
- 就绪状态:排队等待执行。
- 运行状态:线程当前正在运行。
- 挂起状态:也称阻塞态,可能是因为资源不可用,也可能是线程主动延时一段时间而挂起,此时不参与调度。
- 关闭状态:线程结束,不参与调度。
3 线程优先级
在之前的博客中有提到,优先级最大个数256,也可自己配置,最高优先级为0。最低优先级默认分配给空闲线程使用。
4 时间片
时间片以一个系统节拍(OS Tick)为单位,运用在相同优先级的线程调度中。每个线程有自己的时间片,则CPU通过时间片轮转的方式执行具有相同优先级的线程。
5 线程的入口函数–>线程控制块中的entry,void thread_entry(void* paramenter);
一般有两种代码形式: - 无限循环模式(while(1))
在实时系统中,线程通常是被动式的,这个是由实时系统的特性决定的(等待外界事件发生,而后进行相应的服务)。该模式设计的目的就是让这个线程一直被系统循环调度运行,永不删除。
- 顺序执行或有限循环模式(顺序语句、do whlie() 或 for() 循环等)
在执行完毕后,线程将被系统自动删除。
6 线程错误码
一个线程就是一个执行场景,错误码是与执行环境密切相关的,所以每个线程配备了一个变量用于保存错误码,线程的错误码有以下几种:
#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 /* 非 法 参 数 */
线程状态切换
线程的5种转换状态如图所示:
注意:在RT-Thread中,就绪状态其实和运行状态是等同的。
系统线程(空闲线程和主线程)
系统线程由内核创建,分为空闲线程和主线程。
- 空闲线程
- 最低优先级
- 永远就绪态
- 无其他线程时,调用该线程
- 通常死循环
- 永远不能被挂起
- 可以用来回收被删除线程的资源
- 提供了接口来运行用户设置的钩子函数,适合钩入功耗管理、看门喂狗等工作。
- 主线程
main线程,入口函数为:main_thread_entry(),系统调度器启动后,main()开始运行。用户可以在main()函数中添加自己的应用程序初始化代码。
线程的管理方式
线程的相关操作:创建/初始化线程、启动线程、运行线程、删除/脱离线程。
- rt_thread_create()创建动态线程,系统自动从动态内存堆上分配栈空间与线程句柄(初始化heap之后才能使用create创建动态线程)。
- rt_thread_init() 创建静态线程,由用户分配栈空间与线程句柄。
具体来讲就是线程的一些操作函数了。
线程的创建和删除
静态线程的创建:
rt_err_t rt_thread_init(struct rt_thread* thread,//线程句柄,由用户创建
//线程名字,的最大长度由 rtconfig.h 中定义的 RT_NAME_MAX 宏指定
const char* name,
//线程入口函数,也即该线程干嘛使的,parameter就是线程入口函数的参数
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_detach (rt_thread_t thread);
动态对象的创建:
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_delete(rt_thread_t thread);
调用该函数后,线程对象将会被移出线程队列并且从内核对象管理器中删除,线程占用的堆栈空间也会被释放,收回的空间将重新用于其他的内存分配。实际上,用 rt_thread_delete() 函数删除线程接口,仅仅是把相应的线程状态更改为 RT_THREAD_CLOSE 状态,然后放入到 rt_thread_defunct 队列中;而真正的删除动作(释放线程控制块和释放线程栈)需要到下一次执行空闲线程时,由空闲线程完成最后的线程删除动作。
线程的启动
线程并不是创建完之后就进入调度队列的,而只是处于初始化的状态,这时需要一个启动的动作,让该线程处于就绪态。这就用到了线程的启动函数:
rt_err_t rt_thread_startup(rt_thread_t thread);
当线程处于就绪态之后,剩下的步骤就是按照线程的调度原则进行调度了。
线程的获取
在程序运行的时候,相同的一块代码,可能会被多个线程执行。在执行的时候就可以通过线程的获取来得到当前执行线程的句柄。使用的函数是:
rt_thread_t rt_thread_self(void);
成功:当前运行的线程句柄
失败:调度器还未启动
使线程让出处理器资源
在线程执行主动让出处理器资源或者该线程当前执行的时间到了,那么它将不再占用处理器,调度器回选择相同优先级的下一个线程执行。在调用过这个接口之后,线程仍出就绪队列中。
rt_err_t rt_thread_yield(void);
调用该函数后,当前线程首先把自己从它所在的就绪优先级线程队列中删除,然后把自己挂到这个优先级队列链表的尾部,然后激活调度器进行线程上下文切换(如果当前优先级只有这一个线程,则这个线程继续执行,不进行上下文切换动作)。
rt_thread_yield() 函数和 rt_schedule() 函数比较相像,但在有相同优先级的其他就绪态线程存在时,系统的行为却完全不一样。执行 rt_thread_yield() 函数后,当前线程被换出,相同优先级的下一个就绪线程将被执行。而执行 rt_schedule() 函数后,当前线程并不一定被换出,即使被换出,也不会被放到就绪线程链表的尾部,而是在系统中选取就绪的优先级最高的线程执行(如果系统中没有比当前线程优先级更高的线程存在,那么执行完 rt_schedule() 函数后,系统将继续执行当前线程)。
使线程睡眠
也即让运行的当前线程延迟一段时间,然后再指定的时间到达后重新运行。可以调用如下接口:
rt_err_t rt_thread_sleep(rt_tick_t tick);以一个OS Tick为单位
rt_err_t rt_thread_delay(rt_tick_t tick);
rt_err_t rt_thread_mdelay(rt_int32_t ms);//以1ms为单位
线程的挂起与恢复
当使用的资源不可用或者等待资源超时的时候,会有线程的挂起,函数为:
rt_err_t rt_thread_suspend (rt_thread_t thread);
但是官方文档上讲,并不推荐大家使用。
讲挂起的线程重新恢复到就绪态使用的函数是:
rt_err_t rt_thread_resume (rt_thread_t thread);
线程的控制
当需要对线程进行一些其他控制时,例如动态更改线程的优先级,可以调用如下函数接口:
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() 函数调用。
设置和删除空闲钩子
之前的学习中有提到空闲线程,并且了解到空闲线程具有最低的优先级, 并且一般不停的循环执行。那么空闲钩子函数就是再空闲线程中执行的一种函数。这些函数可以做一些诸如系统指示灯之类的工作。需要注意的是:由于空闲线程是一种一直处于就绪态的线程,所以设置的钩子函数必须保证空闲线程再任何时候都不能被挂起。调用的函数是:
rt_err_t rt_thread_idle_sethook(void (*hook)(void));
rt_err_t rt_thread_idle_delhook(void (*hook)(void));
设置调度器钩子
调度器钩子的作用在于帮助我们了解到再一个时刻发生了什么样的线程切换。因为再系统运行的时候,一直处于线程运行、中断触发-响应中断、切换线程这样的一个过程中。所以设置调度器钩子可以帮助我们更加清晰地知道从哪个线程切换到了哪个线程,有助于我们进行debug。调用地函数是:
void rt_scheduler_sethook(void (*hook)(struct rt_thread* from, struct rt_thread* to)
);
代码里面的参数hook就是用户定义的钩子函数指针。
对于钩子函数hook()的声明:
void hook(struct rt_thread* from, struct rt_thread* to);
需要注意的是:在钩子函数中,基本不允许调用系统的API,更不能导致当前运行的上下文挂起!!!
最后就是线程应用的示例,这里就不多说了。感兴趣的可以参考RT-Thread的官方文档: https://www.rt-thread.org/document/site/
最后啰嗦一句:写博客不是目的, 目的是学习并尽可能地记住一些有用的知识, 接下来一段时间还得不断回顾之前学到的内容, 这样才能进一步加深记忆。线程管理部分就到此结束了, 如有谬误, 还望不吝赐教。