# 概述

当一个进程正在运行时触发系统调用或被中断,将进行中断上下文的切换,之后执行 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 () 函数进行上下文切换,在 kernel/sched/core.c 中查看函数的定义:

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);
	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);
	switch_to(prev, next, prev);
	barrier();
	return finish_task_switch(prev);
}

content_switch 函数有三个参数:rq、prev、next,其中 rq 指向本次进程切换发生的 running queue (进程就绪队列);prev 和 next 分别指向切换前后进程的进程描述符。实现的功能如下:

  • 在进程切换之前调用 prepare_task_switch (),然后内核会执行与体系结构相关的一些调测指令,该函数和 finish_task_switch () 成对出现,表示完成上下文的切换。
  • arch_start_context_switch () 给各个体系结构专有的开始上下文切换的工作提供入口
  • 完成进程地址空间的切换。这里通过判断一个 task_struct 的 mm 成员是否为空来判断它是一个用户进程还是内核级线程,为内核级线程则调用 enter_lazy_tlb ()。
  • 调用 switch_to () 切换寄存器状态和栈,swtich_to 函数会进一步调用 __switch_to_asm ()

__switch_to_asm () 实现是与体系结构相关的,这里我们先以 X86_64 为例进行分析:

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)

两条 movq 语句就是新旧进程的分界线,随着内核栈顶的切换,内核栈空间也就切换到了新进程,之后只需要弹出栈中保存的各个寄存器的值即可恢复寄存器状态。将__switch_to_asm 和 switch_to 结合起来,发现是 call 指令和 ret 指令的配对出现。call 指令压栈 RIP 寄存器到进程切换前的 prev 进程内核堆栈,而 ret 指令出栈存入 RIP 寄存器的是进程切换之后的 next 进程的内核堆栈栈顶数据,所以 ret 恢复的就是 next 进程内核堆栈中的 rip 值,即实现了 rip 值的保存与修改。

再看一下 arm64 架构下__switch_to_asm () 的实现 ,其过程就是保存和恢复 cpu_context 结构体,在 arm 中其函数和宏调用过程:switch_to -> switch_to -> cpu_switch_to,具体的切换发生在 cpu_switch_to 中其代码如下:

ENTRY(cpu_switch_to) 			
	mov	x10, #THREAD_CPU_CONTEXT 	// 寄存器 x10 存放 thread.cpu_context 偏移
	add	x8, x0, x10 				//x0 与偏移量相加后存入 x8,获取旧进程 cpu_context 的地址
	mov	x9, sp 						// 将栈顶指针 sp 保存在 x9 寄存器
	stp	x19, x20, [x8], #16			// 将寄存器 x19~x29 保存,保存现场
	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 是 frame pointer,x9 是 stack pointer,lr 是 pc 值
	str	lr, [x8]
	
	add	x8, x1, x10 				// 获取访问 next 进程的 cpu_context 的指针
	ldp	x19, x20, [x8], #16			// 恢复 next 进程的现场
	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
	ldr	lr, [x8]
	mov	sp, x9	
	msr	sp_el0, x1
	
	ret			 					// 此时 ret 将 next 进程的 lr 寄存器的值加载到 PC,进程切换完毕
ENDPROC(cpu_switch_to)
NOKPROBE(cpu_switch_to)