第 3 章:Burst 支持与 Async Unthrottle
阶段概览
这个阶段(v5.14~v6.3)为 CFS 带宽控制增加了两个重要特性:
- Burstable CFS Controller(v5.14, 2021, Huaixin Chang):允许 task group 借用之前未用完的配额,应对突发性负载
- Async Unthrottle(v6.3, 2022, Josh Don):通过 IPI/CSD 机制将 unthrottle 操作异步化,避免 period timer 中长时间持有远程 CPU 的 rq lock
这两个特性虽然独立,但共同指向一个问题:在 per-CFS_RQ 模型中,throttle/unthrottle 的同步机制对系统延迟的影响。
关键 Commit 分析
Commit f4183717b370 — sched/fair: Introduce the burstable CFS controller
作者: Huaixin Chang | 日期: 2021-06-21 | 版本: v5.14
规模: +73 / -10 行, 3 个文件
类型: 🆕 新增功能
变更概述
引入 cfs_burst_us 控制文件和一个新的 burst 配额概念:task group 可以累积在周期内未用完的配额,在后续周期中使用。这缓解了"周期边界效应"——一个任务在周期末尾有突发性负载时,即使之前大部分时间空闲,也可能被 throttle。
用户接口:
cpu.cfs_burst_us(legacy cgroup)cpu.max.burst(cgroup v2)
核心代码分析
数据结构:在 struct cfs_bandwidth 中新增 u64 burst 字段,表示允许超出 quota 的最大数量。
__refill_cfs_bandwidth_runtime() — 核心改变:
原来配额刷新是覆盖式的:
// 旧行为:cfs_b->runtime 被直接重置为 quota
void __refill_cfs_bandwidth_runtime(struct cfs_bandwidth *cfs_b)
{
if (cfs_b->quota != RUNTIME_INF)
cfs_b->runtime = cfs_b->quota;
}2
3
4
5
6
新行为是累加式的,但有上限:
// 新行为:cfs_b->runtime += quota,但上限是 quota + burst
void __refill_cfs_bandwidth_runtime(struct cfs_bandwidth *cfs_b)
{
if (unlikely(cfs_b->quota == RUNTIME_INF))
return;
cfs_b->runtime += cfs_b->quota; // 累加
cfs_b->runtime = min(cfs_b->runtime, // 上限 = quota + burst
cfs_b->quota + cfs_b->burst);
}2
3
4
5
6
7
8
9
10
设计要点:
- 如果周期内未用完 quota,
cfs_b->runtime中会有剩余 - 下个周期刷新时,
runtime += quota,但最多到quota + burst - 这相当于允许 group 借用最多
burst量的"信用配额" - 验证逻辑:
burst <= quota且burst + quota <= max_cfs_runtime
setter 接口:
static int tg_set_cfs_bandwidth(struct task_group *tg, u64 period, u64 quota,
u64 burst)
{
// ... 验证 ...
if (quota != RUNTIME_INF && (burst > quota ||
burst + quota > max_cfs_runtime))
return -EINVAL;
raw_spin_lock_irq(&cfs_b->lock);
cfs_b->period = ns_to_ktime(period);
cfs_b->quota = quota;
cfs_b->burst = burst;
__refill_cfs_bandwidth_runtime(cfs_b);
// ...
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
定时器中的变化:原来的实现中,__refill_cfs_bandwidth_runtime() 在 idle 检查之后才调用——即如果 cfs_b 处于 idle 状态且没有 throttled cfs_rq,定时器会提前退出而不刷新配额。对于 burst 功能,这个顺序需要调整:即使 cfs_b 处于 idle,也需要定期累加 burst 配额。所以:
// 将 __refill_cfs_bandwidth_runtime() 移到 idle 检查之前
/* Refill extra burst quota even if cfs_b->idle */
__refill_cfs_bandwidth_runtime(cfs_b);
if (cfs_b->idle && !throttled)
goto out_deactivate;2
3
4
5
6
演进意义
Burst 支持是带宽控制从"严格限制"走向"弹性控制"的重要一步。它允许系统应对真实工作负载的突发性特征,同时仍然保证长期平均使用率不超过 quota。在架构上,它证明了在 per-CFS_RQ 模型上扩展新功能的可行性。
Commit bcb1704a1ed2 — sched/fair: Add cfs bandwidth burst statistics
作者: Huaixin Chang | 日期: 2021-08-30 | 版本: v5.15
规模: (新增统计接口)
变更概述
为 burst 功能增加了 nr_burst 统计,在 cpu.stat 中输出,用于监控和调试 burst 使用情况。
在 struct cfs_bandwidth 中新增 int nr_burst 字段,在每次 burst 被实际使用时递增。
Commit 8ad075c2eb1f — sched: Async unthrottling for cfs bandwidth
作者: Josh Don | 日期: 2022-11-16 | 版本: v6.3
规模: +150 / -13 行, 2 个文件
类型: 🆕 新增功能
变更概述
这是 per-CFS_RQ 模型中一个重要的性能优化。在分布带宽时,distribute_cfs_runtime() 需要逐个获取被 throttle 的 cfs_rq 所属的 rq->lock 来执行 unthrottle。对于跨多个 CPU 的 cgroup,这会在 period timer 中产生长延迟的锁等待时间。
Josh Don 引入了 CSD (Call Single Data) 机制:不直接在其他 CPU 上执行 unthrottle,而是将工作排队,通过 IPI 异步执行。
核心代码分析
新数据结构:
// sched.h — per-cfs_rq 新增 CSD 链表
struct cfs_rq {
// ...
struct list_head throttled_csd_list; // CSD 队列节点
};
// sched.h — per-rq 新增 CSD 资源和队列
struct rq {
// ...
call_single_data_t cfsb_csd; // CSD 执行上下文
struct list_head cfsb_csd_list; // 待 unthrottle 的 cfs_rq 链表
};2
3
4
5
6
7
8
9
10
11
12
CSD 回调函数:
#ifdef CONFIG_SMP
static void __cfsb_csd_unthrottle(void *arg)
{
struct cfs_rq *cursor, *tmp;
struct rq *rq = arg;
struct rq_flags rf;
rq_lock(rq, &rf);
rcu_read_lock();
list_for_each_entry_safe(cursor, tmp, &rq->cfsb_csd_list,
throttled_csd_list) {
list_del_init(&cursor->throttled_csd_list);
if (cfs_rq_throttled(cursor))
unthrottle_cfs_rq(cursor);
}
rcu_read_unlock();
rq_unlock(rq, &rf);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
异步 unthrottle 入口:
static inline void __unthrottle_cfs_rq_async(struct cfs_rq *cfs_rq)
{
struct rq *rq = rq_of(cfs_rq);
bool first;
if (rq == this_rq()) {
unthrottle_cfs_rq(cfs_rq); // 本地直接执行
return;
}
/* Already enqueued */
if (SCHED_WARN_ON(!list_empty(&cfs_rq->throttled_csd_list)))
return;
first = list_empty(&rq->cfsb_csd_list);
list_add_tail(&cfs_rq->throttled_csd_list, &rq->cfsb_csd_list);
if (first)
smp_call_function_single_async(cpu_of(rq), &rq->cfsb_csd);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
设计要点:
- 如果目标 CPU 是当前 CPU,直接同步 unthrottle(避免不必要的 IPI)
- 否则,将 cfs_rq 加入目标 CPU 的 CSD 队列,通过
smp_call_function_single_async()发送 IPI - CSD 队列确保了同一 CPU 上多次 unthrottle 的合并(只有一个 IPI)
throttled_csd_list是 per-cfs_rq 的,因此一个 cfs_rq 不会被重复排队
distribute_cfs_runtime() 中使用 async unthrottle:
static bool distribute_cfs_runtime(struct cfs_bandwidth *cfs_b)
{
// ...
list_for_each_entry_rcu(cfs_rq, &cfs_b->throttled_cfs_rq,
throttled_list) {
// ...
rq_lock_irqsave(rq, &rf);
// ...
cfs_rq->runtime_remaining += runtime;
if (cfs_rq->runtime_remaining > 0) {
if (cpu_of(rq) != this_cpu)
unthrottle_cfs_rq_async(cfs_rq);
else
local_unthrottle = cfs_rq;
}
// ...
}
// 处理本地 unthrottle
if (local_unthrottle) {
rq_lock_irqsave(rq, &rf);
if (cfs_rq_throttled(local_unthrottle))
unthrottle_cfs_rq(local_unthrottle);
rq_unlock_irqrestore(rq, &rf);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
销毁时的保护:在 destroy_cfs_bandwidth() 中,如果还有 pending 的 CSD 工作(极其罕见的竞态),需要 flush:
static void destroy_cfs_bandwidth(struct cfs_bandwidth *cfs_b)
{
// ...
#ifdef CONFIG_SMP
for_each_possible_cpu(i) {
struct rq *rq = cpu_rq(i);
if (list_empty(&rq->cfsb_csd_list))
continue;
local_irq_save(flags);
__cfsb_csd_unthrottle(rq);
local_irq_restore(flags);
}
#endif
}2
3
4
5
6
7
8
9
10
11
12
13
14
设计解读
Async unthrottle 是对 per-CFS_RQ 模型的一个重大性能优化。它的核心洞察是:unthrottle 不需要严格同步进行。只要被 throttle 的 cfs_rq 获得了配额,它最终会被 unthrottle——适当延迟一点回被 throttle 的时间窗口,对整体公平性的影响可忽略不计,但避免了在定时器上下文中长时间持锁。
演进意义
Async unthrottle 是通往任务级 throttle 的重要中间步骤。它证明了"延迟恢复"的可行性,即throttle 状态的解除不需要在 lock-holding 上下文中同步完成。这个思想直接影响了 task-based throttle 模型的设计。
后续修复:ebb83d84e49b("sched/core: Avoid multiple calling update_rq_clock() in __cfsb_csd_unthrottle()")优化了 CSD 回调中的时钟更新。
阶段性总结
到 v6.3 为止,per-CFS_RQ throttle model 已经发展得相当成熟:
| 特性 | 状态 |
|---|---|
| 基本 throttle/unthrottle | v3.2 |
| PELT 时钟冻结 | v3.7 |
| 竞态修复 | v3.12~v5.7 |
| 公平的 list 遍历 | v4.19 |
| Burst 支持 | v5.14 |
| Async unthrottle | v6.3 |
然而,per-CFS_RQ 模型的核心缺陷仍然存在:整个队列的任务一起被停止。一个持有内核锁的任务被 throttle 时,等待该锁的所有其他任务(可能在别的 CPU 上)都会陷入阻塞,可能导致长时间的 hung task 甚至 lockup。这是社区中反复报告的问题,也是推动下一阶段架构变革的根本动力。