☰
线程与上下文切换
2026/10/1 11:40:56 网站建设 项目流程

线程:操作系统调度CPU的最小单位一个进程可以有多个线程,共享进程资源,每个线程有独立的执行流、独立栈、独立寄存器上下文。

进程:拥有独立的地址空间,全局变量、堆。开销大。

工程中一个tcb线程控制块即一个任务,线程的详细数据结构:

typedef struct tcb { //sp 放第一个 = 汇编永远用 [r1] 零偏移 void *sp; /* 当前栈指针 (切换时存/取) */ void *entry; /* 线程入口函数 */ void *parameter; /* 入口函数参数 */ void *stack_addr; /* 栈起始地址 (低地址) */ uint32_t stack_size; /* 栈大小 (字节) */ uint8_t priority; /* 优先级: 0 最高, 越大越低 */ uint8_t base_priority; /* 基础优先级 = 创建时的静态优先级 (PI 只临时改 priority, 不动它) */ uint8_t state; //线程状态 const char *name; /* 线程名 (栈检查报警 / 将来 ps 命令用) */ list_t list; /* 链表节点: 同一时刻只在 就绪队列 或 睡眠队列 之一 */ uint32_t wake_tick;/* 唤醒时刻 (绝对 tick, 替代旧 remaining_ticks) */ list_t tick_node; /* 定时器链表节点 (独立于 list, 阻塞时同时挂两条队) */ list_t all_node; /* 全局线程链表节点: 栈检查 / ps 遍历所有线程 */ list_t taken_list; /* 持有锁链表: 挂各 mutex_t.taken_node (PI 重算时遍历) */ uint8_t timeout; /* 本次唤醒是否因超时: 1=超时, 0=正常唤醒 */ int block_result; /* 阻塞 API 返回值: 0=成功, -ETIMEDOUT=超时 */ uint8_t dynamic; /* 1=TCB+栈堆分配 (idle 回收时可 free); 0=静态分配, 只摘链不释放 */ } tcb_t;

其中:

  1. sp:当前栈指针,必须放第一个成员,方便在汇编中获取sp指针时不需要计算结构体偏移。
  2. entry:线程的回调函数入口。
  3. parameter:回调函数参数。
  4. stack_addr:线程栈起始地址。
  5. stack_size:线程栈大小。
  6. priority:线程优先级大小,0最高。
  7. base_priority:基础优先级,用于PI处理。
  8. state:线程状态。
  9. name:线程名。
  10. list:链表节点。
  11. wake_tick:唤醒时间。
  12. tick_node:定时器链表。
  13. all_node:全局链表节点。
  14. taken_list:持锁链表。
  15. timeout:超时唤醒标志。
  16. block_result:阻塞的API返回值
  17. dynamic:动态创建线程的标志

线程状态如下:

typedef enum { THREAD_INIT = 0, /* 已创建, 未 thread_start (不在任何队列) */ THREAD_READY, /* 在就绪队列 */ THREAD_BLOCKED, /* 阻塞: 在某 IPC 等待队列 (sem/mutex/mq) */ THREAD_SLEEP, /* 睡眠: 在睡眠队列 */ THREAD_SUSPENDED, /* 被挂起: 在挂起队列 (被 thread_suspend) */ THREAD_DEFUNCT, /* 僵尸: 已退出, 在 g_defunct_list 等 idle 回收 TCB/栈*/ } thread_state_t;

THREAD_INIT:初始化状态,TCB与线程栈已经创建完毕,但是没有调用thread_start(),该状态下不会在任何队列,调度器目前看不到这个线程。

THREAD_READY:就绪状态,执行thread_start()后将该线程添加到就绪队列,随时可以被调度器调度到CPU执行,也可以通过其他方式进入就绪状态,比如唤醒、定时器超时等。

THREAD_BLOCKED:阻塞状态,线程主动等待某个IPC资源,比如信号量、互斥锁、消息队列等唤醒后转为READY状态,加入就绪队列。

THREAD_SLEEP:睡眠状态,线程主动休息一段时间,等待时间到了才可以继续运行,时间到转为就绪队列。

THREAD_SUSPENDED:挂起状态,内核调用thread_suspend()挂起,不参与调度,通过thread_resume()才能唤醒。

THREAD_DEFUNCT:僵尸状态,线程函数已经执行完毕,已经退出,不再运行,但是TCB任务块以及线程栈还不能立即释放,放到僵尸链表,由idle线程统一回收。

状态机如下图所示:

伪造栈帧的本质:新线程从没运行过,没有“被打断的现场”可保存,但PendSV不区分首次与后续切换,只是机械地从sp取16个寄存器恢复,伪造栈帧就是造一套假的寄存器快照放在栈里,让PendSV第一次恢复无法分辨真假,新线程就像“被打断过一样”自然执行。

伪造栈帧需要填充寄存器,Cortex-M内核存在16个寄存器R0-R15、xPSR,具体功能如下表:

寄存器名称用途是否硬件自动压栈
R0R0函数参数1是
R1R1函数参数2是
R2R2函数参数3是
R3R3函数参数4是
R4R4通用变量否
R5R5通用变量否
R6R6通用变量否
R7R7通用变量否
R8R8通用变量否
R9R9通用变量否
R10R10通用变量否
R11R11通用变量否
R12R12是
R13SP栈指针栈指针否
R14LR链接返回地址是
R15PC程序计数器是

注:xPSR是状态寄存器,硬件中断时一起压入栈帧,伪造栈帧必须置Thumb位

R0-R3、R12、LR、PC、xPSR由硬件自动压栈,R4-R11由PendSV手动压栈。SP不会压入栈。上下文切换的核心就是把当前SP存放到tcb->sp,切换时候再从tcb->sp取出。

SP指针分为MSP与PSP:

  1. MSP:主栈指针,复位默认使用,中断服务函数默认使用MSP。
  2. PSP:进程栈指针,一般用于任务线程栈。

LR链接寄存器:正常调用函数bl func相当于LR=下一条指令地址,函数return跳LR,进入异常时,硬件自动保存一个特殊的EXC_RETURN值到LR,用于异常返回。

PC程序计数器:PC保存当前正在取指的指令地址,CPU没取一条指令PC自动加1.

上下文切换逻辑:

  1. 进入PendSV异常,硬件自动压栈R0 R1 R2 R3 LR PC xPSR
  2. PendSV汇编手动压栈R4-R11
  3. 将SP存入tcb->sp
  4. 调度器选择新的tcb任务,取出新tcb的sp
  5. PendSV弹出R4-R11
  6. 异常返回,硬件自动弹出R0 R1 R2 R3 LR PC xPSR跳转,线程恢复运行

压栈完成如下所示:

高地址 (栈底)
xPSR = 0x01000000 ┐
pc = entry │
lr = thread_exit │
r12 = 0 │ 硬件异常栈帧
r3 = 0 │ (8 个, bx lr 时硬件自动弹)
r2 = 0 │
r1 = 0 │
r0 = parameter ┘
─────────────────────
r11 = 0xdeadbeef ┐
r10 = 0xdeadbeef │
r9 = 0xdeadbeef │ 软件保存区
r8 = 0xdeadbeef │ (8 个, PendSV 用 ldmia 弹)
r7 = 0xdeadbeef │
r6 = 0xdeadbeef │
r5 = 0xdeadbeef │
r4 = 0xdeadbeef ┘
低地址 (栈顶)
↑ TCB.sp 指向这里 (指向 r4 的位置)

栈向下生长,栈的上面是高地址下面是低地址,硬件压栈在高地址,软件压栈在低地址。

压栈:硬件自动压栈顺序为xPSR -> PC -> LR -> R12 -> R3 -> R2 -> R1 -> R0

软件压栈顺序为 R11 -> R10 -> R9 -> R8 -> R7 -> R6 -> R5 -> R4

出栈:软件先弹,顺序为 R4 -> R5 -> R6 -> R7 -> R8 -> R9 -> R10 -> R11。SP随之到R0位置

硬件在出弹,顺序为 R0 -> R1 -> R2 -> R3 -> R12 -> LR -> PC -> xPSR。PC被填成entry任务从入口开始跑。

伪造栈帧过程如下:

(1)首先填充整个栈为0xDE

(2)计算栈顶,并设置八字节对齐,这里通过& ~7UL设置对齐,通过&低三位0让其向下对齐到八字节,如下

stk = (uint32_t *)((uint32_t)stack_top & ~7UL);

(3)R0填充回调函数的参数,R1-R3、R12设置为0,LR填充退出回调函数,PC填充需要执行的回调函数,Thumb置1,如下:

frame->exception.r0 = (uint32_t)parameter; /* entry 的第一个参数 */ frame->exception.r1 = 0; frame->exception.r2 = 0; frame->exception.r3 = 0; frame->exception.r12 = 0; frame->exception.lr = (uint32_t)exit; /* entry return 后跳这里 */ frame->exception.pc = (uint32_t)entry; /* 启动时跳到这里 */ frame->exception.psr = 0x01000000UL; /* Thumb 位 = 1, 必须 */

对tcb线程控制块的变量进行初始化后,线程初始化完成。

上下文切换是由PendSV触发的,PendSV具体实现代码如下:

(1)获取PSP指针

mrs r0, psp

(2)获取当前正在执行的tcb的指针并获取里面内容

ldr r1, =g_current_tcb

ldr r1, [r1]

(3)将R4-R11压栈

stmdb r0!, {r4-r11}

(4)将正在运行任务的tcb指针保存到tcb->sp中

str r0, [r1]

(5)取下一个任务的tcb

ldr r1, =g_next_tcb
ldr r1, [r1]
ldr r0, [r1]

(6)将当前任务的指针指向下一个任务

ldr r2, =g_current_tcb
str r1, [r2]

(7)弹出R4-R11

ldmia r0!, {r4-r11}

(8)设置新的任务栈地址,切换栈帧

msr psp, r0

(9)EXC_RETURN 触发硬件出栈,弹出硬件自动压栈寄存器,返回线程模式

  • 0xFFFFFFFD:返回线程模式,使用 PSP
  • 0xFFFFFFF9:返回线程模式,使用 MSP
  • 0xFFFFFFF1:返回异常模式

ldr lr, =EXC_RETURN_PSP
bx lr

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

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

立即咨询