Linux操作系统-深入理解进程切换

一、进程切换概述

        当一个进程正在运行时触发系统调用或被中断,将进行中断上下文的切换,之后执行ISR中断服务,在中断处理结束后,使用_schedule()函数进行进程切换。

        schedule()函数首先从CPU任务队列中取出当前进程的标识符记为prev进程。然后通过进程调度算法确定下一个要被换上的进程,记为next进程。之后,检查next如果和prev进程不一样,调用context_switch()函数进行上下文切换,next进程进入CPU运行。

        在context_switch()中调用switch_to()进行寄存器和堆栈的切换,switch_to()会调用switch_to_asm()函数,在switch_to_asm()的中进行了从prev内核堆栈到next内核堆栈的切换,在最后不使用ret指令,而是通过jmp指令跳转到switch_to()函数,在switch_to()函数的结尾调用return返回,因为在switch_to_asm()中进行了堆栈的切换,因此_switch_to()返回后,回到的是next进程的内核堆栈,而不是prev进程的内核堆栈。

二、进程切换分析

        主要函数调用链:schedule() –> context_switch() –> switch_to –> __switch_to()。

         schedule函数

        内核级线程可以主动调用,普通用户进程调度的时机发生在在中断服务结束后中断返回前。

        context_switch函数

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);

  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);
    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);
}
  • 在进程切换之前调用prepare_task_switch(),然后内核会执行与体系结构相关的一些调测指令,该函数和 finish_task_switch()成对出现,表示完成上下文的切换。
  • arch_start_context_switch()给各个体系结构专有的开始上下文切换的工作提供入口
  • 完成进程地址空间的切换。这里通过判断一个 task_struct 的 mm 成员是否为空来判断它是一个用户进程还是内核级线程,为内核级线程则调用enter_lazy_tlb()。
  • 调用switch_to()切换寄存器状态和栈,swtich_to 函数会进一步调用 __switch_to_asm()

        __swtich_to_asm函数

进程关键上下文的切换swtich_to

ENTRY(__switch_to_asm) //定义一个名为__switch_to_asm的标号,作为进程切换函数的入口
    // 将寄存器%rbp、%rbx、%r12、%r13、%r14和%r15的值保存到当前进程的内核栈中,以便在函数返回时进行恢复
    pushq    %rbp
    pushq    %rbx
    pushq    %r12
    pushq    %r13
    pushq    %r14
    pushq    %r15
    /* switch stack */
    // 将当前进程所使用的内核栈顶指针保存到旧进程的task_struct结构中的thread.sp字段中,
    // 并将新进程的task_struct结构中的thread.sp字段中保存的值作为新进程的内核栈顶指针。
    movq    %rsp, TASK_threadsp(%rdi)
    movq    TASK_threadsp(%rsi), %rsp
    // 从当前进程的内核栈中弹出之前保存的%rbp、%rbx、%r12、%r13、%r14和%r15寄存器的值,并恢复其原本的值
    popq    %r15
    popq    %r14
    popq    %r13
    popq    %r12
    popq    %rbx
    popq    %rbp
    jmp    __switch_to //跳转到C语言实现的__switch_to函数,完成进程切换操作
END(__switch_to)

        最后一步使用jump,将ip寄存器恢复,在切换过程中正好将当前进程的ip寄存器通过call压栈,并通过ret返回了下一个进程的ip寄存器,完成了ip的保存与修改。 

        ARM环境下的cpu_switch_to函数

ENTRY(cpu_switch_to)
  mov  x10, #THREAD_CPU_CONTEXT  // 寄存器x10存放thread.cpu_context偏移,与进程task_struct地址相加后即可获得该进程的cpu_context
  add  x8, x0, x10               // x0与偏移量相加后存入x8,获取旧进程cpu_context的地址
  mov  x9, sp                    // 将栈顶sp存入x9,以备后续保存

  // 保存x19~x28寄存器的值,每条指令执行完毕后x8的值会自动+16,以便保存后续寄存器值
  stp  x19, x20, [x8], #16
  stp  x21, x22, [x8], #16
  stp  x23, x24, [x8], #16
  stp  x25, x26, [x8], #16
  stp  x27, x28, [x8], #16

  stp  x29, x9, [x8], #16        // 保存x29(栈基址)与x9(栈顶sp)
  str  lr, [x8]                  // 保存寄存器LR,该寄存器存放了cpu_switch_to函数的返回地址

  add  x8, x1, x10               // x1与偏移量相加后存入x8,获取新进程cpu_context的地址

  // 恢复x19~x28寄存器的值
  ldp  x19, x20, [x8], #16
  ldp  x21, x22, [x8], #16
  ldp  x23, x24, [x8], #16
  ldp  x25, x26, [x8], #16
  ldp  x27, x28, [x8], #16

  ldp  x29, x9, [x8], #16        // 恢复x29(栈基址)与x9(栈顶sp)
  ldr  lr, [x8]                  // 恢复寄存器LR,这样函数cpu_switch_to返回后就会从新进程上次被中断的位置处继续执行
  mov  sp, x9                    // 从x9处恢复sp的值
  msr  sp_el0, x1                // 将新进程进程task_struct地址放入sp_el0
  ret
ENDPROC(cpu_switch_to)
NOKPROBE(cpu_switch_to)

       同样是保存当前进程上下文,然后切换后,通过新进程的sp指针进行上下文的恢复。对于PC的保存同样是在此函数之前通过硬件来完成,PC的恢复则通过ret指令来完成。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值