Lab5:深入理解进程切换
Linux的进程切换由context_switch函数完成,函数位于源码目录的 kernel/sched/core.c 中,代码如下:
/*
* context_switch - switch to the new MM and the new thread's register state.
*/
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);
/*
* For paravirt, this is coupled with an exit in switch_to to
* combine the page table reload and the switch backend into
* one hypercall.
*/
arch_start_context_switch(prev);
/*
* kernel -> kernel lazy + transfer active
* user -> kernel lazy + mmgrab() active
*
* kernel -> user switch + mmdrop() active
* user -> user switch
*/
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);
/*
* sys_membarrier() requires an smp_mb() between setting
* rq->curr / membarrier_switch_mm() and returning to userspace.
*
* The below provides this either through switch_mm(), or in
* case 'prev->active_mm == next->mm' through
* finish_task_switch()'s mmdrop().
*/
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);
/* Here we just switch the register state and the stack. */
switch_to(prev, next, prev);
barrier();
return finish_task_switch(prev);
}
content_switch 函数有4个参数:rq、prev、next、rf,其中 rq 指向本次进程切换发生的 running queue;prev 和 next 分别指向切换前后进程的进程描述符;rf则起到标志作用,下面看看函数各个部分:
prepare_task_switch(rq, prev, next)
完成任务调度的准备工作。在进程切换之前,首先执行调用每个体系结构都必须定义的prepare_task_switch挂钩,这使得内核执行特定于体系结构的代码,为切换做事先准备。
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);
/*
* sys_membarrier() requires an smp_mb() between setting
* rq->curr / membarrier_switch_mm() and returning to userspace.
*
* The below provides this either through switch_mm(), or in
* case 'prev->active_mm == next->mm' through
* finish_task_switch()'s mmdrop().
*/
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;
}
}
该段代码实现了进程地址空间的切换。
prev 是进程切换前执行的进程(后面简称 A 进程)
next 是进程切换后要执行的进程(后面简称 B 进程)
next->mm 指向 B 进程的地址空间描述符
prev->mm 指向 A 进程的当前正在使用的地址空间描述符(active_mm)
对于用户进程来说,其进程描述符(task_struct)的 mm 和 active_mm 相同,都是指向其进程地址空间。
对于内核线程而言,其 task_struct 的 mm 成员为 NULL(内核线程没有进程地址空间),然而内核线程执行的时候,总是需要一个进程地址空间,而 active_mm 就是指向它借用的那个进程地址空间。
通过上面的分析,可以知道,我们可以通过判断一个 task_struct 的 mm 成员是否为空来判断它是一个用户进程还是内核级线程。如果 B 进程为内核级线程,那么我们需要调用体系结构相关的函数 enter_lazy_tlb,标识该 CPU 核进入 lazy tlb mode。下一行 next->active_mm = prev->active_mm 把 A 进程的 mm 直接拿到了 B 进程中,即内核级线程借用用户进程地址空间运行。
if (!next->mm) { // to kernel
enter_lazy_tlb(prev->active_mm, next);
next->active_mm = prev->active_mm;
之后对上一个进程的类别加以区分:如果 A 进程是用户进程(prev->mm 不为空),则需要将这个被借用的 mm_struct 的引用计数增加一,如果该 mm_struct 对应的用户进程已经退出,则系统需要等到其引用计数为 0(即不再有内核线程借用它)才能将其销毁;如果 A 进程是内核级线程,则把 A 进程的 active_mm 成员清空,结束其对于该 mm_struct 的借用(这只是把借用它的内核线程从一个转换到了另一个,故引用计数无需增加)。
if (prev->mm) // from user
mmgrab(prev->active_mm);
else
prev->active_mm = NULL;
接下来我们来看 B 进程为用户进程的情况。首先 membarrier_switch_mm(rq, prev->active_mm, next->mm) 使用了一个内存屏障,来保证上一个进程访问其内存空间与下一个进程访问其内存空间之间的先后顺序(某种程度上是一种进程同步机制),避免在访存进行过程中发生 mm_struct 的切换导致的访存错误。
终于到了 switch_mm_irqs_off(prev->active_mm, next->mm, next),即真正切换 mm_struct。x86 中定义的 switch_mm_irqs_off 函数在 /arch/x86/mm/tlb.c 下。
最后,如果 A 进程是内核级线程,则需要设置 rq->prev_mm 用于后续清除引用计数,并执行 prev->active_mm = NULL 解除对 active_mm 的借用。
else { // to user
membarrier_switch_mm(rq, prev->active_mm, next->mm);
/*
* sys_membarrier() requires an smp_mb() between setting
* rq->curr / membarrier_switch_mm() and returning to userspace.
*
* The below provides this either through switch_mm(), or in
* case 'prev->active_mm == next->mm' through
* finish_task_switch()'s mmdrop().
*/
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;
}
}
switch_to(prev, next, prev)
切换寄存器状态和栈。
#define switch_to(prev, next, last) \
do { \
prepare_switch_to(next); \
\
((last) = __switch_to_asm((prev), (next))); \
} while (0)
swtich_to 函数会进一步调用 __switch_to_asm,而 __switch_to_asm 的实现是和体系结构强相关的。下面是X86体系结构的代码:
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)
在_switch_to_asm()的中进行了从prev内核堆栈到next内核堆栈的切换,在最后不使用ret指令,而是通过jmp指令跳转到_switch_to()函数,在_switch_to()函数的结尾调用return返回,因为在_switch_to_asm()中进行了堆栈的切换,因此_switch_to()返回后,回到的是next进程的内核堆栈,而不是prev进程的内核堆栈。