上下文切换与状态保存详解
源码路径:
rk3588/kernel-6.1/kernel/sched/、arch/arm64/
内核版本:Linux 6.1(RK3588 / arm64)
关联文档:sched调度机制与原理详解.md
调度切换时,不会把「整个进程」拷贝一份。上一任务的状态分三层保存:
- 一直在内存里的进程数据(堆栈、文件描述符等)
- 调度器在切换前更新的调度状态(vruntime、运行队列位置等)
- CPU 寄存器 / 栈等执行上下文(
switch_to时保存)
目录
- 一、什么要保存,什么不用动
- 二、完整切换流程
- 三、task_struct:进程的档案袋
- 四、调度类保存调度状态
- 五、context_switch:地址空间与寄存器
- 六、arm64 寄存器保存(cpu_switch_to)
- 七、用户态寄存器何时保存
- 八、finish_task_switch 收尾
- 九、典型场景对照
- 十、源码阅读顺序
一、什么要保存,什么不用动
| 数据类型 | 存放位置 | 切换时是否拷贝 |
|---|---|---|
| 堆、栈、全局变量(用户内存) | 进程的 mm 页表里,物理内存中 |
不拷贝,仍在原处 |
| 打开的文件、信号、fd 表 | task_struct / files_struct 等 |
已在内核,不用每次切换保存 |
| 调度信息(vruntime、优先级、运行队列位置) | task_struct + sched_entity |
切换前由调度类更新 |
| CPU 寄存器、内核栈指针、返回地址 | task_struct->thread |
switch_to 时保存/恢复 |
| 页表(地址空间) | mm_struct |
切换 mm,不复制页表内容 |
用户代码里的局部变量、全局变量在用户栈/堆里,切换后仍在原物理页上;下次再调度到该 task,只要恢复寄存器和页表,就能从断点继续。
二、完整切换流程
关键代码路径
core.c 中 __schedule() 在 prev != next 时调用 context_switch():
1 | // core.c: __schedule() |
context_switch() 步骤(core.c 注释):
prepare_task_switch:perf / rseq / switched_from 钩子- 切换
mm(用户态 / 内核态 / 惰性 TLB 等分支) switch_to:汇编切换栈与寄存器finish_task_switch:返回时在 prev 上下文执行清理
三、task_struct:进程的档案袋
每个进程/线程对应一个 task_struct,长期保存在内核内存,切换时不销毁:
| 字段/子结构 | 作用 |
|---|---|
mm |
地址空间(页表根) |
thread |
CPU 上下文(寄存器、FPU 等) |
policy / prio / sched_class |
调度策略与类 |
sched_entity |
CFS 的 vruntime、红黑树节点 |
files、signal、fs |
文件、信号、文件系统状态 |
出队 vs 销毁
- 被抢占(仍
TASK_RUNNING):仍在运行队列,task_struct不变,只是暂时不是rq->curr - 主动阻塞(sleep / mutex):
deactivate_task()出队,但task_struct和mm都还在 - 进程退出:
TASK_DEAD后,在finish_task_switch()中释放栈和task_struct
四、调度类保存调度状态
在真正换 CPU 上下文前,各调度类的 put_prev_task() 会更新统计信息。
CFS(fair.c:put_prev_entity)
1 | static void put_prev_entity(struct cfs_rq *cfs_rq, struct sched_entity *prev) |
保存的是:vruntime、PELT 负载、在 CFS 红黑树中的位置,不是用户变量。
RT / Deadline
类似地更新运行时间、RR 时间片、DL runtime/deadline 预算等,写入 task_struct / sched_dl_entity 相关字段。
五、context_switch:地址空间与寄存器
1 | // core.c: context_switch() |
地址空间(mm)
- 用户 task → 用户 task:
switch_mm()加载 next 的页表(arm64 上如TTBR0_EL1) - 同进程多线程:共享同一
mm,页表往往不变,切换更轻 - 切到内核线程(无
mm):惰性 TLB,借用active_mm
用户内存数据在页表里,只换「当前用哪套页表」,不搬数据。
六、arm64 寄存器保存(cpu_switch_to)
调用链:switch_to → __switch_to()(arch/arm64/kernel/process.c)→ cpu_switch_to(arch/arm64/kernel/entry.S)
cpu_context 结构
1 | // arch/arm64/include/asm/processor.h |
汇编核心逻辑
1 | // arch/arm64/kernel/entry.S: cpu_switch_to |
__switch_to 额外切换的状态
__switch_to() 在 cpu_switch_to 前后还处理:
| 函数 | 保存/恢复内容 |
|---|---|
fpsimd_thread_switch() |
NEON / FP 寄存器 → thread.uw.fpsimd_state |
tls_thread_switch() |
TLS 寄存器 tpidr_el0 |
hw_breakpoint_thread_switch() |
硬件断点 |
mte_thread_switch() |
MTE 标签检查状态 |
ptrauth_thread_switch_user() |
指针认证密钥 |
七、用户态寄存器何时保存
若上一任务在用户态被 tick 中断或 syscall 打断:
- 进内核时:异常入口(
entry.S)把 x0–x30、pc、pstate 等压到该 task 的内核栈(struct pt_regs) - 调度时:
cpu_switch_to再保存 callee-saved + sp/lr 到thread.cpu_context - 下次恢复:先
cpu_switch_to恢复内核上下文,再从内核栈pt_regs恢复用户寄存器,返回用户态
若在内核态被抢占(如持锁路径),主要保存内核 callee-saved 和栈;用户寄存器通常已在更早的入口保存过,或本就在内核执行。
八、finish_task_switch 收尾
switch_to 返回时,CPU 已在 next 上运行,但代码仍借用 prev 的内核栈 做收尾:
1 | // core.c: finish_task_switch() |
九、典型场景对照
| 场景 | 保存/切换内容 |
|---|---|
线程 A → 线程 B(同 mm) |
各自 task_struct->thread 的寄存器/FPU;页表不变 |
| 进程 A → 进程 B | 寄存器 + 切换 mm(页表) |
| 运行 → sleep | 出队 + 保存上下文;唤醒后重新入队再恢复 |
| 运行 → 运行(抢占) | 仍 TASK_RUNNING;CFS 插回红黑树 + 保存寄存器 |
| 进程退出 | TASK_DEAD;finish_task_switch 释放栈和 task_struct |
十、源码阅读顺序
建议按以下顺序对照阅读:
| 顺序 | 文件 | 函数/符号 |
|---|---|---|
| 1 | kernel/sched/core.c |
__schedule() |
| 2 | kernel/sched/core.c |
pick_next_task() / context_switch() |
| 3 | kernel/sched/fair.c |
put_prev_entity() / pick_next_entity() |
| 4 | arch/arm64/kernel/process.c |
__switch_to() |
| 5 | arch/arm64/kernel/entry.S |
cpu_switch_to |
| 6 | arch/arm64/include/asm/processor.h |
struct thread_struct / cpu_context |
| 7 | kernel/sched/core.c |
finish_task_switch() |
总结
用户数据(堆栈变量)一直在进程的地址空间里,不用在切换时拷贝;内核把「还能不能跑、该跑多久」写在
task_struct和调度队列里;真正「断点续跑」靠的是把 CPU 寄存器、内核栈、页表指针存进/读出task_struct->thread和mm,下次cpu_switch_to恢复后继续执行。
文档基于 RK3588 / Linux 6.1 内核 kernel/sched 与 arch/arm64 源码整理。
正在加载留言…