内核进程切换代码分析
进程调度:__schedule()
内核进程切换的调度模式:sched_mode
| 调度模式 | 值 | 描述 |
SM_NONE |
0x0 |
非抢占式调度 |
SM_PREEMPT |
0x1 |
抢占式调度 |
SM_RTLOCK_WAIT |
0x2 |
实时系统调度使用 |
内核进程切换的起点是 __schedule() in kernel/sched/core.c
-
获取当前 CPU 运行的当前进程
- 获取当前 CPU id :
cpu = smp_processor_id(); - 获取当前 CPU 的 runqueue :
rq = cpu_rd(cpu); - 获取当前 runqueue 的进程
task_struct指针 :prev = rq->curr;
- 获取当前 CPU id :
- 关闭当前 CPU 的中断:
local_irq_disable(); rcu_note_context_switch(!!sched_mode);- 给当前 runqueue 加锁,防止其他 CPU 数据竞争:
rq_lock(rq, &rf); - 更新 runqueue 的时钟:
update_rq_clock(rq); -
如果当前调度模式非抢占式且当前进程状态并非
TASK_RUNNING- 如果当前进程被挂起,则将其状态置为
TASK_RUNNING -
否则: (当前进程未被挂起)
- 睡眠当前进程:
deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK) - 如果当前进程在等待 IO (
prev->in_iowait) ,则delayacct_blkio_start();
- 睡眠当前进程:
- 如果当前进程被挂起,则将其状态置为
- 在当前 runqueue 中选择需要调度到的下一个进程:
next = pick_next_task(rq, prev, &rf) -
清除抢占标志位:
clear_tsk_need_resched(prev);
clear_preempt_need_resched(); -
如果下一个进程和当前进程不同:
RCU_INIT_POINTER(rq->curr, next);-
负载迁移:
migrate_disable_switch();
psi_sched_switch(); - 追踪记录进程切换事件:
trace_sched_switch - 进行上下文切换:
rq = context_switch(rq, prev, next, &rf);
-
如果下一个进程和当前进程相同:释放当前 runqueue 的锁
rq_unpin_lock(rq, &rf);
__balance_callbacks(rq);
raw_spin_rq_unlock_irq(rq);
进程上下文切换:context_switch()
- 切换前的准备工作
- 切换地址空间
- 释放当前 runqueue 的锁:
prepare_lock_switch() - 切换 CPU 寄存器和栈指针:
switch_to() barrier()finish_task_switch()
切换前的准备工作
-
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); -
架构相关的上下文切换初始化的代码:
arch_start_context_switch()- 对 riscv 而言,未定义该部分代码,实现为空 in
include/linux/pgtable.h
- 对 riscv 而言,未定义该部分代码,实现为空 in
切换地址空间(页表)
-
如果要切换到内核线程(
!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; - 使用
-
否则(要切换到用户进程):
- 切换地址空间(页表)
- 如果
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;
} switch_mm_cid()inkernel/sched/sched.h
riscv switch_mm
in arch/riscv/mm/context.c
- 如果
prev进程和next进程相同,则返回 - 设置当前 CPU 的地址空间:
set_mm(prev, next, cpu) - flush 当前 CPU 的 icache:
flush_icache_deferred(next, cpu)
set_mm
in arch/riscv/mm/context.c
-
设置当前 CPU 使用
next进程的地址空间的虚拟地址映射:cpumask_set_cpu(cpu, mm_cpumask(next));mm_cpumask表明哪些 harts 的 TLB 含有当前地址空间的虚拟地址映射
-
如果使用了 ASID(
use_asid_allocator):set_mm_asid(next, cpu);- 为保证性能,ASID 机制不会在每次
switch_mm之后都 flush TLB - 使用 ASID 的时候,必须保证 cpumask 中包含所有相应的 CPU 直到 mm reset
- 为保证性能,ASID 机制不会在每次
-
否则(未使用 ASID):
- 设置当前 CPU 未使用
prev进程的地址空间的虚拟地址映射:cpumask_clear_cpu(cpu, mm_cpumask(prev)); - 写入 SATP CSR 并 flush 当前 CPU 的 TLB:
set_mm_noasid(next);
- 设置当前 CPU 未使用
set_mm_asid
in arch/riscv/mm/context.c
- 处理并发场景下
mm->context.id,current_version,active_context的比较和设置 - 检查
context_tlb_flush_pending中是否包含当前 CPU:cpumask_test_and_clear_cpu(cpu, &context_tlb_flush_pending) -
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
-
如果 FPU 使能(
has_fpu()), 切换浮点寄存器:__switch_to_fpu(__prev, __next);has_fpu(): 是否支持 F 或 D 扩展
-
如果 Vector 使能(
has_vector()), 切换向量寄存器:__switch_to_vector(__prev, __next);has_vector(): 是否支持 V 扩展
- 切换通用寄存器:
__switch_to(__prev, __next);
切换浮点寄存器 __switch_to_fpu
- in
arch/riscv/include/asm/switch_to.h - in
arch/riscv/kernel/fpu.S
-
如果 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)
- 将浮点寄存器保存到指定进程的上下文中:
-
从
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_F0为thread_info结构体当中 f0 寄存器的偏移量(inarch/riscv/kernel/asm-offset.c)
- 获取
prev进程的上下文中保存 f0 的地址:a0 - 设置当前 status.FS 为 Dirty
- 将当前所有浮点寄存器保存到
prev进程的thread_info结构体中 - 将 fcsr 保存到
prev进程的thread_info结构体中(通过t0) - 设置当前 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 寄存器
- 获取
next进程的上下文中保存 f0 的地址:a0 - 设置当前 status.FS 为 Dirty
- 从
next进程的thread_info结构体中恢复所有浮点寄存器到当前 CPU - 从
next进程的thread_info结构体中恢复 fcsr (通过t0) - 设置当前 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
-
将向量寄存器保存到
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)
-
-
从
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 寄存器的偏移量(inarch/riscv/kernel/asm-offset.c)
- 获取
prev进程和next进程的上下文(a0, a1)中保存 ra 的地址:a3, a4 - 将当前 callee-saved 寄存器保存到
prev进程的thread_info结构体中 - 从
prev进程的thread_info结构体中加载 callee-saved 寄存器到 CPU 中 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)