RTOS线程切换和线程栈
2026/8/16 7:27:11 网站建设 项目流程

以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是线程创建函数,其中entryparameterstack_startstack_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 */stk=stack_addr+sizeof(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(i=0;i<sizeof(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.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=(unsignedlong)texit;/* lr */stack_frame->exception_stack_frame.pc=(unsignedlong)tentry;/* entry point, pc */stack_frame->exception_stack_frame.psr=0x01000000L;/* PSR *//* step 4 *//* return task's current stack address */returnstk;}

在初始化线程栈前,把线程的入口地址、线程参数、线程退出函数和线程的栈顶地址传进去:

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*)_thread_exit);

rt_hw_stack_init中主要有以下 4 步:

  1. 给线程栈预留 64 字节的初始帧。
  2. 给 64 字节的初始帧初始化为0xdeadbeef
  3. 将线程参数、线程入口函数、线程退出函数赋值给寄存器。
  4. 返回初始化完的线程栈地址。

初始化完成的线程栈

高地址(栈顶) ┌────────────────────┐ ◄── 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~r11,Cortex-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 LR

PendSV_Handler是进行线程切换的函数,主要做了保存现场和恢复现场两步:

线程栈保存

LDR R0,=rt_interrupt_from_thread// R0 = 变量 rt_interrupt_from_thread 的地址LDR R1,[R0]// R1 = 被切换线程 sp 字段的地址(&from_thread->sp)CBZ R1,switch_to_thread// 首次切换 from 为 NULL,跳过保存MRS R1,PSP// R1 = 当前线程栈指针 PSPSTMFD R1!,{R4-R11}// 压栈 R4-R11,R1 递减LDR R0,[R0]// R0 = 重新取出 &from_thread->sp(R1 已被 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->sp)LDR R1,[R1]// R1 = 目标线程栈指针(to_thread->sp)LDMFD R1!,{R4-R11}// 弹出 R4-R11,R1 递增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-R11,PSP 上移到 R0;②MSR PSP, R1;③BX LR由硬件弹出异常返回帧,PSP 回到被抢占前位置,线程继续运行。
  • 关键点:保存现场后的栈帧结构(下方 R4-R11、上方异常返回帧)与线程初始化时的stack_frame完全一致,所以首次调度能复用同一套恢复代码。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询