第17章 调度子系统:从rq到EEVDF
第17章 调度子系统:从rq到EEVDF
内核版本:Linux 7.1.3
架构:x86_64
对应上篇引入章节:第10章
核心源码路径:
include/linux/sched.hkernel/sched/sched.hkernel/sched/core.ckernel/sched/fair.ckernel/sched/rt.ckernel/sched/deadline.ckernel/sched/ext.c
调度器要解决的问题很朴素:CPU 数量有限, runnable 任务可能很多,内核必须决定下一段 CPU 时间给谁。难点在于目标互相拉扯:吞吐量希望少切换,交互延迟希望快响应;公平性希望按权重分配,效率又要求每次选择足够便宜;实时任务要求确定性,普通任务要求平均公平。
17.0 五维内容矩阵
维度一:启动/初始化
调度子系统的早期入口是start_kernel() -> sched_init(),核心代码在kernel/sched/core.c。调用到这里时,系统仍处于 boot CPU 单核执行阶段,中断关闭,普通进程还不能创建,真正的多任务切换也还没有开始。它依赖早期内存管理已经建立到足以分配调度器对象和 per-CPU 数据;也依赖异常与中断环境逐步成形,因为后续上下文切换、抢占返回和时钟 tick 都要靠体系结构入口配合。
sched_init()的工作可以拆成三步。第一步是数据结构准备:为每个 CPU 初始化struct rq,并在其中建立cfs_rq、rt_rq、dl_rq等运行队列。第二步是调度类框架就位:stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class构成优先级链。第三步发生在rest_init():内核通过kernel_thread()创建 PID 1 的kernel_init路径和 PID 2 的kthreadd。
完成后,系统获得三项能力:进程可创建,任务可入队,schedule()有了可选择的运行队列。调度器核心策略基本与架构无关,但保存/恢复寄存器、切换内核栈和地址空间的底层动作落在 x86_64 的switch_to,位于arch/x86/include/asm/switch_to.h及相关汇编路径。
维度二:原理/设计
调度器的根本问题是:CPU 时间如何在多个 runnable 任务之间分配,同时让实时任务获得可预测延迟。这个问题没有单一最优解。时间片越大,吞吐量和 cache 命中往往更好,但交互任务响应会变慢;调度越公平,统计和树操作越复杂;实时任务越强势,普通任务越容易被饿死。
Linux 用调度类隔离策略,用统一的schedule()串起选择过程:
schedule() -> __schedule() -> stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class调度策略到调度类的映射由sched_setscheduler()等路径建立:SCHED_NORMAL和SCHED_BATCH进入 fair 类,SCHED_FIFO和SCHED_RR进入 RT 类,SCHED_DEADLINE进入 DL 类,SCHED_IDLE进入 idle 类。普通任务的核心思想来自 CFS/EEVDF:用虚拟运行时间和虚拟截止时间描述任务相对公平份额,优先运行“欠账更多或截止更早”的任务。
维度三:核心数据结构
调度器对象关系可以压缩成一条链:
task_struct -> sched_entity / sched_rt_entity / sched_dl_entity -> cfs_rq / rt_rq / dl_rq -> rq(per-CPU)task_struct是进程描述符,调度相关字段包括prio、static_prio、normal_prio、policy、sched_class,以及嵌入的se、rt、dl调度实体。struct rq是每 CPU 运行队列,关键字段是curr、idle、nr_running、cfs、rt、dl和 rq 自身锁。struct cfs_rq管理 fair 类实体,包含tasks_timeline、min_vruntime或 EEVDF 相关统计、nr_running/nr_queued、curr等。struct sched_entity是 fair 类中的可调度实体,字段包括vruntime、deadline、load、run_node、on_rq、sum_exec_runtime。
task_struct通常来自 fork 路径的专用 cache,随进程创建和退出分配释放;rq是 per-CPU 长生命周期对象,启动后基本常驻;sched_entity嵌在 task 或调度组对象中,随所属对象存在。调度器热路径尽量避免临时分配,因为调度本身正是系统在内存压力和锁竞争下仍必须可用的基础设施。
维度四:核心算法
最核心的调度决策路径是:
schedule() -> __schedule() -> pick_next_task() -> context_switch() -> switch_to()pick_next_task()按调度类链选择最高优先级类中的下一个任务。fair 类的选择路径是fair_sched_class -> pick_next_task_fair(),在 CFS/EEVDF 队列中选择合适的sched_entity。传统表述是取红黑树中vruntime最小者;在 EEVDF 语义下,还要看 entity 是否 eligible、虚拟截止时间是否更早、lag 是否需要被保护。
唤醒抢占路径是另一条热路径:
try_to_wake_up() -> ttwu_do_wakeup() -> check_preempt_curr() -> resched_curr() -> 设置 TIF_NEED_RESCHEDSMP 负载均衡路径则由周期性调度器或 idle balance 触发:
run_rebalance_domains() -> load_balance() -> find_busiest_group() -> find_busiest_queue() -> migrate task维度五:调试与观测
调度状态优先从运行时接口看:
/proc/sched_debug /proc/schedstat /proc/<pid>/sched tracepoint:sched_switch tracepoint:sched_wakeup tracepoint:sched_migrate_taskGDB 或 crash 调试可以从schedule、__schedule、pick_next_task_fair、try_to_wake_up、load_balance下断点。常见问题不是“调度器坏了”,而是某类约束让任务没有机会运行:RT 任务长期占 CPU、cgroup quota throttle、软中断占用、不可抢占区太长、CPU affinity 过窄或负载均衡被隔离配置限制。soft lockup 的典型诊断是结合 watchdog 日志和/proc/sched_debug,确认某 CPU 当前任务是否长时间不发生调度。
17.1 调度类架构
Linux 用sched_class把不同策略放进同一框架。核心结构在kernel/sched/sched.h:
structsched_class{void(*enqueue_task)(structrq*rq,structtask_struct*p,intflags);bool(*dequeue_task)(structrq*rq,structtask_struct*p,intflags);void(*wakeup_preempt)(structrq*rq,structtask_struct*p,intflags);int(*balance)(structrq*rq,structtask_struct*prev,structrq_flags*rf);structtask_struct*(*pick_task)(structrq*rq,structrq_flags*rf);structtask_struct*(*pick_next_task)(structrq*rq,structtask_struct*prev,structrq_flags*rf);void(*put_prev_task)(structrq*rq,structtask_struct*p,structtask_struct*next);void(*set_next_task)(structrq*rq,structtask_struct*p,bool first);void(*task_tick)(structrq*rq,structtask_struct*p,intqueued);void(*update_curr)(structrq*rq);};优先级链:
stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> ext_sched_class 可选 -> idle_sched_classstop 用于 CPU hotplug、迁移等特殊停止任务;DL 实现 deadline;RT 实现 FIFO/RR;fair 是普通任务 CFS/EEVDF;idle 是兜底。
17.2 核心数据结构
task_struct中的调度字段在include/linux/sched.h:
structtask_struct{intprio;intstatic_prio;intnormal_prio;unsignedintpolicy;structsched_entityse;structsched_rt_entityrt;structsched_dl_entitydl;structsched_ext_entityscx;conststructsched_class*sched_class;};每 CPU runqueue 是调度器的中心:
structrq{unsignedintnr_running;structtask_struct*curr;structtask_struct*idle;raw_spinlock_t__lock;structcfs_rqcfs;structrt_rqrt;structdl_rqdl;conststructsched_class*next_class;intcpu;};CFS/EEVDF 的队列:
structcfs_rq{structload_weightload;unsignedintnr_queued;s64 sum_w_vruntime;u64 sum_weight;u64 zero_vruntime;structrb_root_cachedtasks_timeline;structsched_entity*curr;structsched_entity*next;structsched_avgavg;};普通任务的调度实体:
structsched_entity{structload_weightload;structrb_noderun_node;u64 deadline;u64 min_vruntime;u64 min_slice;u64 max_slice;u64 exec_start;u64 sum_exec_runtime;u64 vruntime;s64 vlag;u64 vprot;u64 slice;structsched_avgavg;};关系图:
CPU -> rq rq->cfs -> cfs_rq->tasks_timeline -> sched_entity -> task_struct rq->rt -> priority arrays rq->dl -> deadline rb-tree17.3 EEVDF 核心思路
传统 CFS 主要围绕vruntime选择最左实体。EEVDF 在保留虚拟时间的基础上,引入 eligible、virtual deadline 和 lag:任务只有在虚拟 lag 表示“有资格”时才参与选择,选择时倾向虚拟截止时间更早者。
简化模型:
weight 决定任务应得 CPU 份额 vruntime 记录已消费的加权运行时间 vlag 估计任务相对公平份额是欠账还是超前 deadline 用于 EEVDF 选择的虚拟截止时间 slice 影响 deadline 和延迟边界就绪队列仍然使用红黑树缓存节点,但语义从“最小 vruntime”演进到“eligible entity 中合适的 earliest deadline”。延迟和滞后边界通过vlag、vprot、slice 保护等字段限制,避免短任务无限插队或长任务长期饥饿。
17.4 唤醒抢占
唤醒路径会把任务 enqueue 到目标 rq,然后调用调度类的wakeup_preempt()。CFS/EEVDF 的判断不是简单“新任务优先级更高”,而是比较当前任务和新唤醒实体的虚拟时间状态、deadline、是否 eligible、是否需要降低交互延迟。
结果是:
- 新任务明显落后或 deadline 更紧时,可以设置 resched。
- 当前任务仍在合理 slice 内时,避免过度抢占。
- RT/DL 类因为优先级链高于 fair,会在策略层面直接压过普通任务。
更完整地看,唤醒不是单点动作,而是一条从等待队列到目标 CPU 的路径。典型睡眠任务在等待锁、I/O、futex、pipe 或 epoll 事件时挂在某个 wait queue 上;事件发生后,唤醒方调用try_to_wake_up(),在合适的锁保护下把任务状态从 sleeping 改回 runnable,然后选择 CPU 并入队。这个选择 CPU 的过程会综合 affinity、当前 CPU、上次运行 CPU、调度域拓扑和 wake affine。它既想让任务尽快运行,又不想为了短任务频繁跨 CPU 迁移,破坏 cache locality。
try_to_wake_up()的核心约束是并发正确性。一个任务可能正在睡眠路径设置状态,另一个 CPU 同时唤醒它;一个任务可能刚从 runqueue 取下,迁移路径又想移动它。调度器用pi_lock、rq lock、状态位和内存屏障把这些窗口收窄。理解唤醒抢占时,不能只看最后的resched_curr(),还要看到前面的状态同步:只有任务确实被成功激活,并且目标 rq 上的调度类认为它应该尽快运行,抢占请求才有意义。
fair 类的抢占判断体现了 EEVDF 的设计目标。旧 CFS 常用wakeup_granularity避免新唤醒任务因为极小的vruntime差异过度抢占;EEVDF 语义下,任务的deadline、vlag和 slice 共同决定“它是不是已经落后到值得抢占”。这使交互任务能更快获得 CPU,同时保留对吞吐型任务的保护。对于 RT 和 Deadline,情况更直接:调度类优先级高于 fair,只要新任务可运行且约束满足,就会要求当前普通任务让出 CPU。
17.5 负载均衡
SMP 上每 CPU 一个 rq。负载均衡负责在 CPU 间迁移任务,触发点包括 tick、新 idle、周期性 balance、wake affine 等。调度域sched_domain描述 CPU 拓扑,root domain 聚合实时/deadline 约束。
负载均衡的权衡是成本和收益:迁移能提高 CPU 利用率和公平性,但会损失 cache locality。现代调度还要考虑 capacity、NUMA、energy-aware scheduling、utilization clamping。
调度域把硬件拓扑表达成层次:SMT sibling、同一 core、同一 LLC、同一 NUMA node、跨 node。每层都有自己的 balance interval、imbalance_pct、flags 和 capacity 信息。负载均衡不是简单找“任务最多”的 CPU,而是先判断调度组是否过载、是否有空闲 CPU、是否存在 capacity 不匹配,再决定从 busiest group 拉取哪些任务。
新 idle balance 是低延迟路径:CPU 即将进入 idle 时,如果同一调度域里有明显可迁移任务,它会主动拉任务,避免自己睡下而其他 CPU 排队。周期性 balance 则由 tick 或调度时钟推动,更偏向全局公平。wake affine 则发生在唤醒时,倾向把刚被唤醒的任务放到与唤醒者相关或 cache 更热的 CPU 上。三者服务不同场景:新 idle 追求利用率,周期性 balance 修正长期偏差,wake affine 追求短期局部性。
NUMA balancing 又增加了一层复杂性。任务的 CPU 位置和内存页位置如果长期分离,会造成远端访问;但迁移任务或迁移页都有成本。调度器、内存管理和 NUMA balancing 共同观察访问模式,试图让任务靠近热内存,或者让热页靠近任务。这个逻辑说明调度器并不是孤立分配 CPU 时间,它和内存拓扑、缓存层级、电源模型共同决定实际性能。
17.6 RT 和 Deadline
RT 调度使用优先级数组和 bitmap。SCHED_FIFO不主动时间片轮转,SCHED_RR有轮转时间片。RT 任务高于 fair,但受 RT bandwidth 控制,避免完全饿死普通任务。
Deadline 调度基于 CBS:每个任务有 runtime、deadline、period。调度器按绝对 deadline 选择,运行时消耗 runtime;runtime 用尽时推迟到下一周期补充。dl_rq使用按 deadline 排序的红黑树,并维护带宽统计。
RT 队列的核心是“固定优先级”。每个优先级一个队列,bitmap 用来快速找到最高非空优先级。SCHED_FIFO任务一旦运行,只要不阻塞、不主动让出、不被更高优先级抢占,就可以持续运行;SCHED_RR在同优先级任务之间按时间片轮转。这个模型简单、可预测,但很容易饿死普通任务,所以内核提供 RT runtime/bandwidth 控制,让 RT 类在一个周期内最多消耗一定运行时间。
Deadline 类解决的是另一类问题:任务声明“每 period 需要 runtime,必须在 deadline 前完成”。CBS 的意义在于把实时需求转化为带宽预算。任务运行会消耗 runtime;预算耗尽时,即使任务还想继续运行,也要被推迟到下一周期。这样多个 deadline 任务可以按总带宽进行可行性控制,避免单个任务超用 CPU 后破坏其他任务的 deadline。
这三类调度策略可以放在同一优先级链中理解:Deadline 关注时间约束,RT 关注固定优先级,Fair/EEVDF 关注长期公平和交互延迟。__schedule()不需要理解每种策略的全部细节,它只需按sched_class链调用pick_next_task()。策略差异封装在类内部,这就是调度类架构的价值。
17.7 从 tick 到上下文切换
调度并不只在系统调用返回时发生。触发调度的来源包括:
timer tick / hrtimer -> scheduler_tick() -> curr->sched_class->task_tick() -> need_resched wakeup -> try_to_wake_up() -> enqueue_task() -> wakeup_preempt() blocking -> set_current_state() -> schedule() explicit yield / preempt enable / irq return -> 检查 TIF_NEED_RESCHED -> schedule()__schedule()是核心切换点。它先把当前任务按状态决定是否留在 rq:仍 runnable 的任务继续在队列里竞争,阻塞任务从 rq 移除;然后从最高优先级调度类开始选择下一个任务。选中后,context_switch()负责 mm、寄存器、栈、体系结构状态和 rq 当前任务指针的切换。
上下文切换的成本来自多处:保存和恢复寄存器、切换内核栈、可能切换地址空间、污染 cache/TLB、打断流水线。调度器追求低延迟,但不会为了每次微小优势都切换任务。EEVDF 的 slice、deadline、lag 控制,本质上就是在“及时响应”和“少切换”之间放置可解释的边界。
17.8 EEVDF 的字段如何协作
可以把普通任务的选择拆成三个问题:
1. 任务是否 runnable:是否在 cfs_rq 中 2. 任务是否 eligible:它是否没有超前太多 3. 在 eligible 任务中选谁:虚拟截止时间更早者优先vruntime仍然表示加权后的已运行时间。权重越大,同样真实运行时间带来的vruntime增量越小,因此高权重任务长期获得更多 CPU。vlag表示任务相对理想公平份额的偏离:正负方向分别可理解为欠账或超前。deadline则把 slice 纳入选择,短 slice 任务可以获得更早的虚拟截止时间,从而改善交互响应。
这种设计比“永远取最小 vruntime”更能表达延迟目标。一个任务即使vruntime较小,如果已经超前或不 eligible,也不该无限插队;一个交互任务刚从睡眠回来,如果相对公平份额落后且 deadline 更近,就应该尽快运行。EEVDF 让公平和延迟不再混在单个vruntime排序里,而是拆成 eligibility 与 deadline 两层判断。
源码阅读时要抓住几个点:enqueue 时更新实体的虚拟时间和 deadline;pick 时从tasks_timeline中寻找合适实体;tick 和 dequeue 时更新当前实体的运行时间;层级 group scheduling 下,调度实体可能代表一个 task,也可能代表一个 task group。后者会把“组之间公平”和“组内任务公平”串起来,形成多层 cfs_rq。
17.9 调度器与锁、抢占模型
调度器本身位于并发控制的中心。rq lock 保护每 CPU runqueue,迁移时可能同时涉及两个 rq,需要按地址或 CPU 顺序加锁以避免死锁。preempt_count控制当前上下文能否被抢占,中断、软中断、持有 raw spinlock 的区域都可能推迟调度。
PREEMPT_DYNAMIC 让同一个内核可以在不同抢占模型之间切换,从服务器吞吐优先到桌面低延迟优先。PREEMPT_RT 则把许多原本不可睡眠的锁和中断路径改造成可抢占、可线程化的形态,以降低最坏延迟。调度器必须同时服务这些配置,因此很多代码会区分 raw spinlock、普通 spinlock、irq state 和 preempt disable 边界。
调度器与 RCU 也紧密相连。上下文切换、idle 进入退出、用户态/内核态边界都会影响 RCU quiescent state 的判断。一个 CPU 长时间运行不可抢占内核代码,会拖延 grace period;实时化和抢占模型变化也会改变 RCU 的实现细节。第10章看到的 RCU 和本章调度器,实际上在运行时互相提供时序信息。
17.10 观察一次调度决策
读/proc/<pid>/sched时,可以把字段分成几组:
se.sum_exec_runtime 任务累计运行时间 se.vruntime 公平类虚拟运行时间 nr_switches 上下文切换次数 se.avg.* PELT 负载/util 信号 policy/prio 调度策略和优先级读/proc/sched_debug时,应先看 CPU/rq 层面的nr_running、curr、cfs/rt/dl 队列,再看调度域和负载均衡信息。tracepointsched_switch能告诉你谁切给谁,sched_wakeup能看到唤醒者和被唤醒者,sched_migrate_task能看到迁移。把三者按时间线连起来,通常可以解释一次延迟尖峰:任务什么时候被唤醒、被放到哪个 CPU、是否迁移、何时真正运行。
调试建议不是只打__schedule()。如果问题是“为什么没有被唤醒”,看try_to_wake_up();如果问题是“为什么唤醒后不抢占”,看wakeup_preempt_entity()或 fair 类 pick 路径;如果问题是“为什么跨 CPU 跑”,看 select task rq 和 load balance;如果问题是“实时任务抢占异常”,看 rt_rq、dl_rq 和 bandwidth 状态。
17.11 源码阅读路线:从schedule()到pick_next_task()
调度器源码容易迷路,因为入口很多。建议先抓一条最普通的主动睡眠路径:
schedule() -> __schedule(SM_NONE) -> prev_state = READ_ONCE(prev->__state) -> deactivate_task() 或保留 runnable -> pick_next_task() -> context_switch()schedule()的调用者通常已经设置好当前任务状态。例如等待 I/O 的任务先把状态设为TASK_UNINTERRUPTIBLE或TASK_INTERRUPTIBLE,再调用schedule();如果在真正切走前事件已经发生,唤醒方可能把状态改回 runnable,调度器就不能把它从 rq 移除。这就是状态检查和锁顺序如此重要的原因。
pick_next_task()体现调度类链。它先看是否能走 fair fast path;否则按 stop、dl、rt、fair、ext、idle 的顺序询问各类。高优先级类只要有可运行任务,就不会落到低优先级类。普通任务平时都在 fair 类里竞争;当系统存在 deadline 或 RT 任务时,fair 类可能长时间没有机会运行,这时 RT bandwidth 和 DL admission control 就是保护系统可用性的关键。
context_switch()不只是换一个 C 指针。它会处理地址空间切换、体系结构寄存器、内核栈、FPU 状态延迟保存、lockdep/RCU/accounting 钩子。对于同一进程的两个线程,mm 相同,切换成本相对低;对于不同进程,可能需要切 CR3 或使用 PCID/ASID 优化 TLB 影响。调度策略选择的是“谁运行”,体系结构代码负责“怎么切过去”。
17.12 PELT、uclamp 与能耗感知
现代调度器不仅看 runnable 数量,还要估计任务负载。PELT(Per-Entity Load Tracking)用指数衰减方式记录任务和 rq 的 runnable/load/util 信号。短时间运行的任务不会立刻被当成满负载,长期运行的任务 util 会逐步上升;任务睡眠后 util 逐步衰减。这些信号喂给负载均衡、CPU frequency、EAS 和 uclamp。
utilization clamping 提供上下限约束。uclamp.min可以保证任务至少按某个利用率看待,避免延迟敏感任务被放到太弱的 CPU 或频率太低;uclamp.max可以限制后台任务不要拉高频率。移动设备和混合大小核平台尤其依赖这个机制,因为“公平地分 CPU 时间”并不等于“选择合适性能/能耗点”。
Energy-aware scheduling 会把 CPU capacity、能耗模型和任务 util 结合起来。小核可能能效高但容量低,大核容量高但耗电大。调度器在选择 CPU 时要判断任务放到哪个 CPU 能满足 util,同时使能耗估计更低。这个过程不是替代公平调度,而是在 select task rq 和负载均衡阶段影响放置决策。服务器也有类似问题,只是约束更多体现为 NUMA、cache 和 SMT。
这些机制说明 Linux 调度器已经从“队列里挑下一个任务”扩展成“在拓扑、能耗、实时性、公平性之间做动态决策”。EEVDF 处理 fair 类内部的延迟与公平,PELT/uclamp/EAS 处理任务放置和性能信号,RT/DL 处理显式实时约束。它们共同构成 7.1.3 中调度子系统的完整面貌。
17.13 调度组与 cgroup
普通任务的公平性不只发生在 task 之间,也发生在 task group 之间。启用 group scheduling 后,cgroup 可以拥有自己的调度实体。父级cfs_rq看到的是代表整个组的sched_entity,组内部再有自己的cfs_rq管理成员任务。这样 CPU 权重可以按服务、容器或用户分配,而不是让拥有更多线程的组天然获得更多 CPU。
层级调度的选择路径可以理解为多次 pick:先在父 cfs_rq 中选中某个组实体,再进入组内 cfs_rq 选具体任务。如果组内还有子组,就继续向下。运行时间也会沿层级累计,保证父级看到的组消耗能反映子任务实际运行。这个模型让cpu.weight、cpu.max等控制接口有了内核基础。
cgroup quota 与普通公平不同。quota 限制一个周期内最多可用 CPU 时间,超过后整个 cgroup 会被 throttle,即使其中还有 runnable 任务也不能继续运行。排查容器“CPU 很空但进程卡住”时,要检查 cgroup CPU throttling 统计。调度器此时不是找不到任务,而是资源控制明确禁止该组继续消耗 CPU。
17.14 失败模式和经验判断
调度问题经常表现为延迟,而不是崩溃。常见原因包括:RT 任务长期占用 CPU、cgroup quota 过低、负载均衡迁移过度、NUMA 位置不佳、软中断挤占、不可抢占内核区太长、CPU isolation 配置和实际中断亲和性不一致。分析时应把“任务没运行”拆成几个问题:它是否 runnable;是否在正确 rq;是否被更高类任务压住;是否被 cgroup throttle;是否被 IRQ/softirq 占用;是否被迁移到远端 CPU。
一个实用的观察顺序是:先用top、pidstat、perf sched或 tracepoint 确认 runnable 延迟;再用/proc/<pid>/sched看nr_switches、wait sum 和 PELT;再看/proc/sched_debug的 rq 状态;最后才进入源码断点。不要一开始就断在__schedule(),因为调度器每秒可能运行海量次数,没有清晰问题假设时很难从断点中得到结论。
本章的核心结论是:调度器不是单个算法,而是一组分层决策。sched_class决定策略优先级,rq/cfs_rq 保存局部状态,EEVDF 处理 fair 任务选择,RT/DL 处理实时约束,负载均衡处理跨 CPU 放置,PELT/uclamp/EAS 处理性能和能耗信号。理解这些层次后,再读kernel/sched/core.c和kernel/sched/fair.c,代码路径会清晰得多。
17.15 sched_ext
Linux 7.1.3 包含kernel/sched/ext.c。sched_ext 允许 BPF scheduler 通过sched_ext_ops接管一部分普通调度策略。它不是替代 core scheduler,而是在调度类框架中增加可扩展层。关键点:
- BPF struct_ops 提供 select_cpu、enqueue、dispatch、tick 等回调。
- 异常、超时或 lockup 时可以 disable 并回退。
- 它把调度策略实验从修改内核 C 代码转移到受约束的 BPF 接口。
17.16 版本演进:5.12 到 7.1.3
- CFS 语义从单纯 vruntime 红黑树逐步演进到 EEVDF,目标是更明确地表达延迟和公平份额。
- sched_ext 进入主线后,调度策略扩展有了 BPF struct_ops 通道。
- PREEMPT_DYNAMIC 让内核可以在不同抢占模型间切换,PREEMPT_RT 持续把实时语义推进主线。
- uclamp 和 EAS 让调度器能把性能、能耗和任务利用率约束纳入决策。
17.17 调试观察点
GDB 断点:
sched_init __schedule pick_next_task_fair enqueue_task_fair wakeup_preempt_entity load_balance运行时接口:
/proc/sched_debug /proc/<pid>/sched /sys/kernel/debug/sched/ tracepoints: sched_switch, sched_wakeup, sched_migrate_task下一章:内存管理子系统深度剖析。
