上下文切换与状态保存详解

上下文切换与状态保存详解

源码路径rk3588/kernel-6.1/kernel/sched/arch/arm64/
内核版本:Linux 6.1(RK3588 / arm64)
关联文档sched调度机制与原理详解.md

调度切换时,不会把「整个进程」拷贝一份。上一任务的状态分三层保存:

  1. 一直在内存里的进程数据(堆栈、文件描述符等)
  2. 调度器在切换前更新的调度状态(vruntime、运行队列位置等)
  3. CPU 寄存器 / 栈等执行上下文switch_to 时保存)

目录


一、什么要保存,什么不用动

数据类型 存放位置 切换时是否拷贝
堆、栈、全局变量(用户内存) 进程的 mm 页表里,物理内存中 不拷贝,仍在原处
打开的文件、信号、fd 表 task_struct / files_struct 已在内核,不用每次切换保存
调度信息(vruntime、优先级、运行队列位置) task_struct + sched_entity 切换前由调度类更新
CPU 寄存器、内核栈指针、返回地址 task_struct->thread switch_to 时保存/恢复
页表(地址空间) mm_struct 切换 mm,不复制页表内容

用户代码里的局部变量、全局变量在用户栈/堆里,切换后仍在原物理页上;下次再调度到该 task,只要恢复寄存器和页表,就能从断点继续。


二、完整切换流程

__schedule 选中 nextprev 要睡眠?deactivate_task 出队<br/>task_struct 仍存活pick_next_taskput_prev_task<br/>CFS 更新 vruntime 等context_switchswitch_mm 换页表switch_to / cpu_switch_to<br/>保存 prev 寄存器在 next 上继续执行finish_task_switch<br/>在 prev 栈上收尾

关键代码路径

core.c__schedule()prev != next 时调用 context_switch()

1
2
3
4
5
6
7
8
// core.c: __schedule()
if (likely(prev != next)) {
rq->nr_switches++;
RCU_INIT_POINTER(rq->curr, next);
++*switch_count;
// ...
rq = context_switch(rq, prev, next, &rf);
}

context_switch() 步骤(core.c 注释):

  1. prepare_task_switch:perf / rseq / switched_from 钩子
  2. 切换 mm(用户态 / 内核态 / 惰性 TLB 等分支)
  3. switch_to:汇编切换栈与寄存器
  4. finish_task_switch:返回时在 prev 上下文执行清理

三、task_struct:进程的档案袋

每个进程/线程对应一个 task_struct,长期保存在内核内存,切换时不销毁

字段/子结构 作用
mm 地址空间(页表根)
thread CPU 上下文(寄存器、FPU 等)
policy / prio / sched_class 调度策略与类
sched_entity CFS 的 vruntime、红黑树节点
filessignalfs 文件、信号、文件系统状态

出队 vs 销毁

  • 被抢占(仍 TASK_RUNNING):仍在运行队列,task_struct 不变,只是暂时不是 rq->curr
  • 主动阻塞(sleep / mutex):deactivate_task() 出队,但 task_structmm 都还在
  • 进程退出TASK_DEAD 后,在 finish_task_switch() 中释放栈和 task_struct

四、调度类保存调度状态

在真正换 CPU 上下文前,各调度类的 put_prev_task() 会更新统计信息。

CFS(fair.cput_prev_entity

1
2
3
4
5
6
7
8
9
10
11
static void put_prev_entity(struct cfs_rq *cfs_rq, struct sched_entity *prev)
{
if (prev->on_rq)
update_curr(cfs_rq); // 累加 vruntime、exec_start 等

if (prev->on_rq) {
__enqueue_entity(cfs_rq, prev); // 仍 runnable 则插回红黑树
update_load_avg(cfs_rq, prev, 0); // 更新 PELT 负载
}
cfs_rq->curr = NULL;
}

保存的是:vruntime、PELT 负载、在 CFS 红黑树中的位置,不是用户变量。

RT / Deadline

类似地更新运行时间、RR 时间片、DL runtime/deadline 预算等,写入 task_struct / sched_dl_entity 相关字段。


五、context_switch:地址空间与寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// core.c: context_switch()
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next, struct rq_flags *rf)
{
prepare_task_switch(rq, prev, next);
arch_start_context_switch(prev);

/* 地址空间切换四种情况:
* kernel → kernel 惰性 TLB,复用 active_mm
* user → kernel 惰性 TLB,mmgrab active_mm
* kernel → user switch_mm 加载页表
* user → user switch_mm 切换页表
*/
if (!next->mm) { // to kernel thread
enter_lazy_tlb(prev->active_mm, next);
next->active_mm = prev->active_mm;
// ...
} else { // to user task
switch_mm_irqs_off(prev->active_mm, next->mm, next);
// ...
}

prepare_lock_switch(rq, next, rf);

/* Here we just switch the register state and the stack. */
switch_to(prev, next, prev);
barrier();

return finish_task_switch(prev);
}

地址空间(mm)

  • 用户 task → 用户 taskswitch_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_toarch/arm64/kernel/entry.S

cpu_context 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// arch/arm64/include/asm/processor.h
struct cpu_context {
unsigned long x19, x20, x21, x22, x23, x24;
unsigned long x25, x26, x27, x28;
unsigned long fp; // x29
unsigned long sp; // 内核栈指针
unsigned long pc; // 返回地址 (lr)
};

struct thread_struct {
struct cpu_context cpu_context;
// TLS、FPSIMD/SVE 浮点与向量状态、断点、MTE 等
// ...
};

汇编核心逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// arch/arm64/kernel/entry.S: cpu_switch_to
// x0 = prev task_struct, x1 = next task_struct
SYM_FUNC_START(cpu_switch_to)
mov x10, #THREAD_CPU_CONTEXT
add x8, x0, x10 // &prev->thread.cpu_context
mov x9, sp
stp x19, x20, [x8], #16 // 保存 callee-saved 寄存器
stp x21, x22, [x8], #16
stp x23, x24, [x8], #16
stp x25, x26, [x8], #16
stp x27, x28, [x8], #16
stp x29, x9, [x8], #16 // fp, sp
str lr, [x8] // pc

add x8, x1, x10 // &next->thread.cpu_context
ldp x19, x20, [x8], #16 // 恢复 next 的寄存器
// ... 对称 ldp ...
mov sp, x9 // 切换到 next 的内核栈
msr sp_el0, x1 // 用户态 SP 关联 next 的 task_struct
ret // 跳到 next 上次保存的 lr 继续执行
SYM_FUNC_END(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 打断:

  1. 进内核时:异常入口(entry.S)把 x0–x30、pc、pstate 等压到该 task 的内核栈struct pt_regs
  2. 调度时cpu_switch_to 再保存 callee-saved + sp/lrthread.cpu_context
  3. 下次恢复:先 cpu_switch_to 恢复内核上下文,再从内核栈 pt_regs 恢复用户寄存器,返回用户态

若在内核态被抢占(如持锁路径),主要保存内核 callee-saved 和栈;用户寄存器通常已在更早的入口保存过,或本就在内核执行。


八、finish_task_switch 收尾

switch_to 返回时,CPU 已在 next 上运行,但代码仍借用 prev 的内核栈 做收尾:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// core.c: finish_task_switch()
static struct rq *finish_task_switch(struct task_struct *prev)
{
struct rq *rq = this_rq();
struct mm_struct *mm = rq->prev_mm;

rq->prev_mm = NULL;
prev_state = READ_ONCE(prev->__state);

vtime_task_switch(prev); // CPU 时间统计
perf_event_task_sched_in(prev, current);
finish_task(prev);
finish_lock_switch(rq); // 释放 rq->lock
finish_arch_post_lock_switch();

if (mm)
mmdrop_sched(mm); // 释放借用的 mm 引用

if (unlikely(prev_state == TASK_DEAD)) {
put_task_stack(prev);
put_task_struct_rcu_user(prev); // 进程真正退出时释放
}

return rq;
}

九、典型场景对照

场景 保存/切换内容
线程 A → 线程 B(同 mm 各自 task_struct->thread 的寄存器/FPU;页表不变
进程 A → 进程 B 寄存器 + 切换 mm(页表)
运行 → sleep 出队 + 保存上下文;唤醒后重新入队再恢复
运行 → 运行(抢占) TASK_RUNNING;CFS 插回红黑树 + 保存寄存器
进程退出 TASK_DEADfinish_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->threadmm,下次 cpu_switch_to 恢复后继续执行。


文档基于 RK3588 / Linux 6.1 内核 kernel/schedarch/arm64 源码整理。

文章互动

阅读 --

留言

0 条留言

正在加载留言…