中科大软院linux | 进程切换的工作机制

进程切换的工作机制

进程切换:为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复执行以前挂起的某个进程。即从正在运行的进程中收回处理器,然后再使待运行进程来占用处理器。

进程上下文

  • 用户地址空间:程序代码、数据、用户堆栈等

  • 控制信息:进程描述符,内核堆栈等

  • 进程的CPU上下文,相关寄存器的值

进程执行环境切换步骤

  • 从就绪队列中选择一个进程

  • 完成进程上下文切换

进程切换核心代码分析

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

进程执行环境的切换

进程执行环境的切换大致分为两大步,一是从就绪队列中选择一个进程(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

首先调用 prepare_task_switch() 来准备进程切换。然后调用 arch_start_context_switch() 函数来开始进程上下文的切换。

关于内存管理的操作,会根据进程的类型(内核空间进程或用户空间进程)进行不同的处理。如果切换到内核空间进程,则会进入“懒 TLB”,并且直接使用前一个进程的地址空间。如果切换到用户空间进程,则需要切换地址空间,并调用 membarrier_switch_mm() 函数和 switch_mm_irqs_off() 函数进行一些额外的操作。

之后,函数调用 prepare_lock_switch() 函数准备锁的切换。

最后,函数调用 switch_to() 函数来进行寄存器状态和栈的切换,并返回 finish_task_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);  
...
}

ARM64架构中的switch_to函数分析

我们重点关注switch_to函数的工作流程,switch_to函数完成了线程的硬件上下文切换。当完成了硬件上下文切换后。通过还原栈顶的寄存器后,保存在栈中的局部变量,因为栈的切换变为当前的运行线程的的栈。当线程栈改变后对应的局部变量地址也改变为当前线程原来的局部变量。所以当最后栈顶变为需切换的线程的栈顶时。代码运行的路径也变为新的线程代码路径和数据。switch_to函数在arm64架构下的定义如下:

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类似,就不多做分析了。

实验总结

通过上述分析,我们已经得到了在arm64架构下进程切换过程中发生的主要操作:

__schedule   // kernel/sched/core.c
->context_switch
  ->switch_mm_irqs_off   //进程地址空间切换
  ->switch_to //处理器状态切换

通过这一过程,Linux内核便完成了进程的切换。实际上在不同的体系结构下,只有switch_to函数是有较大差异的,因为不同的体系结构下的寄存器和堆栈有所不同,涉及具体的操作自然也是不同的,其他的函数基本上是通用的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_之桐_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值