0%

内核进程切换代码分析

进程调度:__schedule()

内核进程切换的调度模式:sched_mode

调度模式 描述
SM_NONE 0x0 非抢占式调度
SM_PREEMPT 0x1 抢占式调度
SM_RTLOCK_WAIT 0x2 实时系统调度使用

内核进程切换的起点是 __schedule() in kernel/sched/core.c

  1. 获取当前 CPU 运行的当前进程

    • 获取当前 CPU id : cpu = smp_processor_id();
    • 获取当前 CPU 的 runqueue : rq = cpu_rd(cpu);
    • 获取当前 runqueue 的进程 task_struct 指针 : prev = rq->curr;
  2. 关闭当前 CPU 的中断:local_irq_disable();
  3. rcu_note_context_switch(!!sched_mode);
  4. 给当前 runqueue 加锁,防止其他 CPU 数据竞争: rq_lock(rq, &rf);
  5. 更新 runqueue 的时钟: update_rq_clock(rq);
  6. 如果当前调度模式非抢占式且当前进程状态并非 TASK_RUNNING

    • 如果当前进程被挂起,则将其状态置为 TASK_RUNNING
    • 否则: (当前进程未被挂起)

      1. 睡眠当前进程: deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK)
      2. 如果当前进程在等待 IO (prev->in_iowait) ,则 delayacct_blkio_start();
  7. 在当前 runqueue 中选择需要调度到的下一个进程:next = pick_next_task(rq, prev, &rf)
  8. 清除抢占标志位:

    clear_tsk_need_resched(prev);
    clear_preempt_need_resched();
  9. 如果下一个进程和当前进程不同:

    • RCU_INIT_POINTER(rq->curr, next);
    • 负载迁移:

      migrate_disable_switch();
      psi_sched_switch();
    • 追踪记录进程切换事件:trace_sched_switch
    • 进行上下文切换:rq = context_switch(rq, prev, next, &rf);
  10. 如果下一个进程和当前进程相同:释放当前 runqueue 的锁

    rq_unpin_lock(rq, &rf);
    __balance_callbacks(rq);
    raw_spin_rq_unlock_irq(rq);

进程上下文切换:context_switch()

  1. 切换前的准备工作
  2. 切换地址空间
  3. 释放当前 runqueue 的锁:prepare_lock_switch()
  4. 切换 CPU 寄存器和栈指针:switch_to()
  5. barrier()
  6. finish_task_switch()

切换前的准备工作

  1. prepare_task_switch()

    kcov_prepare_switch(prev);
    sched_info_switch(rq, prev, next);
    perf_event_task_sched_out(prev, next);
    rseq_preempt(prev);
    fire_sched_out_preempt_notifiers(prev, next);
    kmap_local_sched_out();
    prepare_task(next);
    prepare_arch_switch(next);
  2. 架构相关的上下文切换初始化的代码:arch_start_context_switch()

    • 对 riscv 而言,未定义该部分代码,实现为空 in include/linux/pgtable.h

切换地址空间(页表)

  1. 如果要切换到内核线程(!next->mm):

    • 使用 prev 进程的 active_mm 作为 next 内核线程的 active_mm
    • 更新 prev 进程的 active_mm
    // not defined in riscv, so empty implementation is used. (in include/asm-generic/mmu_context.h)
    enter_lazy_tlb(prev->active_mm, next);

    next->active_mm = prev->active_mm;
    if (prev->mm) // from user process
    // update MMU reference count if CONFIG_MMU_LAZY_TLB_REFCOUNT is defined. (in include/linux/sched/mm.h)
    mmgrab_lazy_tlb(prev->active_mm);
    else // from kernel process
    prev->active_mm = NULL;
  2. 否则(要切换到用户进程):

    • 切换地址空间(页表)
    • 如果 prev 进程是内核线程,设置其 active_mm
    // memory barrier for switch_mm (in kernel/sched/sched.h)
    membarrier_switch_mm(rq, prev->active_mm, next->mm);
    // it is switch_mm for riscv. (in include/kernel/sched/mmu_context.h)
    switch_mm_irqs_off(prev->active_mm, next->mm, next);
    // write mm->lru_gen.bitmap if CONFIG_LRU_GEN is defined. (in include/linux/mm_types.h)
    lru_gen_use_mm(next->mm);

    if (!prev->mm) { // from kernel
    /* will mmdrop_lazy_tlb() in finish_task_switch(). */
    rq->prev_mm = prev->active_mm;
    prev->active_mm = NULL;
    }
  3. switch_mm_cid() in kernel/sched/sched.h
riscv switch_mm

in arch/riscv/mm/context.c

  1. 如果 prev 进程和 next 进程相同,则返回
  2. 设置当前 CPU 的地址空间:set_mm(prev, next, cpu)
  3. flush 当前 CPU 的 icache: flush_icache_deferred(next, cpu)
set_mm

in arch/riscv/mm/context.c

  1. 设置当前 CPU 使用 next 进程的地址空间的虚拟地址映射:cpumask_set_cpu(cpu, mm_cpumask(next));

    • mm_cpumask 表明哪些 harts 的 TLB 含有当前地址空间的虚拟地址映射
  2. 如果使用了 ASID(use_asid_allocator): set_mm_asid(next, cpu);

    • 为保证性能,ASID 机制不会在每次 switch_mm 之后都 flush TLB
    • 使用 ASID 的时候,必须保证 cpumask 中包含所有相应的 CPU 直到 mm reset
  3. 否则(未使用 ASID):

    • 设置当前 CPU 未使用 prev 进程的地址空间的虚拟地址映射: cpumask_clear_cpu(cpu, mm_cpumask(prev));
    • 写入 SATP CSR 并 flush 当前 CPU 的 TLB: set_mm_noasid(next);
set_mm_asid

in arch/riscv/mm/context.c

  1. 处理并发场景下mm->context.id, current_version, active_context 的比较和设置
  2. 检查 context_tlb_flush_pending 中是否包含当前 CPU: cpumask_test_and_clear_cpu(cpu, &context_tlb_flush_pending)
  3. switch_mm_fast:

    • 写入 SATP CSR
    • 如果需要 flush TLB(need_flush_tlb): local_flush_tlb_all();

切换 CPU 寄存器和栈指针:switch_to()

in arch/riscv/include/asm/switch_to.h

  1. 如果 FPU 使能(has_fpu()), 切换浮点寄存器:
    __switch_to_fpu(__prev, __next);

    • has_fpu(): 是否支持 F 或 D 扩展
  2. 如果 Vector 使能(has_vector()), 切换向量寄存器:
    __switch_to_vector(__prev, __next);

    • has_vector(): 是否支持 V 扩展
  3. 切换通用寄存器:

__switch_to(__prev, __next);

切换浮点寄存器 __switch_to_fpu
  • in arch/riscv/include/asm/switch_to.h
  • in arch/riscv/kernel/fpu.S
  1. 如果 status CSR 中的 SD 位 (FS/VS/XS dirty) 不为 0 ,则需要将当前浮点寄存器保存到上下文: fstate_save(prev, regs): 进一步检查 status CSR 的 FS 域,如果是 Dirty:

    • 将浮点寄存器保存到指定进程的上下文中: __fstate_save(task)
    • 将上下文中的 status.FS 设置为 Clean :__fstate_clean(regs)
  2. next 进程的上下文恢复浮点寄存器:fstate_restore(next, task_pt_regs(next)) : 进一步检查 status.FS 是否是 off, 如果不是:

    • 从指定进程的上下文中恢复浮点寄存器: __fstate_restore(task)
    • 将上下文中的 status.FS 设置为 Clean :__fstate_clean(regs)

__fstate_save: 保存所有浮点寄存器和 fcsr 寄存器

  • 其中 TASK_THREAD_F0thread_info 结构体当中 f0 寄存器的偏移量(in arch/riscv/kernel/asm-offset.c
  1. 获取 prev 进程的上下文中保存 f0 的地址: a0
  2. 设置当前 status.FS 为 Dirty
  3. 将当前所有浮点寄存器保存到 prev 进程的 thread_info 结构体中
  4. 将 fcsr 保存到 prev 进程的 thread_info 结构体中(通过 t0)
  5. 设置当前 status.FS 为 Clean
ENTRY(__fstate_save)
li a2, TASK_THREAD_F0
add a0, a0, a2
li t1, SR_FS
csrs CSR_STATUS, t1
frcsr t0
fsd f0, TASK_THREAD_F0_F0(a0)
fsd f1, TASK_THREAD_F1_F0(a0)
...
fsd f31, TASK_THREAD_F31_F0(a0)
sw t0, TASK_THREAD_FCSR_F0(a0)
csrc CSR_STATUS, t1
ret
ENDPROC(__fstate_save)

__fstate_restore: 恢复所有浮点寄存器和 fcsr 寄存器

  1. 获取 next 进程的上下文中保存 f0 的地址: a0
  2. 设置当前 status.FS 为 Dirty
  3. next 进程的 thread_info 结构体中恢复所有浮点寄存器到当前 CPU
  4. next 进程的 thread_info 结构体中恢复 fcsr (通过 t0)
  5. 设置当前 status.FS 为 Clean
ENTRY(__fstate_restore)
li a2, TASK_THREAD_F0
add a0, a0, a2
li t1, SR_FS
lw t0, TASK_THREAD_FCSR_F0(a0)
csrs CSR_STATUS, t1
fld f0, TASK_THREAD_F0_F0(a0)
fld f1, TASK_THREAD_F1_F0(a0)
...
fld f31, TASK_THREAD_F31_F0(a0)
fscsr t0
csrc CSR_STATUS, t1
ret
ENDPROC(__fstate_restore)
切换向量寄存器 __switch_to_vector

in arch/riscv/include/asm/vector.h

  1. 将向量寄存器保存到 prev 进程上下文中:riscv_v_vstate_save(prev, regs)

    • 将向量寄存器和向量 CSR 保存到指定进程的上下文中: __riscv_v_vstate_save(vstate, vstate->datap)

      • 设置 LMUL = 8 的向量寄存器组,一条指令保存 8 个向量寄存器
      • 保存 V 扩展 CSR :__vstate_csr_save(save_to)
    • 将上下文中的 status.VS 设置为 Clean :__riscv_v_vstate_clean(regs)
  2. next 进程的上下文恢复向量寄存器riscv_v_vstate_restore(next, task_pt_regs(next))

    • 从指定进程的上下文中恢复向量寄存器和向量 CSR :
      __riscv_v_vstate_restore(vstate, vstate->datap)

      • 设置 LMUL = 8 的向量寄存器组,一条指令恢复 8 个向量寄存器
      • 恢复 V 扩展 CSR :__vstate_csr_restore(restore_from)
    • 将上下文中的 status.VS 设置为 Clean :__riscv_v_vstate_clean(regs)
切换通用寄存器 __switch_to

in arch/riscv/kernel/entry.S

只保存和恢复 RISC-V ABI 规定的 callee-saved 寄存器

  • 其中 TASK_THREAD_RA 为 task_struct 结构体当中 ra 寄存器的偏移量(in arch/riscv/kernel/asm-offset.c
  1. 获取 prev 进程和 next 进程的上下文(a0, a1)中保存 ra 的地址: a3, a4
  2. 将当前 callee-saved 寄存器保存到 prev 进程的 thread_info 结构体中
  3. prev 进程的 thread_info 结构体中加载 callee-saved 寄存器到 CPU 中
  4. ret 之后将跳转到 next 进程的 ra 寄存器的地址,至此完成切换。
SYM_FUNC_START(__switch_to)
/* Save context into prev->thread */
li a4, TASK_THREAD_RA
add a3, a0, a4
add a4, a1, a4
REG_S ra, TASK_THREAD_RA_RA(a3)
REG_S sp, TASK_THREAD_SP_RA(a3)
REG_S s0, TASK_THREAD_S0_RA(a3)
REG_S s1, TASK_THREAD_S1_RA(a3)
...
REG_S s11, TASK_THREAD_S11_RA(a3)
/* Restore context from next->thread */
REG_L ra, TASK_THREAD_RA_RA(a4)
REG_L sp, TASK_THREAD_SP_RA(a4)
REG_L s0, TASK_THREAD_S0_RA(a4)
REG_L s1, TASK_THREAD_S1_RA(a4)
...
REG_L s11, TASK_THREAD_S11_RA(a4)
/* The offset of thread_info in task_struct is zero. */
move tp, a1
ret
SYM_FUNC_END(__switch_to)