实验要求:深入理解进程切换
- 参考《庖丁解牛Linux操作系统分析》相关内容,跟踪分析进程切换的过程完成实验并写一篇实验报告,总结进程切换的工作机制以及sp和ip在不同体系结构下汇编代码的切换方法,深入理解进程切换。具体要求如下:
实验原理
进程切换:简述用户态进程X切换到用户态进程Y的过程
- 正在运行的用户态进程X。
- 发生中断(包括异常、系统调用等),CPU完成
load cs:rip(entry of a specific ISR)
即跳转到中断处理程序入口。 - 中断上下文切换,具体包括如下几点:
•swapgs
指令保存现场,可以理解CPU通过swapgs指令给当前CPU寄存器状态做了一个快照。
•rsp point to kernel stack
,加载当前进程内核堆栈栈顶地址到RSP寄存器。
•save cs:rip/ss:rsp/rflags
:将当前CPU关键上下文压入进程X的内核堆栈,快速系统调用是由系统调用入口处的汇编代码实现的。
此时完成了中断上下文切换,即从进程X的用户态到进程X的内核态。 - 中断处理过程中或中断返回前调用了
schedule()
,完成进程调度算法选择next进程、进程地址空间切换、switch_to
关键的进程上下文切换等。 switch_to
调用了__switch_to_asm
做关键的进程上下文切换。将当前进程X的内核堆栈切换到进程调度算法选出来的next进程(进程Y)的内核堆栈,并完成eip的状态切换,之后运行Y。- 中断上下文恢复,进程Y进行中断的上下文恢复。
- 中断上下文恢复最后一步
iret - pop cs:rip/ss:rsp/rflags
,从Y的内核堆栈中弹出(3)中对应的压栈内容,完成中断上下文的切换,即从Y的内核态返回到Y的用户态。 - 继续运行用户态进程Y。
详细分析
在linux中的上下文切换以content_switch
为核心,位于kernel/sched/core.c
中,分析并深入理解进程切换
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);
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);
}
进程切换将CPU的控制权从一个进程转移到另一个进程。在多任务操作系统中,多个进程轮流执行,通过进程切换来实现任务的切换和调度。
1. prepare_task_switch(rq, prev, next)
准备进程切换,传递了相关参数。rq 指向running queue
(就绪队列);prev
和 next
分别指向切换前后进程的进程描述符
2. 根据下一个进程的mm字段是否为空来判断进程切换的类型。如果为空,表示将要切换到内核空间;否则,表示将要切换到用户空间。
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);
if (!prev->mm) { // from kernel
/* will mmdrop() in finish_task_switch(). */
rq->prev_mm = prev->active_mm;
prev->active_mm = NULL;
}
}
- 切换到内核空间的处理:
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;
从用户空间切换到内核空间时的处理。先调用enter_lazy_tlb()
来处理TLB(转换后备缓冲)的延迟更新,再将下一个进程的active_mm
设置为当前进程的active_mm
。如果当前进程是从用户空间切换过来(即prev->mm
不为空),则调用mmgrab()
对active_mm
进行引用计数增加操作。最后,将当前进程的active_mm
设置为NULL
。
- 切换到用户空间的处理:
membarrier_switch_mm(rq, prev->active_mm, next->mm);
switch_mm_irqs_off(prev->active_mm, next->mm, next);
if (!prev->mm) { // from kernel
rq->prev_mm = prev->active_mm;
prev->active_mm = NULL;
}
从内核空间切换到用户空间时,先调用membarrier_switch_mm()
来处理内存屏障相关的操作,再调用switch_mm_irqs_off()
来切换页表,并关闭中断。如果当前进程是从内核空间切换过来的(即prev->mm
为空),则将当前进程的active_mm
保存到rq->prev_mm
中,并将prev->active_mm
设置为NULL
。
3. prepare_lock_switch(rq, next, rf)
准备锁的切换,传递了相关参数。
4. switch_to(prev, next, prev)
实际的上下文切换函数,将当前进程切换到下一个进程。它保存当前进程的寄存器状态和堆栈,加载下一个进程的寄存器状态和堆栈。
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)
5.barrier()
内存屏障,用于确保在继续执行之前,前面的所有内存操作都已完成。
6. return finish_task_switch(prev)
用于完成进程切换,传递了当前进程的参数,会执行一些与进程切换相关的清理工作,并返回之前的进程。
实现进程切换的过程,根据下一个进程的mm字段是否为空,判断切换的类型(内核空间或用户空间)进行相应的处理,包括TLB更新、页表切换、中断关闭等。最后调用实际的上下文切换函数将控制权从当前进程转移到下一个进程,并完成一些清理工作。