# 概述
当一个进程正在运行时触发系统调用或被中断,将进行中断上下文的切换,之后执行 ISR 中断服务,在中断处理结束后,使用_schedule () 函数进行进程切换。
schedule () 函数首先从 CPU 任务队列中取出当前进程的标识符记为 prev 进程。然后通过进程调度算法确定下一个要被换上的进程,记为 next 进程。之后,检查 next 如果和 prev 进程不一样,调用 context_switch () 函数进行上下文切换,next 进程进入 CPU 运行。
在 context_switch () 中调用 switch_to () 进行寄存器和堆栈的切换,switch_to () 会调用 switch_to_asm () 函数,在 switch_to_asm () 的中进行了从 prev 内核堆栈到 next 内核堆栈的切换,在最后不使用 ret 指令,而是通过 jmp 指令跳转到 switch_to () 函数,在 switch_to () 函数的结尾调用 return 返回,因为在 switch_to_asm () 中进行了堆栈的切换,因此_switch_to () 返回后,回到的是 next 进程的内核堆栈,而不是 prev 进程的内核堆栈。
# 具体过程分析
schedule () 函数调用 context_switch () 函数进行上下文切换,在 kernel/sched/core.c 中查看函数的定义:
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); | |
if (!next->mm) { // to kernel | |
enter_lazy_tlb(prev->active_mm, next); | |
next->active_mm = prev->active_mm; | |
if (prev->mm) // from user | |
mmgrab(prev->active_mm); | |
else | |
prev->active_mm = NULL; | |
} else { // to user | |
membarrier_switch_mm(rq, prev->active_mm, next->mm); | |
switch_mm_irqs_off(prev->active_mm, next->mm, next); | |
if (!prev->mm) { // from kernel | |
/* will mmdrop() in finish_task_switch(). */ | |
rq->prev_mm = prev->active_mm; | |
prev->active_mm = NULL; | |
} | |
} | |
rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP); | |
prepare_lock_switch(rq, next, rf); | |
switch_to(prev, next, prev); | |
barrier(); | |
return finish_task_switch(prev); | |
} |
content_switch 函数有三个参数:rq、prev、next,其中 rq 指向本次进程切换发生的 running queue (进程就绪队列);prev 和 next 分别指向切换前后进程的进程描述符。实现的功能如下:
- 在进程切换之前调用 prepare_task_switch (),然后内核会执行与体系结构相关的一些调测指令,该函数和 finish_task_switch () 成对出现,表示完成上下文的切换。
- arch_start_context_switch () 给各个体系结构专有的开始上下文切换的工作提供入口
- 完成进程地址空间的切换。这里通过判断一个 task_struct 的 mm 成员是否为空来判断它是一个用户进程还是内核级线程,为内核级线程则调用 enter_lazy_tlb ()。
- 调用 switch_to () 切换寄存器状态和栈,swtich_to 函数会进一步调用 __switch_to_asm ()
__switch_to_asm () 实现是与体系结构相关的,这里我们先以 X86_64 为例进行分析:
ENTRY(__switch_to_asm) | |
UNWIND_HINT_FUNC | |
/* | |
* Save callee-saved registers | |
* This must match the order in inactive_task_frame | |
*/ | |
pushq %rbp | |
pushq %rbx | |
pushq %r12 | |
pushq %r13 | |
pushq %r14 | |
pushq %r15 | |
/* switch stack */ | |
movq %rsp, TASK_threadsp(%rdi) // 保存旧进程的栈顶 | |
movq TASK_threadsp(%rsi), %rsp // 恢复新进程的栈顶 | |
/* restore callee-saved registers */ | |
popq %r15 | |
popq %r14 | |
popq %r13 | |
popq %r12 | |
popq %rbx | |
popq %rbp | |
jmp __switch_to | |
END(__switch_to_asm) |
两条 movq 语句就是新旧进程的分界线,随着内核栈顶的切换,内核栈空间也就切换到了新进程,之后只需要弹出栈中保存的各个寄存器的值即可恢复寄存器状态。将__switch_to_asm 和 switch_to 结合起来,发现是 call 指令和 ret 指令的配对出现。call 指令压栈 RIP 寄存器到进程切换前的 prev 进程内核堆栈,而 ret 指令出栈存入 RIP 寄存器的是进程切换之后的 next 进程的内核堆栈栈顶数据,所以 ret 恢复的就是 next 进程内核堆栈中的 rip 值,即实现了 rip 值的保存与修改。
再看一下 arm64 架构下__switch_to_asm () 的实现 ,其过程就是保存和恢复 cpu_context 结构体,在 arm 中其函数和宏调用过程:switch_to -> switch_to -> cpu_switch_to,具体的切换发生在 cpu_switch_to 中其代码如下:
ENTRY(cpu_switch_to) | |
mov x10, #THREAD_CPU_CONTEXT // 寄存器 x10 存放 thread.cpu_context 偏移 | |
add x8, x0, x10 //x0 与偏移量相加后存入 x8,获取旧进程 cpu_context 的地址 | |
mov x9, sp // 将栈顶指针 sp 保存在 x9 寄存器 | |
stp x19, x20, [x8], #16 // 将寄存器 x19~x29 保存,保存现场 | |
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 //x29 是 frame pointer,x9 是 stack pointer,lr 是 pc 值 | |
str lr, [x8] | |
add x8, x1, x10 // 获取访问 next 进程的 cpu_context 的指针 | |
ldp x19, x20, [x8], #16 // 恢复 next 进程的现场 | |
ldp x21, x22, [x8], #16 | |
ldp x23, x24, [x8], #16 | |
ldp x25, x26, [x8], #16 | |
ldp x27, x28, [x8], #16 | |
ldp x29, x9, [x8], #16 | |
ldr lr, [x8] | |
mov sp, x9 | |
msr sp_el0, x1 | |
ret // 此时 ret 将 next 进程的 lr 寄存器的值加载到 PC,进程切换完毕 | |
ENDPROC(cpu_switch_to) | |
NOKPROBE(cpu_switch_to) |