摘要
面向 Linux 6.18 的一组调度器补丁将 CFS 带宽限制(throttling) 从“立即把队列移出运行队列”改为基于任务的限制:当 CFS 运行队列(cfs_rq)需要限制时,不立刻整体摘除,而是在任务被挑中后挂上 task_work,使其在用户态返回路径上再执行限制(再真正出队)。这样可避免任务在内核态持锁时被强制剥离引发的高尾延迟,并化解 PREEMPT_RT(实时补丁)场景下的潜在死锁。同时,补丁同步调整 PELT 行为与限制时间记账,提升 Observability(可观测性)与行为一致性。该系列已进入 tip 的 sched/core 队列,预计随 6.18 合入。
这里的“限制”到底指什么?
限制(CFS 带宽限制,CFS bandwidth throttling):当某个 cgroup的 CPU 配额 用尽时,禁止该层级下的 CFS 任务继续消耗 CPU 运行时,直到下一周期配额补充为止。
• 触发条件:配额用尽(cgroup v2 的 cpu.max,或 v1cpu.cfs_quota_us/cpu.cfs_period_us)。• 限制动作:把相关任务从“可继续运行”的状态移除/挂起于过渡列表(新模型改为在任务返回用户态时再实际出队)。 • 解除限制(unthrottle):周期到来配额补充后,再把这些任务重新入队,恢复调度。
背景:为什么要把“限制动作”挪到用户态返回路径上?
传统 CFS 带宽控制在配额用尽时,会立即把对应 cfs_rq 从 CPU 的 runqueue 移出,达成“限制”目标。然而这带来两个突出问题:
-
• 尾延迟被放大:若被限制的任务此时正持有内核锁(例如 read_lock),会阻塞其它未受限制任务的锁获取,导致资源获取被拒、尾延迟拉长。 -
• PREEMPT_RT 潜在死锁:在 RT 内核中,读/写锁与软中断线程(如 ksoftirqd/ktimers)可能形成依赖环:被限制任务持读锁→写者阻塞→新的读者走 slowpath;若计时器/软中断线程也被链路牵制,可能无法解开。
核心思路:把“限制动作”从“立刻摘队”改为在 return-to-user(用户态返回)路径上触发。这样就不会在内核态持锁的中间态被硬剥离,从而缓解尾延迟并破除 RT 中的环形依赖。
核心改动与设计要点
1)从“队列级限制”到“任务级限制”
-
• 旧模型: cfs_rq一旦被限制,整体从 CPU runqueue 移出,附属任务全部不可被调度。 -
• 新模型: cfs_rq被限制时仅标记限制状态,不立即移出;当其中某个任务被挑中运行时,给它挂一个 task_work,使其在用户态返回路径上再执行限制(再真正出队/进入过渡列表)。 -
• 效果:避免“内核态持锁时被强摘”,打断锁链,降低尾延迟并消除死锁条件。 -
• 实现点:为任务与 cfs_rq增加限制标记与“limbo”过渡列表;在 解除限制(unthrottle) 时把这些任务批量恢复入队。
2)PELT(Per-Entity Load Tracking,逐实体负载跟踪)行为调整
-
• 旧行为: cfs_rq一旦被限制就冻结 PELT 时钟。 -
• 新行为:只要被限制的 cfs_rq仍有实体排队/在内核态运行,就保持 PELT 时钟运行;当最后一个实体出队时才冻结,使负载估计更贴近真实。
3)限制时间记账(throttle time accounting)口径更新
-
• cpu.stat.local中的throttled_time:记账口径由“配额用尽瞬间”改为“该层级首个任务真正被延后(在 return-to-user 路径上触发)的时刻”,更贴近实际受影响的区间。 -
• 对 Observability(可观测性) 的影响:监控趋势与报警阈值可能需要重新基线。
4)关键路径与数据结构清理
-
• 在任务迁移/换组/切换调度类等场景,保证“被限制任务”的入/出队一致性(如精简 detach_task_cfs_rq()、enqueue_throttled_task()快路径)。 -
• 清理与限制相关的遗留负载均衡逻辑,降低复杂度;将部分 int标志改为bool,并补全注释与告警。
适用场景与落地建议
受益最大的场景
-
• PREEMPT_RT / 低延迟:避免“内核态持锁 + 立刻限制”引发的环形依赖/死锁,改善 latency tail。 -
• 大规模多层级配额管理:在服务器混部场景中广泛启用 CFS 带宽控制( cpu.max或cpu.cfs_*)的集群。
对 Performance Engineering(性能工程)与 Observability(可观测性)的注意点
-
• 记账口径变更: throttled_time的定义更贴近“真正受影响的时段”,对趋势线、百分位尾延迟报警可能产生影响,建议重建基线。 -
• PELT 行为:在“部分限制”状态下 PELT 不再立即冻结,建议结合 /proc/sched_debug观测 cfs_rq 负载、队列深度与权重变化。 -
• CFS调度器变更行为提醒:不再是“限制会立刻把任务从 runqueue 摘除”。现在的行为是:先打限制标记,任务在 return-to-user(返回用户态)时才真正从 rq 移除。
工程实践清单
-
• 压测/回归:使用 fork/exit 压力 与 cgroup_threadgroup_rwsem 压力脚本验证迁移后是否消除锁链卡顿、降低尾延迟。 -
• 指标留档:升级或打补丁前后,记录 throttled_time、软中断延迟、关键业务 P99/P999 延迟做对比。 -
• 代码审计:检查 out-of-tree 模块与定制补丁,避免依赖旧语义导致的边界行为偏差。
与上游进展(简述)
该系列补丁已进入 tip 的 sched/core 队列,若无异议,预计随 6.18 合入。主要改动集中于 kernel/sched/fair.c、sched.h、pelt.h 等,伴随注释与字段 bool 化等清理。强烈建议依赖 CFS 带宽控制与 RT 的系统尽早预研与测试。
总结
把 CFS 带宽限制的执行点从“立刻摘队”移动到用户态返回路径,并配合 PELT 与限制时间记账的同步调整,使调度器在延迟控制与 RT 安全性之间取得更好的平衡:
-
• 更低尾延迟:避免内核态持锁时被强制剥离; -
• 更稳 RT:破除潜在的锁链环路; -
• 更准可观测:记账更贴近业务实际受影响区间。
预计随 6.18 合入。

