首先找到Linux 内核中的上下文切换函数 content_switch ,以此分析内核中进程切换的基本操作与基本代码框架。找到 Linux 内核源码目录的 kernel/sched/core.c ,代码如下:
content_switch 函数有三个参数:rq、prev、next,其中 rq 指向本次进程切换发生的 running queue;prev 和 next 分别指向切换前后进程的进程描述符。
分析代码如下:
首先执行的是 prepare_task_switch。该函数在进程切换之前调用,内核会执行与体系结构相关的一些调测指令。这是在持有 rq 锁并关闭中断的情况下调用的。 它必须与上下文切换后的后续 finish_task_switch 配对。 prepare_task_switch 设置锁定并调用特定于体系结构的hook。
接着执行arch_start_context_switch()。该函数给各个体系结构专有的开始上下文切换的工作提供了入口,不同体系结构的实现不同。
再接着,一段代码实现进程地址空间的切换。
/*
* kernel -> kernel lazy + transfer active
* user -> kernel lazy + mmgrab() active
*
* kernel -> user switch + mmdrop() active
* user -> user switch
*/
prev 是进程切换前执行的进程,next 是进程切换后要执行的进程。
next->mm 指向切换后进程的地址空间描述符,prev->mm 指向 切换前进程的当前正在使用的地址空间描述符(active_mm)。
对于用户进程来说,其进程描述符(task_struct)的 mm 和 active_mm 相同,都是指向其进程地址空间。对于内核线程而言,其 task_struct 的 mm 成员为 NULL,然而内核线程执行的时候,总是需要一个进程地址空间,而 active_mm 就是指向它借用的那个进程地址空间。所以,我们可以通过判断一个 task_struct 的 mm 成员是否为空来判断它是一个用户进程还是内核级线程。
重要函数switch_mm_irqs_off(prev->active_mm, next->mm, next),即真正切换 mm_struct。x86 中定义的 switch_mm_irqs_off 函数在 /arch/x86/mm/tlb.c 下。
下一个重要函数是 switch_to,即切换寄存器状态和栈。swtich_to 函数会进一步调用__switch_to_asm,而 __switch_to_asm 的实现是和体系结构强相关的。
#define switch_to(prev, next, last) \
do { \
prepare_switch_to(next); \
\
((last) = __switch_to_asm((prev), (next))); \
} while (0)
现以 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 是在 C 语言中调用的,对应汇编就是 call 指令,而这段汇编的结尾是 jmp __switch_to,并不是 ret;__switch_to 函数对应了一个 C 函数,最后必然有 return,也就是 ret 指令。将 __switch_to_asm 和 __switch_to 结合起来,正好是 call 指令和 ret指令的配对出现。call 指令压栈时,内核堆栈还是 prev 进程的,所以 rip 寄存器的值被存入 prev 进程内核堆栈;而 ret 指令出栈时,内核堆栈已经切换到 next 进程,所以 ret 恢复的就是 next 进程内核堆栈中的 rip 值,这样就巧妙实现了 rip 值的保存与修改。