深入理解进程切换

        进程执行环境的切换大致分为两大步,一是从就绪队列中选择一个进程(pick_next_task),也就是由进程调度算法决定选择哪一个进程作为下一个进程(next);二是完成进程上下文切换context_switch,进程上下文包含了进程执行需要的所有信息。

static void __sched notrace __schedule(bool preempt)
{
    struct task_struct *prev, *next;
    ...
next = pick_next_task(rq, prev, &rf);
    ...
    rq = context_switch(rq, prev, next, &rf);
    ...
}

 其中context_switch的代码为:

/*
 * 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);
    ...
    /*
     * kernel -> kernel   lazy + transfer active
     *   user -> kernel   lazy + mmgrab() active
     *
     * kernel ->   user   switch + mmdrop() active
     *   user ->   user   switch
     */
    if (!next->mm) {                                // to kernel
    	...
    } else {                                        // to user
        ...
    }
    /* Here we just switch the register state and the stack. */
    switch_to(prev, next, prev);
    barrier();

return finish_task_switch(prev);
}

最核心的是几个关键寄存器的保存与变换。
    •    进程页目录表(页表),即地址空间、数据。
    •    内核堆栈栈顶寄存器sp代表进程内核堆栈(保存函数调用历史),进程描述符(最后的成员thread是关键)和内核堆栈存储于连续存取区域中,进程描述符存在内核堆栈的低地址,栈从高地址向低地址增长,因此通过栈顶指针寄存器还可以获取进程描述符的起始地址。
    •    指令指针寄存器代表进程的CPU上下文,即要执行的下条指令地址。
这些寄存器从一个进程的状态切换到另一个进程的状态,进程切换的关键上下文就算完成了。

schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换。context_switch首先调用switch_mm切换CR3,然后调用宏switch_to来进行CPU上下文切换。

部分关键代码为:

static inline void  
context_switch(struct rq *rq, struct task_struct *prev,  
       struct task_struct *next)  
{  
... 
    if (unlikely(!mm)) { /* 如果被切换进来的进程的mm为空切换,内核线程mm为空 */  
        next->active_mm = oldmm; /* 将共享切换出去进程的active_mm */  
        atomic_inc(&oldmm->mm_count); /* 有一个进程共享,所有引用计数加一 */  
        /* 将per cpu变量cpu_tlbstate状态设为LAZY */  
        enter_lazy_tlb(oldmm, next);  
    } else  /* 普通mm不为空,则调用switch_mm切换地址空间 */  
        switch_mm(oldmm, mm, next);  
...
    /* 这里切换寄存器状态和栈 */  
    switch_to(prev, next, prev);  
...
}

在arach/x86/include/asm/switch_to.h中

找到 

 

在arch/x86/entry/entry_64.S中找到switch_to_asm:这段代码有内核堆栈栈顶指针RSP寄存器的切换,有jmp   __switch_to

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)

注意__switch_to_asm是在C代码中调用的,也就是使用call指令,而这段汇编的结尾是jmp __switch_to,__switch_to函数是C代码最后有个return,也就是ret指令。
将__switch_to_asm和__switch_to结合起来,正好是call指令和ret指令的配对出现。

中断上下文和进程上下文的一个关键区别是堆栈切换的方法。中断是由CPU实现的,所以中断上下文切换过程中最关键的栈顶寄存器sp和指令指针寄存器ip是由CPU协助完成的;进程切换是由内核实现的,所以进程上下文切换过程中最关键的栈顶寄存器sp切换是通过进程描述符的thread.sp实现的,指令指针寄存器ip的切换是在内核堆栈切换的基础上巧妙利用call/ret指令实现的。

ARM64体系结构下__switch_to的实现见arch/arm64/kernel/process.c,其中cpu_switch_to是我们特别关心的进程的CPU上下文切换的关键代码。情况和x86类似,就不多做分析了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值