4.系统线程
系统线程是指由系统创建的线程,用户线程是由用户程序调用线程管理接口创建的线程,在RT-Thread 内核中的系统线程有空闲线程和主线程。
(1)空闲线程
空闲线程
是系统创建的最低优先级的线程,线程状态永远为就绪态
。当系统中无其他就绪线程存在时,调度器将调度到空闲线程,它通常是一个死循环,且永远不能被挂起。
另外,空闲线程在RT-Thread 也有着它的特殊用途:
- 若某线程运行完毕,系统将自动删除线程:自动执行rt_thread_exit() 函数,先将该线程从系统就绪队列中删除,再将该线程的状态更改为关闭状态,不再参与系统调度,然后挂入rt_thread_defunct 僵尸队列(资源未回收、处于关闭状态的线程队列)中,最后空闲线程会回收被删除线程的资源。
- 空闲线程也提供了接口来运行用户设置的钩子函数,在空闲线程运行时会调用该钩子函数,适合钩入功耗管理、看门狗喂狗等工作。(关于钩子函数和看门狗不懂的可以看这里)
(2) 主线程
在系统启动时,系统会创建main 线程,它的入口函数为main_thread_entry(),用户的应用入口函数main() 就是从这里真正开始的,系统调度器启动后,main 线程就开始运行。
过程如下图,用户可以在main() 函数里添加自己的应用程序初始化代码。
四、线程的管理方式
可以使用rt_thread_create() 创建一个动态线程,使用rt_thread_init() 初始化一个静态线程。
动态线程与静态线程的区别是:动态线程是系统自动从动态内存堆上分配栈空间与线程句柄(初始化heap 之后才能使用create 创建动态线程),静态线程是由用户分配栈空间与线程句柄。
下图描述了线程的相关操作,包含:创建/ 初始化线程、启动线程、运行线程、删除/ 脱离线程。
1.创建和删除线程
(1)创建线程
一个线程要成为可执行的对象,就必须由操作系统的内核来为它创建一个线程。可以通过如下的接口创建一个动态线程:
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);
调用这个函数时,系统会从动态堆内存中分配一个线程句柄以及按照参数中指定的栈大小从动态堆内存中分配相应的空间。分配出来的栈空间是按照rtconfig.h 中配置的RT_ALIGN_SIZE 方式对齐。
线程创建rt_thread_create() 的参数和返回值见下图:
(2)删除线程
对于一些使用rt_thread_create() 创建出来的线程,当不需要使用,或者运行出错时,我们可以使用下面的函数接口来从系统中把线程完全删除掉:
rt_err_t rt_thread_delete(rt_thread_t thread);
调用该函数后,线程对象将会被移出线程队列并且从内核对象管理器中删除,线程占用的堆栈空间也会被释放,收回的空间将重新用于其他的内存分配。实际上,用rt_thread_delete() 函数删除线程接口,仅仅是把相应的线程状态更改为RT_THREAD_CLOSE 状态,然后放入到rt_thread_defunct 队列中;而真正的删除动作(释放线程控制块和释放线程栈)需要到下一次执行空闲线程时,由空闲线程完成最后的线程删除动作。
线程删除rt_thread_delete() 接口的参数和返回值见下图:
这个函数仅在使能了系统动态堆时才有效(即RT_USING_HEAP 宏定义已经定义了)。
2.初始化和脱离线程
(1)初始化线程
线程的初始化
可以使用下面的函数接口完成,来初始化静态线程对象:
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);
(2)脱离线程
对于用rt_thread_init() 初始化的线程,使用rt_thread_detach() 将使线程对象在线程队列和内核对象管理器中被脱离。线程脱离函数如下:
rt_err_t rt_thread_detach (rt_thread_t thread);
参数 | 描述 |
thread | 线程句柄,它应该是由rt_thread_init 进行初始化的线程句柄。 |
返回 | — |
RT_EOK | 线程脱离成功 |
-RT_ERROR | 线程脱离失败 |
3.启动线程
创建(初始化)的线程状态处于初始状态,并未进入就绪线程的调度队列,我们可以在线程初始化/创建成功后调用下面的函数接口让该线程进入就绪态:
rt_err_t rt_thread_startup(rt_thread_t thread);
当调用这个函数时,将把线程的状态更改为就绪状态,并放到相应优先级队列中等待调度。如果新启
动的线程优先级比当前线程优先级高,将立刻切换到这个线程。
4.获得当前线程
在程序的运行过程中,相同的一段代码可能会被多个线程执行,在执行的时候可以通过下面的函数接口获得当前执行的线程句柄:
rt_thread_t rt_thread_self(void);
5.使线程出让处理器资源
当前线程的时间片用完或者该线程主动要求让出处理器资源时,它将不再占有处理器,调度器会选择相同优先级的下一个线程执行。线程调用这个接口后,这个线程仍然在就绪队列中。
线程让出处理器使用下面的函数接口:
rt_err_t rt_thread_yield(void);
调用该函数后,当前线程首先把自己从它所在的就绪优先级线程队列中删除,然后把自己挂到这个优先级队列链表的尾部,然后激活调度器进行线程上下文切换(如果当前优先级只有这一个线程,则这个线程继续执行,不进行上下文切换动作)。
6.使线程睡眠
在实际应用中,我们有时需要让运行的当前线程延迟一段时间,在指定的时间到达后重新运行,这就叫做“线程睡眠”。
线程睡眠可使用以下三个函数接口:
rt_err_t rt_thread_sleep(rt_tick_t tick); rt_err_t rt_thread_delay(rt_tick_t tick); rt_err_t rt_thread_mdelay(rt_int32_t ms);
7.挂起和恢复线程
(1)线程挂起
- 当线程调用rt_thread_delay() 时,线程将主动挂起;当调用rt_sem_take(),rt_mb_recv() 等函数时,资源不可使用也将导致线程挂起。
- 处于挂起状态的线程,如果其等待的资源超时(超过其设定的等待时间),那么该线程将不再等待这些资源,并返回到就绪状态;或者,当其他线程释放掉该线程所等待的资源时,该线程也会返回到就绪状态。
线程挂起
使用下面的函数接口:
rt_err_t rt_thread_suspend (rt_thread_t thread);
!!! note “注意事项” 通常不应该使用这个函数来挂起线程本身, 如果确实需要采用rt_thread_suspend() 函数挂起当前任务, 需要在调用rt_thread_suspend() 函数后立刻调用rt_schedule() 函数进行手动的线程上下文切换。
(2)恢复线程
恢复线程就是让挂起的线程重新进入就绪状态,并将线程放入系统的就绪队列中;如果被恢复线程在
所有就绪态线程中,位于最高优先级链表的第一位,那么系统将进行线程上下文的切换。
线程恢复
使用下面的函数接口:
rt_err_t rt_thread_resume (rt_thread_t thread);
8.控制线程
当需要对线程进行一些其他控制时,例如动态更改线程的优先级,可以调用如下函数接口:
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));
参数 | 描述 |
hook | 设置/删除的钩子函数 |
返回 | — |
RT-EOK | 设置/删除成功 |
-RT_EFULL | 设置失败 |
-RT_ENOSYS | 删除失败 |
!!! note “注意事项” 空闲线程是一个线程状态永远为就绪态的线程,因此设置的钩子函数必须保证空闲线程在任何时刻都不会处于挂起状态,例如rt_thread_delay(),rt_sem_take() 等可能会导致线程挂起的函数都不能使用。
10.设置调度器钩子
在整个系统的运行时,系统都处于线程运行、中断触发- 响应中断、切换到其他线程,甚至是线程间的切换过程中,或者说系统的上下文切换是系统中最普遍的事件。有时用户可能会想知道在一个时刻发生了什么样的线程切换,可以通过调用下面的函数接口设置一个相应的钩子函数。
在系统线程切换时,这个钩子函数将被调用:
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);
参数 | 描述 |
from | 表示系统所要切换出的线程控制块指针 |
to | 表示系统所要切换到的线程控制块指针 |
!!! note “注意事项” 请仔细编写你的钩子函数,稍有不慎将很可能导致整个系统运行不正常(在这个 钩子函数中,基本上不允许调用系统API,更不应该导致当前运行的上下文挂起)。