一、进程切换的基本概念
1.进程切换
操作系统为了控制进程的执行,必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行,这种行为被称为进程切换,任务切换或上下文切换。
让进程来占用处理器,实质上是把某个进程存放在私有堆栈中寄存器的数据(前一次本进程被中止时的中间数据)再恢复到处理器的寄存器中去,并把待运行进程的断点送入处理器的程序指针PC,于是待运行进程就开始被处理器运行了,也就是这个进程已经占有处理器的使用权了。
在切换时,一个进程存储在处理器各寄存器中的中间数据叫做进程的上下文,所以进程的切换实质上就是被中止运行进程与待运行进程上下文的切换。在进程未占用处理器时,进程的上下文是存储在进程的私有堆栈中的。
2.硬件上下文
每个进程都有自己的地址空间,但是所有的进程却共享CPU寄存器。所以,在恢复进程执行之前,内核必须保证该进程在挂起时的寄存器值重新加载到CPU的寄存器中。
这些需要加载到CPU寄存器中的值就成为硬件上下文。硬件上下文是进程执行上下文的一个子集,进程执行上下文包含进程执行所需要的所有信息。在Linux中,进程的硬件上下文一部分存储在进程描述符中,而其它部分存储在内核态的栈中。
3.执行进程切换
进程切换的时机:
·中断处理程序中直接调用schedule()函数,实现进程调度。
·内核线程,是一个特殊的进程,只有内核态没有用户态。所以即可以主动调用schedule()函数进行调度,也可以被中断处理程序调用。
·内核态进程没法直接主动调度,因为schedule()是一个内核函数,不是系统调用。所以只能在中断处理程序进行调度。
关键代码梳理
·首先,schedule()函数会调用next = pick_next_task(rq, prev);,所做的工作就是根据调度算法策略,选取要执行的下一个进程。
·其次,根据调度策略得到要执行的进程后,调用context_switch(rq, prev, next);,完成进程上下文切换。其中,最关键的switch_to(prev,next, prev);切换堆栈和寄存器的状态。
二、进程切换代码分析
在 kernel/sched/core.c 中找到context_switch, rq代表当前CPU的running 队列,prev,next分别指向进程上下文组成的双向链表的进程描述符。
/*
* 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);
lru_gen_use_mm(next->mm);
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(),其用来准备切换任务,调用一些体系结构特有的hook。
接着,调用 arch_start_context_switch() 函数来开始进程上下文的切换,该函数给各个体系结构专有的开始上下文切换的工作提供了入口,不同体系结构的实现不同。
然后进行具体的地址空间的切换:
·prev 是进程切换前执行的进程
·next 是进程切换后要执行的进程
·next->mm 指向要执行进程的地址空间描述符
·prev->mm 指向 切换前进程的当前正在使用的地址空间描述符
对于用户进程来说,其进程描述符(task_struct)的 mm 和 active_mm 相同,都是指向其进程地址空间
对于内核线程而言,其 task_struct 的 mm 成员为 NULL,内核线程没有自己的进程地址空间,但又需要使用一块空间作为进程空间, active_mm 即为指向它借用的那个进程地址空间。
于是,我们可以通过判断一个 task_struct 的 mm 成员是否为空来判断它是一个用户进程还是内核级线程。
1.如果切换后进程是内核线程
通过代码可知,如果要切换的进程是内核级进程,next->active_mm = prev->active_mm 直接把切换前进程的mm传递给此内核级进程,即内核级线程借用用户进程地址空间运行。
然后,对于切换前进程而言:
if (prev->mm) // from user
mmgrab(prev->active_mm);
如果 切换前进程是用户进程(prev->mm 不为空),则需要将这个被借用的 mm_struct 的引用计数增加一,如果该 mm_struct 对应的用户进程已经退出,则系统需要等到其引用计数为 0。
static inline void mmgrab(struct mm_struct *mm)
{
atomic_inc(&mm->mm_count);
}
如果 切换前进程是内核级线程,则把切换前进程的 active_mm 成员清空,结束其对于该 mm_struct 的借用(这只是把借用它的内核线程从一个转换到了另一个,故引用计数无需增加)。
else
prev->active_mm = NULL;
2.如果切换后进程是用户进程
首先 membarrier_switch_mm(rq, prev->active_mm, next->mm) 使用了一个内存屏障,来保证上一个进程访问其内存空间与下一个进程访问其内存空间之间的先后顺序(某种程度上是一种进程同步机制),避免在访存进行过程中发生 mm_struct 的切换导致的访存错误。
然后进入switch_mm_irqs_off(prev->active_mm, next->mm, next),切换mm_struct
最后,如果 切换前进程是内核线程,则需要设置 rq->prev_mm 用于后续清除引用计数,并执行 prev->active_mm = NULL 解除对 active_mm 的借用。
3.switch_to()函数
此函数完成进程硬件上下文的切换,switch_to调用了 __switch_to_asm
((last) = __switch_to_asm((prev), (next)));
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
此段代码完成了切换前进程到切换后进程堆栈的转变。
4.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)
1 mov x10, #THREAD_CPU_CONTEXT // 寄存器x10存放thread.cpu_context偏移
2 add x8, x0, x10
3 mov x9, sp
4 stp x19, x20, [x8], #16
6 stp x21, x22, [x8], #16
7 stp x23, x24, [x8], #16
8 stp x25, x26, [x8], #16
9 stp x27, x28, [x8], #16
10 stp x29, x9, [x8], #16
11 str lr, [x8]
12 add x8, x1, x10 // 获取访问next进程的cpu_context的指针
13 ldp x19, x20, [x8], #16 // 恢复next进程的现场
14 ldp x21, x22, [x8], #16
15 ldp x23, x24, [x8], #16
16 ldp x25, x26, [x8], #16
17 ldp x27, x28, [x8], #16
18 ldp x29, x9, [x8], #16
19 ldr lr, [x8]
20 mov sp, x9
21 msr sp_el0, x1
22 ret
ENDPROC(cpu_switch_to)
NOKPROBE(cpu_switch_to)