为了能到远方,脚下的每一步都不能少.|

hunterooo

园龄:3年9个月粉丝:0关注:1

Linux内核分析:深入理解进程切换

 

 

我们知道进程切换就是变更进程上下文,而实现上下文切换的函数就是context_switch函数,该函数为kernel/sched/core.c文件中,代码如下:

复制代码
/*
 * 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);

    /*
     * 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);
}
复制代码

 

对其中的一些关键函数进行分析

1.prepare_task_switch(rq, prev, next)

  参数说明:

    •   rq:指向运行队列(runqueue)的指针,表示当前的调度器正在管理的运行队列。
    •   prev:指向前一个任务(previous task)的指针,表示当前正在运行的任务。
    •   next:指向下一个任务(next task)的指针,表示即将要运行的任务。

  该函数的作用就是为下一次任务切换做好准备工作,以确保系统可以顺利地进行任务切换,并保证各个任务能够正确地执行。

 

2.arch_start_context_switch(prev)

  函数arch_start_context_switch(prev)的作用是为任务切换做准备工作,特别是在使用虚拟化技术(paravirt)的情况下,这个函数会和switch_to函数中的一个exit操作一起,将页表重新加载和切换后端结合成一个超级调用(hypercall)。

 

3.进程空间切换

  接下来的一段代码主要是实现在任务切换时,处理当前任务和即将要执行的任务之间的页表切换内存管理的操作。以确保在任务切换时系统能够正确地管理内存和页表,并且保持硬件中断的正确处理。

 

  如果下一个任务(next)没有关联进程地址空间(mm)(即为内核任务),则将当前任务(prev)的活动进程地址空间(active_mm)中的页表信息延迟到TLB(Translation Lookaside Buffer)失效时再处理(enter_lazy_tlb),并将下一个任务(next)的活动进程地址空间(active_mm)设置为当前任务(prev)的活动进程地址空间(prev->active_mm)。如果当前任务(prev)也是进程任务,则增加活动进程地址空间(prev->active_mm)的引用计数(mmgrab),否则将其设置为NULL。

  如果下一个任务(next)有关联的进程地址空间(mm)(即为用户任务),则调用membarrier_switch_mm函数切换内存映射,并确保在此过程中适当的同步,然后调用switch_mm_irqs_off函数切换页表和处理与硬件中断相关的操作。如果当前任务(prev)没有关联的进程地址空间(即为内核任务),则将其活动进程地址空间(prev->active_mm)设置为NULL,并在finish_task_switch函数中减少其引用计数(mmdrop)。

4. rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP)

  这段代码的作用就是清除rq(运行队列)的时钟更新标志。

5. prepare_lock_switch(rq, next, rf);

该函数的作用是为任务切换做一些准备工作,具体来说,这个函数会设置运行队列的标志,以确保在任务切换期间不会出现竞争条件。在进行任务切换时,必须对运行队列上的锁进行获取,以确保在多个CPU上同时进行任务切换时不会出现竞争条件。

6.switch_to(prev, next, prev);

此函数是功能实现的主要函数,包括切换寄存器状态和内核堆栈。该函数会进一步调用__switch_to_asm,对应的汇编代码实现会根据具体的体系结构有不同实现。

X86_64体系结构

复制代码
/*
 * %rdi: prev task
 * %rsi: next task
 */
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)
复制代码

 

代码中有内核堆栈栈顶指针RSP寄存器的切换,有jmp __switch_to,但是没有thread.ip及标号1的位置。__switch_to_asm是在C代码中调用的,也就是使用call指令,而这段汇编的结尾是jmp __switch_to,__switch_to函数是C代码最后有个return,也就是ret指令。 将__switch_to_asm和__switch_to结合起来,正好是call指令和ret指令的配对出现。call指令压栈RIP寄存器到进程切换前的prev进程内核堆栈;而ret指令出栈存入RIP寄存器的是进程切换之后的next进程的内核堆栈栈顶数据。

 

ARM64体系结构

ARM64体系结构下通用代码部分与64X86体系结构完全相同,重点放在swtich_toARM64体系结构下的具体实现代码上

复制代码
/*
 * Thread switching.
 */
__notrace_funcgraph struct task_struct *__switch_to(struct task_struct *prev,
                struct task_struct *next)
{
    struct task_struct *last;

    fpsimd_thread_switch(next);
    tls_thread_switch(next);
    hw_breakpoint_thread_switch(next);
    contextidr_thread_switch(next);
    entry_task_switch(next);
    uao_thread_switch(next);
    ptrauth_thread_switch(next);
    ssbs_thread_switch(next);

    /*
     * Complete any pending TLB or cache maintenance on this CPU in case
     * the thread migrates to a different CPU.
     * This full barrier is also required by the membarrier system
     * call.
     */
    dsb(ish);

    /* the actual thread switch */
    last = cpu_switch_to(prev, next);

    return last;
}




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)
 
复制代码

 

arm64结构下 x86_64 基本差不多,也是通过函数调用堆栈来完成指令指针寄存器RIP的切换。

本文作者:hunterooo

本文链接:https://www.cnblogs.com/hunter-chen/p/17357693.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   hunterooo  阅读(555)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起