2.1实时系统的需求
实时性:固定的时间内正确地对外部事件做出响应
2.2线程调度器
RT-Thread中提供的线程调度器是基于优先级的全抢占式调度
除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的,包括线程调度器自身。
2.2.1优先级:
- 共256个优先级,也就是包含了一个共256个优先级队列的数组
- 0为最高优先级,255分配给空闲线程使用,一般用户不使用
- 资源比较紧张的系统中,可以根据实际情况选择只支持8个或32个 优先级的系统配置
2.2.2处理措施:
当有优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。
2.2.3双向环形链表:
在RT-Thread调度器的实现中,包含了一个共256个优先级队列的数组,每个数组元素中放置相同优先级链表的表头。这些相同优先级的列表形成一个双向环形链表,最低优先级线程链表一般只包含一个idle线程
2.2.4线程就绪优先级队列
线程A和线程B在优先级队列1#中,优先级高于线程C,线程C必须要等待优先级队列1#的中所有线程(因为阻塞)都让出处理器后才能得到执行。
查找最高优先级线程的过程决定了调度时间是否具有确定性,故操作系统如果只是具备了高优先级任务能够“立即”获得处理器并得到执行的特点,那么它仍然不算是实时操作系统。
2.2.5基于位图的优先级算法:
RT-Thread内核中采用了基于位图的优先级算法(时间复杂度O(1),即与就绪线程的多少无关),通过位图的定位快速的获得优先级最高的线程。
2.2.6相同优先级线程:
允许创建相同优先级的线程,采用时间片轮转方式进行调度(也就是通常说的分时调度器),仅在当前系统中无更高优先级 就绪线程存在的情况下才有效,每个线程的时间片大小都可以 在初始化或创建这个线程时指定。
2.2.7优先级链表:
系统中的总线程数不受限制,只和系统所能提供的内存资源相关。为了保证系统的实时性,系统尽最大可能地保证高 优先级的线程得以运行。线程调度的原则是一旦任务状态发生了改变,并且当前运行的线程 优先级小于优先级队列组中线程最高优先级时,立刻进行线程切换(除非当前系统处于中断 处理程序中或禁止线程切换的状态)。
2.3 线程控制块
线程控制块:操作系统用于控制线程的一个数据结构,存放线程信息,如优先级,线程名称等,也包含线程与线程之间连接用的链表结构,线程等待事件集合等
- 线程控制块由结构体struct rt_thread表示
- 另外一种C 表达方式rt_thread_t,表示的是线程的句柄,在C语言中的实现是指向线程控制块的指针
- init_priority-线程创建时指定的线程优先级,线程运行过程当中是不会被改变的 (除非用户执行线程控制函数进行手动调整线程优先级)
- cleanup成员-线程退出时,被idle线程回调一次以执行用户设置的清理现场等工 作
- user_data-由用户挂接一些数据信息到线程控制块中,以提供类似线 程私有数据的实现,例如lwIP线程中用于放置定时器链表的表头。
2.4 线程状态
线程运行的过程中,同一时间只允许一个线程在处理器中运行
在RT-Thread实时操作系统中, 线程包含五种状态,操作系统会自动根据它运行的情况而动态调整它的状态。
几种状态间的转换关系如 线程转换图 所示
- 线程通过调用函数rt_thread_create/init进入到初始状态(RT_THREAD_INIT);
- 再通过调用函数rt_thread_startup进入到就绪状态(RT_THREAD_READY);
- 当处于运行状态的线程调用rt_thread_delay,rt_sem_take,rt_mb_recv等函数或由于获取不到资源时,将进入到挂起状态(RT_THREAD_SUSPEND);
- 处于挂起状态的线程,如果等待超时依然 未能获得资源或由于其他线程释放了资源那么它将返回到就绪状态。
- 挂起状态的线程,如果调用rt_thread_delete/detach将更改为关闭状态(RT_THREAD_CLOSE);
- 而运行状态的线程,如果运行结束会在线程最后部分执行rt_thread_exit函数而更改为关闭状态(RT_THREAD_CLOSE)。
2.5 空闲线程
2.5.1特点:
- 最低的优先级
- 无其他线程可运行时,调度器将调度到空闲线程
- 通常是死循环,永远不被挂起。
2.5.2空闲线程拥有的函数:
1.RT-Thread实时操作系统为空闲线程提供了钩子函数,系统在空闲的时候执行一些特定的任务,例如系统运行指示灯闪烁,电源管理等
2.线程清理 (rt_thread->cleanup回调函数)函数、真正的线程删除动作放到了空闲线程中(在删除线 程时,仅改变线程的状态为关闭状态不再参与系统调度)。
2.6 调度器相关接口
2.6.1 调度器初始化
作用:系统启动时,执行调度器的初始化;初始化系统调度器用到的全局变量
函数接口:void rt_system_scheduler_init(void);
2.6.2 启动调度器
作用:切换首个线程调用接口
函数接口:void rt_system_scheduler_start(void);
注:查找系统中优先级最高的就绪态线程并执行,调用这个函数前,必须先做idle线程的初始化,即保证系统至少能够找到一个就绪状态的线程执行
2.6.3 执行调度
作用:执行一次线程调度的函数接口
函数接口:void rt_schedule(void);
调用后,系统计算一次系统中就绪态的线程,如果存在比当前线程更高优先级的线程时,系统将切换到高优先级的线程去
注:在中断服务例程中也可以调用这个函数,如果满足任务切换的条件,它会记录下 中断前的线程及需要切换到的更高优先级线程,在中断服务例程处理完毕后执行真正的线程上下文切换(即中断中的线程上下文切换),最终切换到目标线程去。
2.6.4 设置调度器钩子
作用:用户调用函数查看线程切换过程记录
函数接口:void rt_scheduler_sethook(void (hook)(struct rt_thread from, struct rt_thread* to));
此函数把用户提供的hook函数设置到系统调度器钩子中,当系统进行上下文切换时,这个hook函数将会被系统调用。
2.7 线程相关接口
2.7.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_NULL。
参数 | 名称 | 描述 |
---|---|---|
name | 线程的名称 | 最大长度由rtconfig.h中定义的RT_NAME_MAX宏指定,多余部分会被自动截掉 |
entry | 线程入口函数 | |
parameter | 线程入口函数参数 | |
stack_size | 线程栈大小 | 单位字节;在大多数系统中需要做栈空间地址对齐 |
priority | 线程的优先级 | 优先级范围根据系统配置情况,数值越小优先级越高,0代表最高优先级。 |
tick | 线程的时间片大小 | 时间片(tick)的单位是操作系统的时钟节拍。 |
当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度。这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行。
2.7.2 线程删除
作用:把线程完全删除掉,线程对象删除,占用的堆栈空间释放
函数接口:rt_err_t rt_thread_delete(rt_thread_t thread);
函数返回:返回RT_EOK
注:
- 本质上把相应线程状态改为RT_THREAD_CLOSE状 态,然后放入到rt_thread_defunct队列中;而真正的删除动作(释放线程控制块和释放线程栈)需要到下一次执行idle线程时,由idle线程完成最后的线程删除动作。用rt_thread_init初始化的静态线程则不能使用此接口删除。
- 在线程运行完成,自动结束的情况下,系统会自动删除线程,不需要再调用rt_thread_delete()函数接口。这个接口不应由线程本身来调用以删除线程自身,一般只能由其他线程调用或在定时器超时函数中调用。
- 这个函数仅在使能了系统动态堆时才有效(即RT_USING_HEAP宏定义已经定义了)。
2.7.3 线程初始化
作用:初始化静态线程对象
函数接口:
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);
参数 | 名称 | 描述 |
---|---|---|
thread | 线程句柄 | 由用户提供出来,指向对应的线程控制块内存地址。 |
name | 线程的名称 | 最大长度由rtconfig.h中定义的RT_NAME_MAX宏指定,多余部分会被自动截掉。 |
entry | 线程入口函数 | |
parameter | 线程入口函数参数 | |
stack_size | 线程栈大小 | 单位字节。在大多数系统中需要做栈空间地址对齐 |
stack_start | 线程栈起始地址 | |
priority | 线程的优先级 | 优先级范围根据系统配置情况,数值越小优先级越高,0代表最高优先级。 |
tick | 线程的时间片大小 | 单位是操作系统的时钟节拍。 |
返回值:返回RT_EOK;
注:线程句柄(或者说线程控制块指针),线程栈由用户提供(用户提供的栈 首地址需做系统对齐)
静态线程:线程控制块、线程运行栈一般都设置为全局变量,在编译时就被确定、被分配处理,内核不负责动态分配内存空间。
2.7.4 线程脱离
作用:使线程对象在线程队列和内核对象管理器中被删除
函数接口:rt_err_t rt_thread_detach (rt_thread_t thread);
返回值:返回RT_EOK;
rt_thread_delete() | rt_thread_detach() | |
---|---|---|
操作对象 | 是rt_thread_create()创建的句柄 | 使用 rt_thread_init()函数初始化的线程控制块 |
注:线程本身不应调用这个接口脱离线程本身。
2.7.5 线程启动
作用:启动处于初始态的线程对象,使其进入就绪线程的调度队列中其相应的优先级队列,
若新启动的线程优先级比当前线程优先级高,将立刻切换到这个线程
函数接口:rt_err_t rt_thread_startup(rt_thread_t thread);
返回值:返回RT_EOK;
2.7.6 当前线程
作用:获得当前执行的线程句柄
函数接口:rt_thread_t rt_thread_self(void);
返回值:返回当前运行的线程句柄
如果调度器还未启动,将返回RT_NULL
注:不要在中断服务程序中调用此函数,因为它并不能准确获得当前的执行线程。
2.7.7 线程让出处理器
作用:当前线程时间片用完,或者该线程自动要求让出处理器资源时
线程自身调用函数,让出处理器,自身挂到这个优先级队列链表的尾部,激活调度器进行线程上下文切换
函数接口:rt_err_t rt_thread_yield(void);
注:如果当前优先级 只有这一个线程,则这个线程继续执行,不进行上下文切换动作
rt_thread_yield() | rt_schedule() | |
---|---|---|
有相同优先级的其他就绪态线程存在时的系统行为 | 当前线程被换出相同优先级的下一个就绪线程将被执行 | 在系统中选取就绪的优先级最高的线程执行(如果没有优先级更高的,将继续当前线程) |
2.7.8 线程睡眠
作用:让运行线程延迟,指定时间后,线程会被唤醒并再次进入就绪状态。
函数接口:rt_err_t rt_thread_sleep(rt_tick_t tick);
rt_err_t rt_thread_delay(rt_tick_t tick);
函数返回:返回RT_EOK
2.7.9 线程挂起
作用:线程挂起
函数接口:rt_err_t rt_thread_suspend (rt_thread_t thread);
使用场景:
rt_thread_delay | rt_sem_take rt_mb_recv | |
---|---|---|
操作 | 调用线程将主动挂起 | 资源不可使用也将导致调用线程挂起 |
函数返回:如果这个线程的状态并不是就绪状态,将返回-RT_ERROR,否则返回RT_EOK
-
线程使用函数挂起线程本身
需要在调用rt_thread_suspend()函数后立刻调用rt_schedule()函 数进行手动的线程上下文切换。
-
挂起状态的线程返回到就绪状态的情况
- 等待的资源超时(超过其设定的等待时间,那么该线程将不再等待这些资源
- 其他线程释放掉该线程所等待的资源时,该线程也会返回到就绪状态
2.7.10 线程恢复
作用:让挂起的线程重新进入就绪状态
函数接口:rt_err_t rt_thread_resume (rt_thread_t thread);
函数返回 :
恢复的线程状态是RT_THREAD_SUSPEND状态,返回 RT_EOK
不是,将返回-RT_ERROR
2.7.11 线程控制
作用:动态更改线程的优先级
函数接口:
rt_err_t rt_thread_control(rt_thread_t thread, rt_uint8_t cmd, void* arg);
参数 | 描述 |
---|---|
thread | 线程句柄 |
cmd | 指示控制命令 |
arg | 控制参数 |
指示控制命令cmd当前支持的命令包括
- RT_THREAD_CTRL_CHANGE_PRIORITY - 动态更改线程的优先级; •
- RT_THREAD_CTRL_STARTUP - 开始运行一个线程,等同于rt_thread_startup() 函数调用;
- RT_THREAD_CTRL_CLOSE - 关闭一个线程,等同于rt_thread_delete()函数调 用。
函数返回:控制执行正确返回RT_EOK,否则返回RT_ERROR。
2.7.12 初始化空闲线程
系统运行过程中必须存在一个最终可运行的线程
作用:初始化空闲线程
函数接口:void rt_thread_idle_init(void);
2.7.13 设置空闲线程钩子
作用:设置空闲线程运行时执行的钩子函数。
函数接口:void rt_thread_idle_sethook(void (*hook)(void));
参数 hook 设置的钩子函数
注:空闲线程运行时会自动执行设置的钩子函数,由于空闲线程具有系统的最低优先级,所以只有在空闲的时候才会执行此钩子函数。空闲线程是一个线程状态永远为就绪 态的线程,因此设置的钩子函数必须保证空闲线程在任何时刻都不会处于挂起状态,例如 rt_thread_delay() , rt_sem_take() 等可能会导致线程挂起的函数都不能使用。
2.8 线程设计
2.8.1 程序的运行上下文
采用RT-Thread的实时系统,几种类型的程序运行状态构成了程序运行的上下文状态
RT-Thread中程序运行的上下文包括:
• 中断服务例程;
中断服务例程是一种需要特别注意的上下文环境,运行在非线程的执行环境下(特权模式),在这个上下文环境中不能使用挂起当前线程的 操作,因为当前线程并不存在,执行相关的操作会有:Function[abc_func] shall not used in ISR的打印信息(abc_func就是你不应该在中断服务例程中调用的函数)。
另外,中断服务程序最好保持精简短小,因为中断服务是一种高于任何线程的存在。
• 普通线程;
做为一个实时系统,一个优先级明确的实时系统,不能有死循环操作
• 空闲线程;
没有其他工作进行时自动进入的系统线程
开发者可以通 过idle线程钩子方式,在idle线程上钩入自己的功能函数。
对于空闲线程钩子上挂接的 程序,它应该:
• 不会挂起idle线程;
• 不应该陷入死循环,需要留出部分时间用于系统处理僵尸线程的系统资源回收。
2.8.2 线程设计要点
实时系统多数是一种被动的系统,被动的响应外部事件, 当外部事件触发后执行设定的工作内容。所以在对系统进行线程设计时,需要考虑到:
- 上下文环境
- 线程的状态跃迁
指的是线程运行中状态的变化,从就绪态过渡到挂起态。线程能够主动让出处理器资源,进入到阻塞状态。这需要设计者在设计线程的时候 就明确的知道什么情况下需要让线程从就绪态跃迁到阻塞态。 - 线程运行时间长度合理分配