4. 任务上下文切换:任务控制块(TCB)结构、栈帧布局、PendSV异常与上下文切换流程

任务上下文切换是RTOS内核最核心的机制,它决定了系统能否在多个任务之间快速、正确地切换执行流。本章深入剖析任务控制块(TCB)的数据结构、任务栈帧的硬件/软件布局、PendSV异常的工作原理,以及完整的上下文切换流程。

4.1 任务控制块(TCB)结构

TCB是内核管理任务的核心数据结构,每个任务对应一个TCB实例。TCB记录了任务的所有状态信息,包括栈指针、优先级、状态标志、任务名称等。

关键设计原则: TCB必须紧凑且访问高效,因为调度器在每次上下文切换时都会频繁读写TCB成员。通常将最常访问的字段(如栈指针、优先级)放在结构体头部,以利用CPU的偏移量快速寻址。

4.1.1 典型TCB结构定义

typedef struct tcb {
    uint32_t    *sp;            /* 栈指针(当前栈顶,核心字段) */
    uint32_t    priority;       /* 任务优先级(数值越小优先级越高) */
    uint32_t    state;          /* 任务状态:就绪/运行/阻塞/挂起/终止 */
    uint32_t    delay_ticks;    /* 延时剩余tick数(用于阻塞延时) */
    uint32_t    slice_ticks;    /* 时间片剩余tick数(用于轮转调度) */
    const char  *name;          /* 任务名称(调试用) */
    void        *stack_base;    /* 栈底地址(用于栈溢出检测) */
    uint32_t    stack_size;     /* 栈大小(字节数) */
    struct tcb  *next;          /* 链表指针(就绪队列/阻塞队列) */
    #if (configUSE_MUTEXES > 0)
        void    *mutex_holder;  /* 当前持有的互斥量(用于优先级继承) */
    #endif
} TCB_t;

4.1.2 TCB字段功能对照表

字段 类型 作用 访问频率
sp uint32_t* 保存任务当前栈顶指针,切换时直接加载到PSP 极高(每次切换)
priority uint32_t 任务优先级,决定调度顺序 高(每次调度决策)
state uint32_t 任务当前状态,用于队列管理和调度判断 高
delay_ticks uint32_t 任务阻塞等待的剩余时间 中(tick中断更新)
slice_ticks uint32_t 时间片轮转的剩余配额 中(tick中断更新)
name const char* 任务名称,用于调试输出和日志 低(调试时)
stack_base void* 栈底地址,配合栈溢出检测钩子使用 低(异常检测时)

4.2 栈帧布局

ARM Cortex-M系列处理器在响应异常时,硬件会自动压栈8个寄存器(xPSR、PC、LR、R12、R3-R0),形成硬件栈帧。RTOS内核在此基础上,由软件压栈剩余的通用寄存器(R4-R11、LR_special),形成完整的软件栈帧。

调试要点: 在单步调试上下文切换时,观察栈指针(PSP)的变化和栈内存中的寄存器排列,是验证切换正确性的最直接手段。硬件栈帧的起始地址总是8字节对齐的。

4.2.1 完整栈帧布局(从高地址到低地址)

高地址  ← 栈底方向
+----------------------------+
| 软件压栈部分               |
| R4                          |  ← 软件压栈顺序:R4-R11, LR
| R5                          |
| R6                          |
| R7                          |
| R8                          |
| R9                          |
| R10                         |
| R11                         |
| LR (EXC_RETURN)             |  ← 异常返回专用链接寄存器
+----------------------------+
| 硬件压栈部分               |
| xPSR                        |  ← 程序状态寄存器
| PC                          |  ← 任务恢复后的执行地址
| LR                          |  ← 任务正常返回地址
| R12                         |
| R3                          |
| R2                          |
| R1                          |
| R0                          |  ← 栈顶(SP指向此处)
+----------------------------+
低地址  → 栈顶方向

4.2.2 硬件栈帧与软件栈帧对比

特性 硬件栈帧 软件栈帧
压栈时机 CPU响应异常(如PendSV、SysTick)时自动完成 异常处理程序入口处由软件指令(PUSH)完成
包含寄存器 xPSR, PC, LR, R12, R3-R0(共8个) R4-R11, LR(EXC_RETURN)(共9个)
栈指针选择 使用PSP(进程栈指针) 使用PSP(与硬件栈帧连续)
恢复顺序 异常返回时硬件自动出栈 异常返回前软件出栈(POP指令)
调试观察点 观察SP变化和内存中的寄存器值排列 观察PUSH/POP指令执行后的栈内容

4.3 PendSV异常与上下文切换流程

PendSV(Pendable Service Call)是ARM Cortex-M内核专门为RTOS设计的可挂起系统异常。它优先级最低(通常设置为0xFF),且可以被所有其他异常和中断抢占,从而避免在中断处理过程中发生上下文切换导致优先级反转问题。

4.3.1 PendSV触发机制

  • 软件触发: 向ICSR寄存器的PENDSVSET位写1,挂起PendSV异常。
  • 延迟执行: PendSV不会立即执行,而是等待所有更高优先级的异常处理完毕后,在返回线程模式前执行。
  • 典型触发场景:
    • SysTick中断中调用任务切换(时间片轮转)。
    • 任务主动调用yield或阻塞原语(如vTaskDelay)。
    • 中断服务例程中唤醒高优先级任务。
常见调试陷阱: 如果PendSV的优先级没有设置为最低(0xFF),当高优先级中断中触发了PendSV,而PendSV又试图访问被该中断占用的资源时,会导致死锁。务必检查NVIC中PendSV的优先级配置。

4.3.2 上下文切换完整流程

  1. 触发PendSV: 调度器决定切换任务,写PENDSVSET位挂起PendSV。
  2. 硬件自动压栈: CPU响应PendSV异常,使用PSP自动压栈xPSR、PC、LR、R12、R3-R0,SP递减。
  3. 进入PendSV处理函数: CPU从向量表加载PendSV入口地址,切换到Handler模式,使用MSP。
  4. 软件压栈剩余寄存器: 在PendSV_Handler中,使用PUSH指令将R4-R11、LR压入PSP栈。
  5. 保存当前任务TCB: 将更新后的PSP值写入当前任务TCB的sp字段。
  6. 选择下一个任务: 调度器从就绪队列中选出优先级最高的任务,获取其TCB指针。
  7. 加载新任务栈指针: 从新任务TCB的sp字段读取栈指针,加载到PSP。
  8. 软件出栈恢复寄存器: 使用POP指令从PSP栈恢复R4-R11、LR。
  9. 异常返回: 执行BX LR指令(LR中为EXC_RETURN),CPU硬件自动出栈恢复xPSR、PC、LR、R12、R3-R0。
  10. 恢复新任务执行: CPU回到线程模式,使用PSP,从新任务的PC地址开始执行。

4.3.3 PendSV上下文切换流程图

PendSV上下文切换完整流程图 1. 调度器触发PendSV 2. 硬件自动压栈(8个寄存器) 3. 软件压栈 R4-R11, LR 4. 保存PSP → 当前任务TCB.sp 5. 调度器选择下一个任务 6. 新任务TCB.sp → PSP 7. 软件出栈 R4-R11, LR → BX LR 异常返回 关键调试点 • 检查PSP值变化 • 验证栈帧寄存器排列 • 确认EXC_RETURN值

4.4 上下文切换核心代码实现

以下代码展示了PendSV_Handler的典型实现,包含软件压栈、TCB保存/恢复、软件出栈和异常返回的完整逻辑。

__asm void PendSV_Handler(void)
{
    /* 保存当前任务上下文(软件压栈) */
    MRS     R0, PSP           ; 获取当前任务栈指针
    STMFD   R0!, {R4-R11, LR} ; 压栈 R4-R11 和 LR (EXC_RETURN)

    /* 保存当前任务TCB */
    LDR     R1, =current_TCB  ; 获取当前TCB指针地址
    LDR     R2, [R1]          ; 加载当前TCB指针
    STR     R0, [R2]          ; 将更新后的PSP写入TCB.sp

    /* 选择下一个任务 */
    LDR     R1, =next_TCB     ; 获取下一个TCB指针地址
    LDR     R2, [R1]          ; 加载下一个TCB指针
    STR     R2, [R1]          ; 更新 current_TCB = next_TCB

    /* 恢复新任务上下文(软件出栈) */
    LDR     R0, [R2]          ; 从新TCB加载PSP
    LDMFD   R0!, {R4-R11, LR} ; 出栈恢复 R4-R11 和 LR

    /* 设置PSP并异常返回 */
    MSR     PSP, R0           ; 更新PSP
    BX      LR                ; 异常返回,硬件自动出栈剩余寄存器
}
调试技巧: 在PendSV_Handler入口处设置断点,观察R0(PSP值)和栈内存。硬件压栈后的栈帧起始地址应8字节对齐。如果发现栈帧错位,通常是因为任务创建时初始栈指针未对齐,或者中断嵌套导致栈指针混乱。

4.5 上下文切换时序与中断嵌套处理

在中断活跃期间,PendSV被阻塞,不会立即执行上下文切换。这保证了中断响应延迟不受任务切换影响。当中断返回时,如果PendSV被挂起,CPU会在返回线程模式前执行PendSV处理。

4.5.1 中断嵌套场景下的上下文切换时序

中断嵌套场景下的PendSV执行时序 时间 任务A执行 低优先级中断 高优先级中断 PendSV挂起 PendSV执行 任务B执行 时序说明: 1. 任务A执行期间,低优先级中断触发,任务A被抢占。 2. 低优先级中断中触发PendSV(挂起),但PendSV被阻塞。 3. 高优先级中断嵌套进入,处理完毕后返回低优先级中断。 4. 低优先级中断返回前,PendSV执行上下文切换,切换到任务B。

4.5.2 中断嵌套下的调试检查清单

检查项 预期行为 异常现象
PendSV优先级 设置为0xFF(最低) PendSV在中断中执行,导致上下文切换破坏中断栈帧
PSP使用 线程模式使用PSP,Handler模式使用MSP 栈指针混乱,栈帧被覆盖
EXC_RETURN值 返回线程模式+PSP:0xFFFFFFFD 异常返回后使用错误栈指针,导致HardFault
中断嵌套深度 PendSV在中断全部返回后才执行 PendSV在中断活跃期间执行,破坏中断上下文
栈帧对齐 硬件栈帧起始地址8字节对齐 未对齐导致硬件异常或数据错位
调试方法论: 在单步调试上下文切换时,建议按以下顺序验证:
1. 在PendSV_Handler入口处设置断点,检查PSP值是否指向当前任务栈。
2. 单步执行PUSH指令后,检查栈内存中R4-R11和LR的值是否正确。
3. 保存TCB后,验证current_TCB->sp是否等于更新后的PSP。
4. 加载新任务TCB后,检查PSP是否指向新任务的栈帧。
5. 执行BX LR前,检查LR中的EXC_RETURN值是否为0xFFFFFFFD。
6. 异常返回后,检查PC是否跳转到新任务的正确地址。