通过分析context_switch函数了解进程切换过程
进程切换就是变更进程上下文
最核心的是几个关键寄存器的保存和变换。进程页目录表,内核堆栈栈顶寄存器sp,指令指针寄存器。
1、context_switch函数定义如下,
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next, struct rq_flags *rf)
{
struct mm_struct *mm, *oldmm;
prepare_task_switch(rq, prev, next);
mm = next->mm;
oldmm = prev->active_mm;
/*
* 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);
/*
* If mm is non-NULL, we pass through switch_mm(). If mm is
* NULL, we will pass through mmdrop() in finish_task_switch().
* Both of these contain the full memory barrier required by
* membarrier after storing to rq->curr, before returning to
* user-space.
*/
if (!mm) {
next->active_mm = oldmm;
mmgrab(oldmm);
enter_lazy_tlb(oldmm, next);
} else
switch_mm_irqs_off(oldmm, mm, next);
if (!prev->mm) {
prev->active_mm = NULL;
rq->prev_mm = oldmm;
}
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);
}
(1)每一个进程都有一个内存描述符mm_struct,用来描述进程的虚拟地址空间。如果切换进程的mm为空,即该进程为内核线程,将active_mm设置为原来进程的mm。如果切换进程的mm不为空,则调用switch_mm切换地址空间。
(2)进行进程切换时,首先要通过int 0x80触发系统调用,这时候会调用schedule()函数切换到内核线程,内核线程再次调用schedule()函数从原来的进程切换到新的进程。因此需要一次中断上下文切换和一次进程上下文切换。
(3)调用switch_to函数切换寄存器状态和栈。switch_to函数是进程上下文切换的关键函数,其中__switch_to_asm是一段汇编代码,如下
#define switch_to(prev, next, last) \
do { \
prepare_switch_to(next); \
\
((last) = __switch_to_asm((prev), (next))); \
} while (0)
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
#ifdef CONFIG_STACKPROTECTOR
movq TASK_stack_canary(%rsi), %rbx
movq %rbx, PER_CPU_VAR(fixed_percpu_data) + stack_canary_offset
#endif
#ifdef CONFIG_RETPOLINE
/*
* When switching from a shallower to a deeper call stack
* the RSB may either underflow or use entries populated
* with userspace addresses. On CPUs where those concerns
* exist, overwrite the RSB with entries which capture
* speculative execution to prevent attack.
*/
FILL_RETURN_BUFFER %r12, RSB_CLEAR_LOOPS, X86_FEATURE_RSB_CTXSW
#endif
/* restore callee-saved registers */
popq %r15
popq %r14
popq %r13
popq %r12
popq %rbx
popq %rbp
jmp __switch_to
END(__switch_to_asm)
(4)__switch_to_asm将prev进程的寄存器状态压栈,其中RIP寄存器是在调用__switch_to_asm时(即使用了call指令)压栈的,__switch_to将寄存器状态转化为next进程的状态,并通过return(即ret指令),将next原来栈顶地址sp存入RIP寄存器。
参考
[1] https://zhuanlan.zhihu.com/p/604541899
[2] https://blog.csdn.net/qq_26768741/article/details/54375524
[3] https://louyu.cc/articles/linux-sever/2023/04/?p=3150