在Linux内核中,context_switch函数是一个非常重要的函数,用于实现进程的上下文切换。它位于sched/core.c文件中,负责在不同进程之间进行切换。
context_switch函数的主要功能是保存当前运行进程的上下文,并加载要切换到的目标进程的上下文,以便让目标进程继续执行。它涉及到一系列的操作,包括保存和恢复寄存器、更新页表和任务切换。
下面是对context_switch函数的详细介绍:
保存当前进程的上下文:
首先,当前进程的寄存器上下文被保存到其内核线程的内核栈中。这些寄存器包括通用寄存器、程序计数器PC、栈指针SP以及其他特定于体系结构的寄存器。
内核栈的起始位置会被更新,以便在需要切换回当前进程时能够正确地恢复寄存器上下文。
更新页表和任务切换:
如果切换到的目标进程属于不同的地址空间,那么页表会被切换到目标进程的页表,以确保正确的内存映射。
目标进程的任务结构指针和线程信息指针会被更新,以便内核能够跟踪和管理当前运行的进程。
加载目标进程的上下文:
目标进程的寄存器上下文会从其内核线程的内核栈中恢复。
这包括恢复通用寄存器、程序计数器PC、栈指针SP以及其他特定于体系结构的寄存器。
切换到目标进程执行:
在完成上下文切换后,控制权被转移到目标进程的代码中,使其可以继续执行。
需要注意的是,context_switch函数是高度与体系结构相关的,不同的处理器架构可能会有不同的实现。因此,在实际代码中,可能会存在多个版本的context_switch函数,针对不同的处理器架构进行了优化和适配。
通过context_switch函数的实现,Linux内核能够实现进程的切换和调度,从而实现多任务并发执行。它是操作系统内核中实现进程调度的核心部分之一,对于系统的性能和资源管理起着至关重要的作用。
下面是它的具体代码:
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);
}
调度队列是一个序列,rq指本次进程切换时的运行队列,prev指向切换前的进程,next指向切换后进程。
具体切换过程如下:
1.在进程切换前调用prepare_task_switch函数来使得内核执行一些相关的指令。
2.调用 arch_start_context_switch 函数开始进行进程切换。
3.下面的代码进行进程地址切换
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;
}
}
决定切换到内核模式还是用户模式。
如果 next->mm 为 NULL,即为切换到内核模式,此时调用 enter_lazy_tlb 函数,进入懒惰的 TLB模式,将 next->active_mm 设置为 prev->active_mm,如果 prev->mm 不为 NULL,表示当前进程是用户态进程,调用 mmgrab函数,否则将 prev->active_mm 设置为 NULL。
如果 next->mm 不为 NULL,即切换到用户模式,调用 membarrier_switch_mm 函数,使用内存屏障,保证上一个进程访问其内存空间与下一个进程访问其内存空间之间的先后顺序,调用 switch_mm_irqs_of函数,来真正切换内存管理结构,如果prev->mm 为 NULL,则表示是从内核模式切换过来的,则需要设置 rq->prev_mm 用于后续清除引用计数,并执行 prev->active_mm = NULL 解除对 active_mm 的借用
执行swtich_to 函数调用 __switch_to_asm,在两个进程之间切换 CPU 寄存器状态和栈,它会保存当前进程(prev)的寄存器状态,加载新进程(next)的寄存器状态。
最后,调用 finish_task_switch 函数,完成进程切换,这个函数与第一步的prepare_task_switch函数是成对出现的,用来完成一些清理操作。