AIGC标识 RISC-V Linux进程调度与上下文切换指南:从__switch_to到多核负载均衡

1. 引言:调度器是操作系统的脉搏

如果说内存初始化解决了"资源如何就位",那么调度解决的是"资源如何分配"。在 Linux 中,调度器位于中断、定时器与进程管理三者的交汇点:时钟中断提供节奏,进程管理提供对象,调度器在两者之间按策略分配 CPU 时间。本文沿调度路径展开,依次解析触发点、上下文切换细节与多核负载均衡三个核心问题,为后续运行时视角专题奠定基础。


2. CFS 与运行队列

较新内核默认以 CFS(Completely Fair Scheduler) 为主调度类,按虚拟运行时间(vruntime)的红黑树组织可运行任务。每 CPU 维护独立的运行队列与负载统计,避免跨核争用。任务通过 enqueue_task / dequeue_task 接口加入或离开运行队列,pick_next_task 选取下一个获得 CPU 的任务。

CFS 的设计目标是"按权分配时间",但对工程实践而言,更值得关注的是调度类的协作:实时任务以固定优先级抢占 CFS,空闲任务在无任务时进入 idle loop。日常调度的绝大多数场景落在 CFS 与 RT 两个调度类之间,其余 deadline 与 idle 类用于专项场景。


3. 调度触发:时钟中断到 context_switch

调度器的输入是调度时机,主要由三类事件构成:

  • 周期性时钟中断:RISC-V 平台通过 SBI 与 CLINT/ACLINT 提供定时器中断,处理器中断处理例程调用 scheduler_tick(),递减当前任务的时间片,必要时置 TIF_NEED_RESCHED;
  • 阻塞与唤醒:任务因系统调用进入睡眠而让出 CPU,或因资源就绪被唤醒时,可能触发 resched_curr(),将目标 CPU 置上重调度标志;
  • 显式调度点:如 schedule() 直接调用,或中断退出时的 preempt_schedule_irq。

当中断处理返回前若发现 TIF_NEED_RESCHED,即调用 schedule() 入口,最终落入 __schedule():取出下一个任务并调用 context_switch() 完成切换。该函数是调度器与进程管理的边界——其后的工作全部围绕"如何把 CPU 交给下一个任务"展开。


4. __switch_to:寄存器保存集与切换细节

context_switch() 完成后,调用链进入架构层的 __switch_to(prev, next),这是 RISC-V 上寄存器保存集与硬件状态切换的核心。RISC-V 调用约定下,被调用者保存的寄存器为 s0–s11、ra、sp 共 14 个通用寄存器。__switch_to 将这些寄存器保存到 prev->thread.sp 对应的内核栈帧,并从 next->thread 恢复相同字段:

__switch_to:
    save ra, s0..s11 to (prev->thread.sp)
    load ra, s0..s11 from (next->thread.sp)
    switch TP to next->thread_info
    switch satp to next->mm->pgd, with new ASID
    update sscratch to next kernel stack top
    ret                  /* returns into next task's context */

TP(x4)作为 task_struct 指针的固定载体,必须随任务切换,否则所有通过 current 宏访问 task_struct 的代码都会指向错误对象。satp 与 sscratch 的更新同样不可省略:satp 切换到 next 的页表根并附新 ASID,sscratch 指向 next 的内核栈顶,以便 trap 入口能快速取得当前任务的栈与上下文。

值得注意的是,__switch_to 的 ret 指令是关键:函数返回地址 ra 由汇编切换为 next 任务预先保存的值,从而让控制流"自然"回到 next 上一次被切换出去的代码位置——这是上下文切换对应用完全透明的硬件基础。


5. satp/ASID 协同:进程地址空间的切换

satp 的 PPN 字段切换到 next->mm->pgd 是直观的;ASID 字段的协同才是多进程 TLB 复用的关键。内核为每个 mm 分配一个 ASID,新 ASID 通过 switch_mm() 写入 satp;旧 ASID 仍在 TLB 中持有映射,由此实现"切换进程无须全量冲刷 TLB"。

当 ASID 计数器耗尽时,内核需回收所有 ASID 并广播 TLB flush。在 RISC-V 上,这一广播通过 SBI(远程 fence)或 IPI 触发各核执行 SFENCE.VMA 完成。flush 的成本与跨核通信延迟直接相关——在小核数上几乎无感,在多核 Cluster 上则成为调度路径的关键性能点。


6. 多核调度域与负载均衡

单核视角的调度器只解决"何时切换",多核还要解决"在哪个核运行"。内核将 CPU 按硬件拓扑划分为调度域(sched_domain):每个域对应一个负载均衡单位,域内通过拉取/推送任务维持负载均衡;跨域均衡的频率更低、开销更大。调度域的层级结构由设备树或固件表描述的拓扑决定,运行期不可更改。

在 RISC-V 多核 SoC 上,调度域的划分通常与硬件拓扑对齐——以最高 8 核 Cluster 为单位共享 LLC 与一致性互联的处理器,Cluster 本身即成为天然的多核调度域。玄铁 C950 通过 XT-Link 一致性互联构成多核 Cluster,并支持 RVWMO/TSO 内存模型与硬件一致性广播,使跨核调度与 TLB shootdown 在 Cluster 内具备较低延迟;该处理器的 Cluster 结构、内存模型与跨核交互细节可参见玄铁 C950 产品页。在该类平台上,调度域的层数与粒度(Cluster、DIE、NUMA 节点)由 dts 拓扑共同描述。


7. 实战:观测与绑核实验

在 QEMU virt 或真机上观测调度行为的最直接入口:

# 启动两个计算密集型任务并限定到不同 CPU
taskset -c 0 stress-ng --cpu 1 --timeout 30 &
taskset -c 1 stress-ng --cpu 1 --timeout 30 &

# 观测各 CPU 运行队列负载
watch -n 1 'cat /proc/sched_debug | head -40'

# 记录调度事件
perf sched record -a sleep 10
perf sched report

/proc/sched_debug 输出每个 CPU 的 CFS 运行队列、当前任务、就绪任务数与最近一次负载均衡时间;perf sched report 给出任务调度的延迟分布与热路径。结合 chrt -f 调整实时优先级,可观察 SCHED_FIFO 对 CFS 的抢占行为。

绑核实验使用 taskset 或 sched_setaffinity(),可消除负载均衡带来的不确定因素,便于在基准测试中复现一致的 CPU 归属。结合 numactl 可进一步观察 NUMA 调度域对跨节点访问的影响——多核 SoC 上 NUMA 拓扑会显著放大跨域调度的代价。


8. 常见问题与调试要点

  • 调度延迟高:多由时钟中断频率与 I/O 唤醒密集所致;调整 kernel.sched_latency_ns 与 kernel.sched_min_granularity_ns 改善;
  • 负载不均:检查调度域拓扑是否正确(/proc/sys/kernel/sched_domain/cpu*/domain*/flags),结合 /proc/sched_debug 与 dts 拓扑对照确认;
  • TLB shootdown 开销大:ASID 频繁耗尽或跨核任务迁移频繁所致,可通过减少跨核迁移或调高 ASID 位宽改善;
  • 实时任务调度不稳:确认 CONFIG_PREEMPT_RT 配置与 cgroup cpu 限制未被不当使用,实时任务的时间确定性严重依赖内核抢占模型与硬件中断亲和性设置。

9. 总结

调度路径以时钟中断为节拍、以 __schedule() 为主干、以 __switch_to 与 satp/ASID 协同为硬件衔接点,最终通过多核调度域将 CPU 时间分配到合适的核心。理解这条路径不仅有助于性能调优,也为后续讲解调度器扩展(如 EEVDF)奠定基础。

posted @ 2026-09-09 17:54  RISCV_Explore  阅读(16)  评论(0)    收藏  举报