本文共 3775 字,大约阅读时间需要 12 分钟。
以linux 4.16.3为例
A->B->C
A进程切换到B进程,B进程切换到C进程。 在A进程的栈看来, prev为A, next为B 在B进程的栈看来, prev为B, next为Cstatic __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; mm = next->mm; oldmm = prev->active_mm; if (!mm) { next->active_mm = oldmm; mmgrab(oldmm); enter_lazy_tlb(oldmm, next); } else switch_mm_irqs_off(oldmm, mm, next); =>void switch_mm_irqs_off(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk) { this_cpu_write(cpu_tlbstate.is_lazy, false); load_new_mm_cr3(next->pgd, new_asid, true); // 重新加载页表 if (next != &init_mm) this_cpu_write(cpu_tlbstate.last_ctx_id, next->context.ctx_id); this_cpu_write(cpu_tlbstate.loaded_mm, next); this_cpu_write(cpu_tlbstate.loaded_mm_asid, new_asid); load_mm_cr4(next); switch_ldt(real_prev, next); } switch_to(prev, next, prev); =>#define switch_to(prev, next, last) \ do { \ prepare_switch_to(next); \ \ ((last) = __switch_to_asm((prev), (next))); \ // 这是个函数调用, 会把下一条指令的地址也就是rip压到A的堆栈中, 也就是 barrier();对应的第一条指令的地址 } 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 // 切换rsp指针, 以后push和pop就在B进程的内核栈进行 #ifdef CONFIG_CC_STACKPROTECTOR movq TASK_stack_canary(%rsi), %rbx movq %rbx, PER_CPU_VAR(irq_stack_union)+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 // 切换rbp指针, 后续的局部变量都是B的 jmp __switch_to // jmp不压栈, 所以_switch_to 不会返回到jmp的下一条指令 =>__visible __notrace_funcgraph struct task_struct * __switch_to(struct task_struct *prev_p, struct task_struct *next_p) // 两个入参分别是rdi rsi, 通过反汇编可以看出 { struct thread_struct *prev = &prev_p->thread; // 由于是寄存器传递参数, 所以prev_p还是指向A, 但是prev和next已经是在B的内核栈空间创建 struct thread_struct *next = &next_p->thread; // next_p还是指向B struct tss_struct *tss = &per_cpu(cpu_tss_rw, cpu); save_fsgs(prev_p); load_TLS(next, cpu); savesegment(es, prev->es); savesegment(ds, prev->ds); load_seg_legacy(prev->fsindex, prev->fsbase, next->fsindex, next->fsbase, FS); load_seg_legacy(prev->gsindex, prev->gsbase, next->gsindex, next->gsbase, GS); switch_fpu_finish(next_fpu, cpu); /* * Switch the PDA and FPU contexts. */ this_cpu_write(current_task, next_p); this_cpu_write(cpu_current_top_of_stack, task_top_of_stack(next_p)); /* Reload sp0. */ update_sp0(next_p); return prev_p; // A只有rdi指向, 所以很容易丢失, 通过一个返回值返回用于别的用途, 返回到了 switch_to(prev, next, prev);的下一条语句, 即barrier(); } END(__switch_to_asm) barrier(); return finish_task_switch(prev);}
总之, 关键记住三点:
第一: rsp决定push和pop; rbp决定局部变量. 当rsp和rbp切换之后, A进程就变成了B进程; 第二: call会压下一条指令的地址到本进程的内核栈中, 而jmp不会, 所以用 jmp __switch_to 而不用 call __switch_to; 第三: 当进程上下文切换, 上一个进程的信息只能通过寄存器传递关于Linux进程切换switch_to宏的一个细节(认识内联汇编)
https://blog.csdn.net/qq_42763389/article/details/89028749switch_to()宏的理解
http://blog.chinaunix.net/uid-130624-id-2907709.html【内核】进程切换 switch_to 与 __switch_to (好文)
https://cnblogs.com/visayafan/archive/2011/12/10/2283660.html