一、进程切换是如何进行的
在 Linux 内核中,进程切换是通过上下文切换(Context Switch)来实现的。当一个进程需要被挂起,让出 CPU 给其他进程执行时,操作系统会将当前进程的上下文(包括 CPU 寄存器和内存映像等状态)保存在进程的内核态栈(Kernel Stack)中,然后将 CPU 分配给下一个需要执行的进程,并将其对应的上下文恢复到 CPU 寄存器和内存中,使其能够继续执行。
下面是进程切换的具体流程:
当一个进程被调度器选中需要被挂起时,操作系统会保存当前进程的所有寄存器状态到进程的内核态栈中,包括通用寄存器、堆栈指针、程序计数器等。这样可以保证进程下一次执行时能够恢复到之前执行的状态。
然后操作系统会从就绪队列中选中一个需要执行的进程,将其相应的上下文信息加载到 CPU 的寄存器和内存中,使其能够开始执行。
当 CPU 执行到一个时间片结束或者进程主动放弃 CPU 执行权时,操作系统会将当前进程的上下文保存到内核态栈中,同时将 CPU 分配给下一个需要执行的进程。这个过程就是上下文切换。
在切换到下一个进程时,操作系统会从内核态栈中加载下一个进程的上下文信息到 CPU 寄存器和内存中,使其能够继续执行。这样就完成了进程的切换。
二、代码分析
/*
* 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);
}
-
prepare_task_switch(rq, prev, next):用于准备任务切换,主要是更新两个进程的状态和调度队列等信息。
-
arch_start_context_switch(prev):用于在切换进程之前执行一些体系结构相关的操作。对于 paravirt 架构,这个函数会和 switch_to() 函数中的一部分代码合并为一个 hypercall。
-
enter_lazy_tlb(prev->active_mm, next):用于进行懒惰 TLB 刷新,它会延迟 TLB 刷新操作,直到需要访问一个无效的页表项时才会刷新 TLB。这样可以减少 TLB 刷新的次数,提高系统性能。
-
switch_mm_irqs_off(prev->active_mm, next->mm, next):用于切换两个进程的地址空间,其中 prev->active_mm 是前一个进程所使用的地址空间,next->mm 是下一个进程所使用的地址空间。
-
rq->prev_mm = prev->active_mm:用于将前一个进程的地址空间保存在调度队列中,以便在下一次调度时能够恢复它的状态。
-
prepare_lock_switch(rq, next, rf):用于准备锁切换,主要是更新调度队列中的锁等信息。
-
switch_to(prev, next, prev):用于实际进行进程上下文切换,其中 prev 是前一个进程的任务结构体,next 是下一个进程的任务结构体,prev 是前一个进程的内核栈。
-
finish_task_switch(prev):用于完成任务切换,主要是更新调度队列中的任务等信息,并返回前一个进程的调度队列。
三、总结
通过对Linux内核进程切换的流程了解和对代码的具体分析,可以总结出:
Linux内核进程切换过程中,会保存当前进程的上下文信息,并从调度队列中选中下一个需要执行的进程,然后将下一个进程的上下文信息加载到 CPU的寄存器和内存中,使其能够继续执行。在切换进程时,它会涉及到懒惰 TLB刷新、地址空间切换、锁切换等一系列操作,以确保进程能够正确地运行。最后,它会完成任务切换,并返回前一个进程的调度队列。