Linux的进程切换过程包括如下两大部分:
一、选择哪个进程作为下一个进程;
二、完成进程上下文切换:
1、用户地址空间
2、控制信息
3、进程CPU上下文,寄存器的值
以linux-5.4.34为例分析进程切换:
schedule()函数位于kernel/sched/core.c中,在__schedule()中调用context_switch选择一个新的进程来运行,并进行上下文的切换。context_switch首先调用switch_mm切换CR3,然后调用宏switch_to来进行CPU上下文切换。
asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current;
sched_submit_work(tsk);
do {
preempt_disable();
__schedule(false);
sched_preempt_enable_no_resched();
} while (need_resched());
sched_update_worker(tsk);
}
EXPORT_SYMBOL(schedule);
在__schedule()中,通过next = pick_next_task(rq, prev)找到下一个进程,context_switch(rq, prev, next),进行进程上下文的切换。
其中mm为进程描述符,对于用户进程来说,mm指向虚拟地址空间的用户空间部分,而对于内核线程,mm为NULL。如果切换进来的新进程为内核进程,新进程的active_mm设置为旧进程的用户地址空间,并将旧进程的mm_count+1。
static __always_inline struct rq *context_switch(struct rq *rq,
struct task_struct *prev,
struct task_struct *next,
struct rq_flags *rf)
{
...
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 {
...
/* 这里切换寄存器状态和栈 */
switch_to(prev, next, prev);
...
}
通过一个宏替换执行switch_to,不同的体系结构所定义的宏各不相同,位于arch/{archname}/include/asm/switch_to.h中,x86定义如下:
#define switch_to(prev, next, last) \
do { \
prepare_switch_to(next); \
\
((last) = __switch_to_asm((prev), (next))); \
} while (0)
中间的两句movq表示的即为堆栈切换的过程,之后只需要弹出栈中保存的各个寄存器的值即可恢复寄存器状态。但上面似乎并没有显式地保存、修改指针寄存器 rip 的值,rip的值是如何切换的呢?
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)
这里需要对函数调用堆栈框架的深入理解才能发现端倪,注意__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进程的内核堆栈栈顶数据。
可以继续分析arm下的进程切换,switch_to在arm下定义:
#define switch_to(prev,next,last) \
do { \
__complete_pending_tlbi(); \
last = __switch_to(prev,task_thread_info(prev), task_thread_info(next)); \
} while (0)
task_thread_info返回task->thread_info结构体的地址
static inline struct thread_info *task_thread_info(struct task_struct *task)
{
return &task->thread_info;
}
__switch_to如下:根据ATPCS规则,r0-r3传递参数,r4-r11保存局部变量,r12为ip,r13为sp,r14为lr,r15为pc。
ENTRY(__switch_to)
UNWIND(.fnstart )
UNWIND(.cantunwind )
add ip, r1, #TI_CPU_SAVE
ARM( stmia ip!, {r4 - sl, fp, sp, lr} ) @ Store most regs on stack
THUMB( stmia ip!, {r4 - sl, fp} ) @ Store most regs on stack
THUMB( str sp, [ip], #4 )
THUMB( str lr, [ip], #4 )
ldr r4, [r2, #TI_TP_VALUE]
ldr r5, [r2, #TI_TP_VALUE + 4]
#ifdef CONFIG_CPU_USE_DOMAINS
mrc p15, 0, r6, c3, c0, 0 @ Get domain register
str r6, [r1, #TI_CPU_DOMAIN] @ Save old domain register
ldr r6, [r2, #TI_CPU_DOMAIN]
#endif
switch_tls r1, r4, r5, r3, r7
#if defined(CONFIG_STACKPROTECTOR) && !defined(CONFIG_SMP)
ldr r7, [r2, #TI_TASK]
ldr r8, =__stack_chk_guard
.if (TSK_STACK_CANARY > IMM12_MASK)
add r7, r7, #TSK_STACK_CANARY & ~IMM12_MASK
.endif
ldr r7, [r7, #TSK_STACK_CANARY & IMM12_MASK]
#endif
#ifdef CONFIG_CPU_USE_DOMAINS
mcr p15, 0, r6, c3, c0, 0 @ Set domain register
#endif
mov r5, r0
add r4, r2, #TI_CPU_SAVE
ldr r0, =thread_notify_head
mov r1, #THREAD_NOTIFY_SWITCH
bl atomic_notifier_call_chain
#if defined(CONFIG_STACKPROTECTOR) && !defined(CONFIG_SMP)
str r7, [r8]
#endif
THUMB( mov ip, r4 )
mov r0, r5
ARM( ldmia r4, {r4 - sl, fp, sp, pc} ) @ Load all regs saved previously
THUMB( ldmia ip!, {r4 - sl, fp} ) @ Load all regs saved previously
THUMB( ldr sp, [ip], #4 )
THUMB( ldr pc, [ip] )
UNWIND(.fnend )
ENDPROC(__switch_to)
add ip, r1, #TI_CPU_SAVE 将IP寄存器赋值为r1+ TI_CPU_SAVE,r1即为prev->thread_info,TI_CPU_SAVE是cpu_context成员在thread_info中的偏移。因此IP寄存器保存了prev->thread_info->cpu_context的地址。
ARM( stmia ip!, {r4 - sl, fp, sp, lr}) 将r4 - sl, fp, sp, lr寄存器中的内容保存到IP寄存器所指向的内存地址,即prev->thread_info->cpu_context,这相当于保存了prev进程运行时的寄存器上下文。
ARM( ldmia r4, {r4 - sl, fp, sp, pc}) 将next->thread_info->cpu_context的数据加载到r4 - sl, fp, sp, lr,pc寄存器中,next->thread_info->cpu_context->sp存入寄存器SP相当于内核栈切换完成,next->thread_info->cpu_context->pc存入寄存器PC相当于跳转到next进程运行。即切换到next进程运行时的寄存器上下文。