一、进程切换的工作机制
进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复执行以前挂起的某个进程。即从正在运行的进程中收回处理器,然后再使待运行进程来占用处理器。
进程上下文
- 用户地址空间:程序代码、数据、用户堆栈等
- 控制信息:进程描述符,内核堆栈等
- 进程的CPU上下文,相关寄存器的值
进程执行环境切换步骤
- 从就绪队列中选择一个进程
- 完成进程上下文切换
进程切换核心代码分析
schedule()
函数选择一个新的进程来运行,并调用context_switch
进行上下文的切换context_switch
调用switch_mm
切换CR3- 调用宏
switch_to
来进行CPU上下文切换。
二、进程切换核心代码分析
1. context_switch函数
代码如下:
/*
* 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);
...
/*
* kernel -> kernel lazy + transfer active
* user -> kernel lazy + mmgrab() active
*
* kernel -> user switch + mmdrop() active
* user -> user switch
*/
if (!next->mm) { // to kernel
...
} else { // to user
...
}
...
if (unlikely(!mm)) { /* 如果被切换进来的进程的mm为空切换,内核线程mm为空 */
next->active_mm = oldmm; /* 将共享切换出去进程的active_mm */
atomic_inc(&oldmm->mm_count); /* 有一个进程共享,所有引用计数加一 */
/* 将per cpu变量cpu_tlbstate状态设为LAZY */
enter_lazy_tlb(oldmm, next);
} else /* 普通mm不为空,则调用switch_mm切换地址空间 */
switch_mm(oldmm, mm, next);
...
/* 这里切换寄存器状态和栈 */
switch_to(prev, next, prev);
...
return finish_task_switch(prev);
}
进程关键上下文的切换switch_to
context_switch
中的一个重要函数是switch_to
,switch_to
调用了 __switch_to_asm
该函数是处理操作系统中进程切换的汇编代码,可以将当前正在运行的进程切换为被调度的下一个进程。
ENTRY(__switch_to_asm)
pushq %rbp
pushq %rbx
pushq %r12
pushq %r13
pushq %r14
pushq %r15
/* switch stack */
movq %rsp, TASK_threadsp(%rdi)
movq TASK_threadsp(%rsi), %rsp
popq %r15
popq %r14
popq %r13
popq %r12
popq %rbx
popq %rbp
jmp __switch_to
END(__switch_to)
汇编代码解析:
首先定义一个名为__switch_to_asm的标号,作为进程切换函数的入口;将寄存器%rbp、%rbx、%r12、%r13、%r14和%r15的值保存到当前进程的内核栈中,以便在函数返回时进行恢复;将当前进程所使用的内核栈顶指针保存到旧进程的task_struct结构中的thread.sp字段中,并将新进程的task_struct结构中的thread.sp字段中保存的值作为新进程的内核栈顶指针;从当前进程的内核栈中弹出之前保存的%rbp、%rbx、%r12、%r13、%r14和%r15寄存器的值,并恢复其原本的值。
linux 中进程切换涉及到一个调用链:
schedule() –> context_switch() –> switch_to –> __switch_to()
context_switch
函数分析:
- 调用
prepare_task_switch()
来准备进程切换。然后调用arch_start_context_switch()
函数来开始进程上下文的切换。
关于内存管理的操作,会根据进程的类型(内核空间进程或用户空间进程)进行不同的处理。如果切换到内核空间进程,则会进入“懒 TLB”,并且直接使用前一个进程的地址空间。如果切换到用户空间进程,则需要切换地址空间,并调用 membarrier_switch_mm() 函数和 switch_mm_irqs_off() 函数进行一些额外的操作。
-
函数调用
prepare_lock_switch()
函数准备锁的切换。 -
最后,函数调用
switch_to()
函数来进行寄存器状态和栈的切换,并返回finish_task_switch()
函数来完成进程切换。
2. ARM64下进程切换核心代码
代码如下:
/*
* Thread switching.
*/
__notrace_funcgraph struct task_struct *__switch_to(struct task_struct *prev,
struct task_struct *next)
{
struct task_struct *last;
...
/* the actual thread switch */
last = cpu_switch_to(prev, next);
return last;
}
/*
* Register switch for AArch64. The callee-saved registers need to be saved
* and restored. On entry:
* x0 = previous task_struct (must be preserved across the switch)
* x1 = next task_struct
* Previous and next are guaranteed not to be the same.
*
*/
ENTRY(cpu_switch_to)
mov x10, #THREAD_CPU_CONTEXT // 寄存器x10存放thread.cpu_context偏移
add x8, x0, x10
mov x9, sp
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
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
ENDPROC(cpu_switch_to)
NOKPROBE(cpu_switch_to)