在rt-thread中线程主要包括以下一些内容,线程控制块、线程栈、函数入口。
RTOS基本都包括两种线程方式:动态创建rt_thread_create()
和静态创建rt_thread_init()
。
因为有些系统设计时对安全性要求比较高,内存需要提前分配好,只能使用静态创建的方式。
线程结构体的一些主要成员
struct rt_thread
{
/* 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 */
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 */
};
优先级里有当current_priority
和init_priority
后续文章讲到互斥量时有优先级继承时会详细说明。
init_tick
和remaining_tick
在第3.2节时间片概念中会详细说明。
无论选择动态还是静态创建最后都会调用这个函数static rt_err_t _thread_init()
。简要分析一下这个函数的作用
thread->entry = (void *)entry;//函数指针
thread->parameter = parameter;//创建任务时可以带个参数
/* stack init 任务栈初始化*/
thread->sp = (void *)rt_hw_stack_init(thread->entry, thread->parameter,
(rt_uint8_t *)((char *)thread->stack_addr + thread->stack_size - sizeof(rt_ubase_t)),
(void *)rt_thread_exit);
thread->stack_addr = stack_start;//初始化栈顶指针
thread->stack_size = stack_size;//初始化栈的大小
/* priority init 优先级初始化*/
RT_ASSERT(priority < RT_THREAD_PRIORITY_MAX);
thread->init_priority = priority;//初始化初始化优先级
thread->current_priority = priority;//初始化当前优先级
/* tick init */
thread->init_tick = tick;//初始化时间片
thread->remaining_tick = tick;//初始化剩余时间片
线程栈的结构体如下代码,其实就是我们熟悉ARM中16个寄存器。但是他们定义的顺序不是r0-15,而是r4~ r11,r0~ r3,r12,lr,pc,psr,其原因在2.2.1节会讲明。
struct exception_stack_frame
{
rt_uint32_t r0;
rt_uint32_t r1;
rt_uint32_t r2;
rt_uint32_t r3;
rt_uint32_t r12;
rt_uint32_t lr;
rt_uint32_t pc;
rt_uint32_t psr;
};
struct stack_frame
{
/* r4 ~ r11 register */
rt_uint32_t r4;
rt_uint32_t r5;
rt_uint32_t r6;
rt_uint32_t r7;
rt_uint32_t r8;
rt_uint32_t r9;
rt_uint32_t r10;
rt_uint32_t r11;
struct exception_stack_frame exception_stack_frame;
};
线程栈初始化函数主要实现:
struct stack_frame *stack_frame;
rt_uint8_t *stk;
unsigned long i;
stk = stack_addr + sizeof(rt_uint32_t);
stk = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);
stk -= sizeof(struct stack_frame);
stack_frame = (struct stack_frame *)stk;
/* init all register */
for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++)
{
((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;
}
stack_frame->exception_stack_frame.r0 = (unsigned long)parameter; /* r0 : argument */
stack_frame->exception_stack_frame.r1 = 0; /* r1 */
stack_frame->exception_stack_frame.r2 = 0; /* r2 */
stack_frame->exception_stack_frame.r3 = 0; /* r3 */
stack_frame->exception_stack_frame.r12 = 0; /* r12 */
stack_frame->exception_stack_frame.lr = (unsigned long)texit; /* lr */
stack_frame->exception_stack_frame.pc = (unsigned long)tentry; /* entry point, pc */
stack_frame->exception_stack_frame.psr = 0x01000000L; /* PSR */
用一张图片来展示这个过程
rt-thread将线程分成5个状态:初始状态、就绪状态、运行状态、挂起状态和关闭状态。如图所示:
注:rt-thread现在只允许线程A挂起线程A,不允许线程A挂起线程B,所以rt_thread_suspend()
理论上并不能把一个就绪态的任务转换到挂起态,而是把运行态任务切换至挂起态。
初始状态
任务使用rt_thread_init()
/ rt_thread_create()
创建后则处于初始状态。
任务会被添加到对应优先级的就绪态链表中,如下图所示:
就绪状态
就绪态顾名思义就是程序准备运行的状态,以下是几种会让程序处于就绪态的方式:
rt_thread_delete()
/rt_thread_detach()
删除的线程线程A运行一段时间后切换线程B,需要先保存A线程的现场,再去切换线程B。若再切换成线程A,则再需要保存线程B的现场,再去切换线程A。
保护现场保护什么呢?以下面一段代码解释一下:
void task_A()
{
int a = 1;
int b ;
b = a + 2;
}
其汇编代码如下图所示:
大致意思是这样的(也不用全懂哈哈)
0x08000144 2001 MOVS r0,#0x01 :将1存入r0
0x08000146 9001 STR r0,[sp,#0x04] :将r0存入sp+4的位置也就是局部变量a
7: b = a + 2;
0x08000148 9801 LDR r0,[sp,#0x04] :将sp+4的值存入r0
0x0800014A 1C80 ADDS r0,r0,#2 :r0=r0+2
0x0800014C 9000 STR r0,[sp,#0x00] :将r0存入sp的位置
8: return b;
0x0800014E 9800 LDR r0,[sp,#0x00] :sp+0的值存入r0
ARM架构中一共有16个寄存器(r0-r7,r8-r12,sp,lr,pc)中间运算的时候会将值存入r0,若此时任务切换则r0的值会在别的任务中被覆盖,恢复后是不可预料的。所以再任务切换前需要需要保护这16个寄存器。局部变量a不需要保存,因为他保存再线程A的栈中间,出栈即可。
保存现场的处理函数在pendSV异常中,主要是这两句汇编
MRS r1, psp ; get from thread stack pointer
STMFD r1!, {r4 - r11} ; push r4 - r11 register
STMFD
Store Multi Full Dec
找出线程A的线程栈,将r4 - r11多个寄存器满减压栈到线程A的栈中。前文说了有16个寄存器,软件只存了这8个剩下的几个寄存器在进入中断是已经存储到线程A的栈中,如下图:
还有一段描述
细心的读者一定在猜测:为啥袒护R0‐R3以及R12呢,R4‐R11就是下等公民?原来,在ARM
上,有一套的C函数调用标准约定(《C/C++ Procedure Call Standard for the ARM Architecture》,
AAPCS, Ref5)。个中原因就在它上面:它使得中断服务例程能用C语言编写,编译器优先使
用被入栈的寄存器来保存中间结果(当然,如果程序过大也可能要用到R4‐R11,此时编译器
负责生成代码来push它们。但是,ISR应该短小精悍,不要让系统如此操心——译者注
这也解释了rt-thread线程栈结构体r0-r15不是连续的原因。
有了保护现场的经验,切换任务就简单了。
LDMFD r1!, {r4 - r11} ; pop r4 - r11 register
MSR psp, r1 ; update stack pointer
pendsv_exit
; restore interrupt
MSR PRIMASK, r2
ORR lr, lr, #0x04
从线程B的栈中恢复r4-r11寄存器,跟新SP指针指向r11。推出异常硬件自动恢复线程B的R0‐R3以及R12。
任务切换比较深奥,多读两遍再结合韦东山老师的视频就能理解了。若还不能理解可以先学使用,再过几个月回过来看看。
这个问题答案在《Cortex-M3 权威指南》中由说明。
上图是两个任务轮转调度的示意图。但若在产生 SysTick 异常时正在响应一个中断,则
SysTick 异常会抢占其 ISR。在这种情况下,OS 不得执行上下文切换,否则将使中断请求被延
迟,而且在真实系统中延迟时间还往往不可预知——任何有一丁点实时要求的系统都决不能
容忍这种事。因此,在 CM3 中也是严禁没商量——如果 OS 在某中断活跃时尝试切入线程模
式,将触犯用法 fault 异常
PendSV 异常会自动延迟上下文切换的请求,
直到其它的 ISR 都完成了处理后才放行。为实现这个机制,需要把 PendSV 编程为最低优先级的异常。如果 OS 检测到某 IRQ 正在活动并且被 SysTick 抢占,它将悬起一个 PendSV 异常,以便缓期执行上下文切换。
假设我们创建3个线程ABC,优先级分别时12,12,15(如下图所示)。在rt-thread中优先级数字越低优先级越高,这里与freeRTOS相反。rt-thread链表插入使用的是rt_list_insert_before()
头插法,双向循环链表头的前面也等价于链表的最后面(可能有点绕),所以初始化的顺序是先初始化线程A再初始化线程B。
rt-thread优先级规则:任务调度器会优先找到优先级最高的链表的第一项执行
freeRTOS优先级规则:任务调度器会优先找到优先级最高的链表的最后一项执行
任务调度器会优先找到优先级最高的链表,也就是优先级为12的链表,然后取链表头后面的第一个线程(A),当A运行时则从就绪态链表中移除。当线程A被挂起时,任务调度器会继续寻找优先级最高的第一个线程,此时是B。
当任务B被挂起时,按照上述规则则会执行任务C。
当挂起态的任务结束阻塞调用rt_thread_resume()
会重新插入到就绪态链表中。
当A,B两个线程均无阻塞且处于当前最高就绪态相同优先级,A运行时间片为10个tick,B为5个tick,当前执行B线程。按照上述优先级规则执行那么永远只会执行B线程,A永远不会执行。为了避免此类情况发生,设计了时间片的概念。当B执行了5个tick后,则挂到就绪态当前优先级的队尾,此时任务切换后执行的便是A线程。A执行完10个tick则会切换会B。AB两个线程都得到了执行的时间
其背后原理是,当任务B的remaining_tick
用完后会被移动到就绪态链表的最后面。当任务调度器调度按照3.1节内叙述的规则找到任务A的控制块运行。
b站:韦东山RT-Thread系列教程: RT-Thread的内部机制
《RT-Thread 完全开发手册之快速入门》
《Cortex-M3 权威指南》