本文主要分析 Linux 5.4.34 版本内核中进程切换的基本操作与基本代码框架。
一、进程切换的工作机制
在实际的代码中,每个进程切换基本都由两个步骤组成,即切换页全局目录以安装一个新的地址空间以及切换内核态堆栈和进程cpu上下文。
具体的代码走向如下:schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换。context_switch首先调用switch_mm切换CR3,然后调用宏switch_to来进行CPU上下文切换。
content_switch 函数位于 Linux 内核源码目录的 kernel/sched/core.c 中,代码如下:
content_switch 函数有三个参数:rq、prev、next,其中 rq 指向本次进程切换发生的 进程就绪队列;prev 和 next 分别指向切换前后进程的进程描述符。下面我们来分析一下具体的执行过程。
看代码可知,首先执行的是prepare_task_switch函数,该函数需要在进程切换之前调用,内核会执行与体系结构相关的一些调测指令。上下文切换完成后,必须调用 finish_task_switch,即这两个函数一定是要成对出现的。
然后是arch_start_context_switch()函数,该函数给各个体系结构专有的开始上下文切换的工作提供了入口,在不同的体系结构中其实现是不同的。
在本函数的主体部分(上述if-else部分)实现了进程地址空间切换过程,这里prev是进程切换之前的进程,next是进程切换后要执行的进程,mm是进程的地址空间描述符。
-
这里首先判断next进程mm是否为空,如果为空的话,说明是内核级线程(否则就是用户进程),如果是内核级线程的话需要调用enter_lazy_tlb,标记cpu内核进入了lazy tlb mode(这里查阅了资料应该是为了减少切换上下文时不必要的TLB更新,CPU进入该模式后不对TLB进行更新)。然后地址空间不用更改,如果之前的是用户进程,则引用计数增加1个,如果之前的进程是内核级线程,则需要把原来的进程的active_mm清空,结束对mm_struct的借用。
-
如果next->mm不为空,即要切换到的进程为用户进程。首先调用membarrier_switch_mm函数来建立了一个内存屏障,来保证上一个进程访问其内存空间与下一个进程访问内存空间之前的一个先后顺序(其实就是一个进程同步),避免在访存时出现访存错误;然后是执行switch_mm_irqs_off函数,即真正切换 mm_struct;最后如果切换前的进程是内核进程,则需要设置一些东西来用于后续清除引用计数。
最后面就是switch_to函数,即切换寄存器状态和栈,switch_to会进一步调用__switch_to_asm,而 __switch_to_asm 的实现是和体系结构强相关的。
二、sp和ip在不同体系及结构下汇编代码的切换方法
__switch_to_asm是在C代码中调用的,也就是使用call指令,而这段汇编的结尾是jmp __switch_to,__switch_to函数是C代码最后有个return,也就是ret指令。 将__switch_to_asm和__switch_to结合起来,正好是call指令和ret指令的配对出现。 call指令压栈RIP寄存器到进程切换前的prev进程内核堆栈;而ret指令出栈存入RIP寄存器的是进程切换之后的next进程的内核堆栈栈顶数据.
然后下面是arm64下的代码
arm64 也没有显式保存和恢复程序计数器 PC 的值,这和 x86_64 的处理方法是大同小异的,也是通过函数调用堆栈的特性来巧妙解决这一问题。