一、进程切换概述
当一个进程正在运行时触发系统调用或被中断,将进行中断上下文的切换,之后执行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() –> switch_to –> __switch_to()。
schedule函数
内核级线程可以主动调用,普通用户进程调度的时机发生在在中断服务结束后中断返回前。
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 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);
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);
}
- 在进程切换之前调用prepare_task_switch(),然后内核会执行与体系结构相关的一些调测指令,该函数和 finish_task_switch()成对出现,表示完成上下文的切换。
- arch_start_context_switch()给各个体系结构专有的开始上下文切换的工作提供入口
- 完成进程地址空间的切换。这里通过判断一个 task_struct 的 mm 成员是否为空来判断它是一个用户进程还是内核级线程,为内核级线程则调用enter_lazy_tlb()。
- 调用switch_to()切换寄存器状态和栈,swtich_to 函数会进一步调用 __switch_to_asm()
__swtich_to_asm函数
进程关键上下文的切换swtich_to
ENTRY(__switch_to_asm) //定义一个名为__switch_to_asm的标号,作为进程切换函数的入口
// 将寄存器%rbp、%rbx、%r12、%r13、%r14和%r15的值保存到当前进程的内核栈中,以便在函数返回时进行恢复
pushq %rbp
pushq %rbx
pushq %r12
pushq %r13
pushq %r14
pushq %r15
/* switch stack */
// 将当前进程所使用的内核栈顶指针保存到旧进程的task_struct结构中的thread.sp字段中,
// 并将新进程的task_struct结构中的thread.sp字段中保存的值作为新进程的内核栈顶指针。
movq %rsp, TASK_threadsp(%rdi)
movq TASK_threadsp(%rsi), %rsp
// 从当前进程的内核栈中弹出之前保存的%rbp、%rbx、%r12、%r13、%r14和%r15寄存器的值,并恢复其原本的值
popq %r15
popq %r14
popq %r13
popq %r12
popq %rbx
popq %rbp
jmp __switch_to //跳转到C语言实现的__switch_to函数,完成进程切换操作
END(__switch_to)
最后一步使用jump,将ip寄存器恢复,在切换过程中正好将当前进程的ip寄存器通过call压栈,并通过ret返回了下一个进程的ip寄存器,完成了ip的保存与修改。
ARM环境下的cpu_switch_to函数
ENTRY(cpu_switch_to)
mov x10, #THREAD_CPU_CONTEXT // 寄存器x10存放thread.cpu_context偏移,与进程task_struct地址相加后即可获得该进程的cpu_context
add x8, x0, x10 // x0与偏移量相加后存入x8,获取旧进程cpu_context的地址
mov x9, sp // 将栈顶sp存入x9,以备后续保存
// 保存x19~x28寄存器的值,每条指令执行完毕后x8的值会自动+16,以便保存后续寄存器值
stp x19, x20, [x8], #16
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(栈基址)与x9(栈顶sp)
str lr, [x8] // 保存寄存器LR,该寄存器存放了cpu_switch_to函数的返回地址
add x8, x1, x10 // x1与偏移量相加后存入x8,获取新进程cpu_context的地址
// 恢复x19~x28寄存器的值
ldp x19, x20, [x8], #16
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 // 恢复x29(栈基址)与x9(栈顶sp)
ldr lr, [x8] // 恢复寄存器LR,这样函数cpu_switch_to返回后就会从新进程上次被中断的位置处继续执行
mov sp, x9 // 从x9处恢复sp的值
msr sp_el0, x1 // 将新进程进程task_struct地址放入sp_el0
ret
ENDPROC(cpu_switch_to)
NOKPROBE(cpu_switch_to)
同样是保存当前进程上下文,然后切换后,通过新进程的sp指针进行上下文的恢复。对于PC的保存同样是在此函数之前通过硬件来完成,PC的恢复则通过ret指令来完成。