尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

RTOS线程切换和线程栈

RTOS线程切换和线程栈 以CortexM RTThread为例线程栈初始化线程创建函数一个线程最重要的就是线程栈函数调用、局部变量分配都需要用到线程栈。在线程创建的时候线程栈的初始化尤为重要。rt_err_trt_thread_init(structrt_thread*thread,constchar*name,void(*entry)(void*parameter),void*parameter,void*stack_start,rt_uint32_tstack_size,rt_uint8_tpriority,rt_uint32_ttick)rt_thread_init是线程创建函数其中entry、parameter、stack_start、stack_size都跟线程栈初始化相关。entry线程入口函数可以理解成跟main函数类似的函数parameter线程的参数stack_start线程栈的起始地址stack_size线程栈的大小byte线程栈初始化函数在线程栈初始化函数中可以看到是如何初始化线程栈。structexception_stack_frame{rt_uint32_tr0;rt_uint32_tr1;rt_uint32_tr2;rt_uint32_tr3;rt_uint32_tr12;rt_uint32_tlr;rt_uint32_tpc;rt_uint32_tpsr;};structstack_frame{/* r4 ~ r11 register */rt_uint32_tr4;rt_uint32_tr5;rt_uint32_tr6;rt_uint32_tr7;rt_uint32_tr8;rt_uint32_tr9;rt_uint32_tr10;rt_uint32_tr11;structexception_stack_frameexception_stack_frame;};rt_uint8_t*rt_hw_stack_init(void*tentry,void*parameter,rt_uint8_t*stack_addr,void*texit){structstack_frame*stack_frame;rt_uint8_t*stk;unsignedlongi;/* step 1 */stkstack_addrsizeof(rt_uint32_t);stk(rt_uint8_t*)RT_ALIGN_DOWN((rt_uint32_t)stk,8);stk-sizeof(structstack_frame);stack_frame(structstack_frame*)stk;/* step 2 *//* init all register */for(i0;isizeof(structstack_frame)/sizeof(rt_uint32_t);i){((rt_uint32_t*)stack_frame)[i]0xdeadbeef;}/* step 3 */stack_frame-exception_stack_frame.r0(unsignedlong)parameter;/* r0 : argument */stack_frame-exception_stack_frame.r10;/* r1 */stack_frame-exception_stack_frame.r20;/* r2 */stack_frame-exception_stack_frame.r30;/* r3 */stack_frame-exception_stack_frame.r120;/* r12 */stack_frame-exception_stack_frame.lr(unsignedlong)texit;/* lr */stack_frame-exception_stack_frame.pc(unsignedlong)tentry;/* entry point, pc */stack_frame-exception_stack_frame.psr0x01000000L;/* PSR *//* step 4 *//* return tasks current stack address */returnstk;}在初始化线程栈前把线程的入口地址、线程参数、线程退出函数和线程的栈顶地址传进去thread-sp(void*)rt_hw_stack_init(thread-entry,thread-parameter,(rt_uint8_t*)((char*)thread-stack_addrthread-stack_size-sizeof(rt_ubase_t)),(void*)_thread_exit);在rt_hw_stack_init中主要有以下 4 步给线程栈预留 64 字节的初始帧。给 64 字节的初始帧初始化为0xdeadbeef。将线程参数、线程入口函数、线程退出函数赋值给寄存器。返回初始化完的线程栈地址。初始化完成的线程栈高地址栈顶 ┌────────────────────┐ ◄── stack_addr stack_size栈顶 │ │ 对齐预留区 ├────────────────────┤ ◄── stack_frame 顶部8 字节对齐 │ psr 0x01000000L │ ┐ │ pc tentry │ │ │ lr texit │ │ exception_stack_frame │ r12 0 │ │ 硬件异常返回帧32B │ r3 0 │ │ │ r2 0 │ │ │ r1 0 │ │ │ r0 parameter │ ┘ │ r11 0xdeadbeef │ ┐ │ r10 0xdeadbeef │ │ │ r9 0xdeadbeef │ │ │ r8 0xdeadbeef │ │ r4~r11 保存区32B │ r7 0xdeadbeef │ │ 软件手动恢复 │ r6 0xdeadbeef │ │ │ r5 0xdeadbeef │ │ │ r4 0xdeadbeef │ ┘ ├────────────────────┤ ◄── thread-sp返回值 stk │ │ │ ... │ │ │ └────────────────────┘ ◄── stack_addr栈底 低地址栈底sp 以下为剩余可用栈空间向下增长几点说明栈向下增长满递减栈sp指向r4stack_frame起始地址共 64 字节。低 32 字节是r4~r11Cortex-M3 硬件异常不会自动压这些寄存器需软件手动恢复填充0xdeadbeef便于调试时发现越界/未初始化。高 32 字节是exception_stack_frame成员顺序r0~r3, r12, lr, pc, psr正好匹配 Cortex-M3 硬件异常压栈格式。首次调度软件先LDMIA r0!, {r4-r11}恢复r4~r11sp上移到r0处再异常返回由硬件恢复r0~r3, r12, lr, pc, psr最终跳转到pc tentry且r0 parameter作为入口参数。psr 0x01000000L的 bit24 是 Thumb 状态位Cortex-M3 必须置 1。线程调度时线程栈的变化rt_hw_context_switch((rt_ubase_t)from_thread-sp,(rt_ubase_t)to_thread-sp);PendSV_Handler:/* disable interrupt to protect context switch */MRS R2,PRIMASK CPSID I/* get rt_thread_switch_interrupt_flag */LDR R0,rt_thread_switch_interrupt_flag LDR R1,[R0]CBZ R1,pendsv_exit/* pendsv already handled *//* clear rt_thread_switch_interrupt_flag to 0 */MOV R1,#0STR R1,[R0]/* 线程栈保存 */LDR R0,rt_interrupt_from_thread LDR R1,[R0]CBZ R1,switch_to_thread/* skip register save at the first time */MRS R1,PSP/* get from thread stack pointer */STMFD R1!,{R4-R11}/* push R4 - R11 register */LDR R0,[R0]STR R1,[R0]/* update from thread stack pointer *//* 线程栈恢复 */switch_to_thread:LDR R1,rt_interrupt_to_thread LDR R1,[R1]LDR R1,[R1]/* load thread stack pointer */LDMFD R1!,{R4-R11}/* pop R4 - R11 register */MSR PSP,R1/* update stack pointer */pendsv_exit:/* restore interrupt */MSR PRIMASK,R2 ORR LR,LR,#0x04BX LRPendSV_Handler是进行线程切换的函数主要做了保存现场和恢复现场两步线程栈保存LDR R0,rt_interrupt_from_thread// R0 变量 rt_interrupt_from_thread 的地址LDR R1,[R0]// R1 被切换线程 sp 字段的地址from_thread-spCBZ R1,switch_to_thread// 首次切换 from 为 NULL跳过保存MRS R1,PSP// R1 当前线程栈指针 PSPSTMFD R1!,{R4-R11}// 压栈 R4-R11R1 递减LDR R0,[R0]// R0 重新取出 from_thread-spR1 已被 PSP 覆盖STR R1,[R0]// from_thread-sp R1保存压栈后的新栈顶线程栈恢复LDR R1,rt_interrupt_to_thread// R1 变量 rt_interrupt_to_thread 的地址LDR R1,[R1]// R1 目标线程 sp 字段的地址to_thread-spLDR R1,[R1]// R1 目标线程栈指针to_thread-spLDMFD R1!,{R4-R11}// 弹出 R4-R11R1 递增MSR PSP,R1// PSP R1更新线程栈指针几点说明R0、R1、R2、R3、R12、LR、PC 和 PSR 在发生异常或中断时由硬件自动保存所以软件只需要保存/恢复 R4-R11。ORR LR, LR, #0x04把异常返回值EXC_RETURN的 bit2 置 1使异常返回时使用 PSP线程栈BX LR触发异常返回后硬件自动从 PSP 弹出 R0-R3、R12、LR、PC、PSR完成现场恢复。调度与恢复时的线程栈图保存现场from 线程被切出高地址栈顶 ┌────────┐ │ ... │ ◄── 局部变量等已用栈空间 ├────────┤ ◄── 被抢占前 PSP进入 PendSV 前 │ xpsr │ ┐ │ pc │ │ │ lr │ │ │ r12 │ │ ① 硬件自动压栈 │ r3 │ │ 异常返回帧 32B │ r2 │ │ │ r1 │ │ │ r0 │ ┘ │ r11 │ ┐ │ r10 │ │ │ r9 │ │ │ r8 │ │ ② 软件压栈 │ r7 │ │ R4-R11 32B │ r6 │ │ STMFD R1!,{R4-R11} │ r5 │ │ │ r4 │ ┘ ├────────┤ ◄── PSP保存后 from_thread-sp │ │ │ │ └────────┘ 低地址栈底剩余可用栈空间向下增长恢复现场to 线程被切入高地址栈顶 ┌────────┐ │ ... │ ◄── 局部变量等已用栈空间 ├────────┤ ◄── ③ 硬件弹异常帧后 PSP 回到这里 │ xpsr │ │ pc │ │ lr │ │ r12 │ │ r3 │ │ r2 │ │ r1 │ │ r0 │ ◄── ② LDMFD 弹完 R4-R11 后 PSP 到这里 │ r11 │ │ r10 │ │ r9 │ │ r8 │ │ r7 │ │ r6 │ │ r5 │ │ r4 │ ◄── ① 恢复前 PSP to_thread-sp ├────────┤ │ │ └────────┘ 低地址栈底保存现场分两步进入 PendSV 时硬件自动压栈异常返回帧R0-R3、R12、LR、PC、xPSR随后软件STMFD R1!,{R4-R11}压 R4-R11保存后from_thread-sp指向 R4。恢复现场是保存的逆过程分三步①LDMFD R1!,{R4-R11}弹出 R4-R11PSP 上移到 R0②MSR PSP, R1③BX LR由硬件弹出异常返回帧PSP 回到被抢占前位置线程继续运行。关键点保存现场后的栈帧结构下方 R4-R11、上方异常返回帧与线程初始化时的stack_frame完全一致所以首次调度能复用同一套恢复代码。
返回列表