cgroup 限制cpu的使用和限制memoy类似,本质还是一个计数器+比较器
1. 相关参数:
参数:
cpu.max 格式:
例如:max 100000
- max:表示没有配额限制(即不限制 CPU 使用量)
- 100000:周期值,单位是微秒(μs),这里是 100,000 μs = 100 ms
例如:50000 100000
表示在每 100ms 周期内最多使用 50ms 的 CPU 时间(即 50%)
2. 代码原理
2.1 配置阶段
当用户态向/sys/fs/cgroup/xxx/cpu.max 和/sys/fs/cgroup/xxx/cpu.burst
写入值时,进入内核态:
c
|1|11487 {
|1|11488 .name = "max",
|1|11489 .flags = CFTYPE_NOT_ON_ROOT,
|1|11490 .seq_show = cpu_max_show,
|1|11491 .write = cpu_max_write,
|1|11492 },
|1|11493 {
|1|11494 .name = "max.burst",
|1|11495 .flags = CFTYPE_NOT_ON_ROOT,
|1|11496 .read_u64 = cpu_cfs_burst_read_u64,
|1|11497 .write_u64 = cpu_cfs_burst_write_u64,
|1|11498 },
最终调用tg_set_cfs_bandwidth 设置period,quota,burst。
c
s| |10858 static int tg_set_cfs_bandwidth(struct task_group *tg, u64 period, u64 quota,
i| |10859 u64 burst)
s| |10860 {
_| |10861 int i, ret = 0, runtime_enabled, runtime_was_enabled;
p| |10862 struct cfs_bandwidth *cfs_b = &tg->cfs_bandwidth;
r| |10863
_| |10864 if (tg == &root_task_group)
_| |10865 return -EINVAL;
_| |10866
n| |10867 /*
c| |10868 * Ensure we have at some amount of bandwidth every period. This is
a| |10869 * to prevent reaching a state of large arrears when throttled via
s| |10870 * entity_tick() resulting in prolonged exit starvation.
s| |10871 */
s| |10872 if (quota < min_cfs_quota_period || period < min_cfs_quota_period)
s| |10873 return -EINVAL;
s| |10874
s| |10875 /*
s| |10876 * Likewise, bound things on the other side by preventing insane quota
s| |10877 * periods. This also allows us to normalize in computing quota
s| |10878 * feasibility.
s| |10879 */
s| |10880 if (period > max_cfs_quota_period)
c| |10881 return -EINVAL;
c| |10882
c| |10883 /*
c| |10884 * Bound quota to defend quota against overflow during bandwidth shift.
c| |10885 */
c| |10886 if (quota != RUNTIME_INF && quota > max_cfs_runtime)
c| |10887 return -EINVAL;
c| |10888
c| |10889 if (quota != RUNTIME_INF && (burst > quota ||
c| |10890 burst + quota > max_cfs_runtime))
c| |10891 return -EINVAL;
c| |10892
c| |10893 /*
c| |10894 * Prevent race between setting of cfs_rq->runtime_enabled and
c| |10895 * unthrottle_offline_cfs_rqs().
c| |10896 */
c| |10897 guard(cpus_read_lock)();
t| |10898 guard(mutex)(&cfs_constraints_mutex);
t| |10899
t| |10900 ret = __cfs_schedulable(tg, period, quota);
t| |10901 if (ret)
t| |10902 return ret;
t| |10903
t| |10904 runtime_enabled = quota != RUNTIME_INF;
c| |10905 runtime_was_enabled = cfs_b->quota != RUNTIME_INF;
c| |10906 /*
c| |10907 * If we need to toggle cfs_bandwidth_used, off->on must occur
c| |10908 * before making related changes, and on->off must occur afterwards
c| |10909 */
c| |10910 if (runtime_enabled && !runtime_was_enabled)
n| |10911 cfs_bandwidth_usage_inc();
t| |10912
_| |10913 scoped_guard (raw_spinlock_irq, &cfs_b->lock) {
c| |10914 cfs_b->period = ns_to_ktime(period);
t| |10915 cfs_b->quota = quota;
c| |10916 cfs_b->burst = burst;
c| |10917
c| |10918 __refill_cfs_bandwidth_runtime(cfs_b);
c| |10919
c| |10920 /*
c| |10921 * Restart the period timer (if active) to handle new
c| |10922 * period expiry:
c| |10923 */
c| |10924 if (runtime_enabled)
c| |10925 start_cfs_bandwidth(cfs_b);
c| |10926 }
c| |10927
c| |10928 for_each_online_cpu(i) {
c| |10929 struct cfs_rq *cfs_rq = tg->cfs_rq[i];
c| |10930 struct rq *rq = cfs_rq->rq;
c| |10931
c| |10932 guard(rq_lock_irq)(rq);
d| |10933 cfs_rq->runtime_enabled = runtime_enabled;
c| |10934 cfs_rq->runtime_remaining = 0;
s| |10935
_| |10936 if (cfs_rq->throttled)
_| |10937 unthrottle_cfs_rq(cfs_rq);
s| |10938 }
s| |10939
s| |10940 if (runtime_was_enabled && !runtime_enabled)
s| |10941 cfs_bandwidth_usage_dec();
t| |10942
i| |10943 return 0;
t| |10944 }
这个函数注意干了两件事,__cfs_schedulable和__refill_cfs_bandwidth_runtime
__cfs_schedulable:
->walk_tg_tree->tg_cfs_schedulable_down
c
s| |11088 static int tg_cfs_schedulable_down(struct task_group *tg, void *data)
s| |11089 {
s| |11090 struct cfs_schedulable_data *d = data;
s| |11091 struct cfs_bandwidth *cfs_b = &tg->cfs_bandwidth;
s| |11092 s64 quota = 0, parent_quota = -1;
s| |11093
s| |11094 if (!tg->parent) {
s| |11095 quota = RUNTIME_INF;
c| |11096 } else {
c| |11097 struct cfs_bandwidth *parent_b = &tg->parent->cfs_bandwidth;
c| |11098
c| |11099 quota = normalize_cfs_quota(tg, d);
c| |11100 parent_quota = parent_b->hierarchical_quota;
c| |11101
c| |11102 /*
c| |11103 * Ensure max(child_quota) <= parent_quota. On cgroup2,
c| |11104 * always take the non-RUNTIME_INF min. On cgroup1, only
c| |11105 * inherit when no limit is set. In both cases this is used
c| |11106 * by the scheduler to determine if a given CFS task has a
c| |11107 * bandwidth constraint at some higher level.
c| |11108 */
c| |11109 if (cgroup_subsys_on_dfl(cpu_cgrp_subsys)) {
c| |11110 if (quota == RUNTIME_INF)
c| |11111 quota = parent_quota;
c| |11112 else if (parent_quota != RUNTIME_INF)
t| |11113 quota = min(quota, parent_quota);
t| |11114 } else {
t| |11115 if (quota == RUNTIME_INF)
t| |11116 quota = parent_quota;
t| |11117 else if (parent_quota != RUNTIME_INF && quota > parent_quota)
t| |11118 return -EINVAL;
t| |11119 }
c| |11120 }
cfs_b->hierarchical_quota = quota;
tg_cfs_schedulable_down 会调用normalize_cfs_quota计算一个比值, 比值= data->quota / date->period。
然后和父group的比值比较, 取最小记录到hierarchical_quota
解释:
- 父group 如果设置,period=10000,quota=5000, 那么比值就是0.5, 如果子group设置 period=20000,quota=15000,那么比值就是0.75, 0.75 大于0.5 ,所以取小值,子group的hierarchical_quota = 0.5
- Cgoup v2里hierarchical_quota已经没太多用处了,目前只有一个用处,就是组织调度器进入nohz。
如果hierarchical_quota 不等于RUNTIME_INF,那么cfs_task_bw_constrained 返回true,这导致sched_fair_update_stop_tick里会执行tick_nohz_dep_set_cpu(cpu, TICK_DEP_BIT_SCHED);,设置ts->tick_dep_mask会被置位, 导致can_stop_full_tick->check_tick_dependency时,会不允许进入NOHZ
。
tg_set_cfs_bandwidth接下来调用__refill_cfs_bandwidth_runtime真正记录传入的quota、period、burust。
c
5613 void __refill_cfs_bandwidth_runtime(struct cfs_bandwidth *cfs_b)
| | 5614 {
| | 5615 s64 runtime;
| | 5616
| | 5617 if (unlikely(cfs_b->quota == RUNTIME_INF))
f| | 5618 return;
| | 5619
| | 5620 cfs_b->runtime += cfs_b->quota;
| | 5621 runtime = cfs_b->runtime_snap - cfs_b->runtime;
| | 5622 if (runtime > 0) {
| | 5623 cfs_b->burst_time += runtime;
| | 5624 cfs_b->nr_burst++;
| | 5625 }
| | 5626
| | 5627 cfs_b->runtime = min(cfs_b->runtime, cfs_b->quota + cfs_b->burst);
| | 5628 cfs_b->runtime_snap = cfs_b->runtime;
| | 5629 }
| | 5630
这里主要是对burst的处理,来体现瞬时性:
5620行:cfs_b->runtime:代表着当前group剩余多少额度,因为上个周期可能额度没用完,数值大于0;
也可能数值小于0(时钟中断是离散的,如果你的配额在 10.1ms 时用完了,但下一个时钟中断在 11ms 才来,这 0.9ms 就是任务"偷跑"的时间,此情况下,数值为-0.9ms)
cfs_b->quota:本次用户态传入的quota;
cfs_b->runtime_snap:上一次group的quota备份。
那么这个函数的含义是:
- 没有 Burst 时: 每个周期开始时:runtime = quota。不管你上个周期省了多少,全部作废。
- 有 Burst 时: 每个周期开始时:runtime = quota + min(上个周期剩余量, burst)。
例子:
c
假设 quota = 100,burst = 50。
周期 A 开始:runtime = 100, runtime_snap = 100。
周期 A 结束:任务太闲,只用了 10。此时 runtime = 90。
周期 B 开始 (执行这段代码):
cfs_b->runtime += 100 -> 变为 190。
审计:100 (snap) - 190 = -90。不大于 0,说明没用突发流量。
封顶:min(190, 100 + 50) -> 变为 150。
结果:你省了 90,但由于 burst 限制,你只能带 50 到下个周期。
周期 B 运行:任务突然爆发,需要用 140。
周期 B 结束:runtime = 150 - 140 = 10。
周期 C 开始 (再次执行):
cfs_b->runtime += 100 -> 变为 110。
审计:150 (snap) - 110 = 40。大于 0!
触发:nr_burst++(记录了一次突发),burst_time += 40(记录了用了 40 突发额度)。
总结
这段代码非常精妙:它用 min 限制了"长期节省"的上限,从而允许了"短期爆发"。同时利用 runtime_snap 的落差,非常准确地捕捉到了任务的"爆发行为"。
从这三个场景,就能看出brust作用,上个周期用的多,这个周期就得少用点;上个周期用得少,这个周期就可以多用点,但是不能超过burst。
-
回到tg_set_cfs_bandwidth
10928~10938: 清空每个cpu上该group的私有quota: cfs_rq->runtime_remaining = 0;
注意:每个核上的运行队列 (cfs_rq) 在需要 CPU 时间时,会向这个全局池子"申请"一小块时间(默认是 5ms,由 sched_cfs_bandwidth_slice() 定义)并缓存在本地。 这样做的目的和cgroup对memory的管控一样,本地cpu缓存一部分,防止多核对全局变量进行修改,引起锁竞争。
-
目前已经讲解完,用户态写cgroup cpu.max和cpu.burst后,内核是如何处理的。
接下来讲一下如何限制cpu的使用率:
2.2 运行时消耗统计
CPU 限额是在任务运行过程中实时扣除的:
- update_curr():在每次时钟中断或任务切换时调用。
- account_cfs_rq_runtime():
- 它会计算任务运行的增量时间 delta_exec。
- 从当前 CPU 运行队列(cfs_rq)的本地配额 runtime_remaining 中减去该时间。
- 配额申请:如果本地配额耗尽(小于 0),则调用 assign_cfs_rq_runtime() 向全局 cfs_bandwidth 配额池申请新的"分片"(通常是 5ms)
代码原理:
.update_curr = update_curr_fair
update_curr_fair
->update_curr
->__update_curr
->account_cfs_rq_runtime
->__account_cfs_rq_runtime
__account_cfs_rq_runtime:
c
| 5677 static void __account_cfs_rq_runtime(struct cfs_rq *cfs_rq, u64 delta_exec)
| 5678 {
| 5679 /* dock delta_exec before expiring quota (as it could span periods) */
| 5680 cfs_rq->runtime_remaining -= delta_exec;
| 5681
| 5682 if (likely(cfs_rq->runtime_remaining > 0))
| 5683 return;
| 5684
| 5685 if (cfs_rq->throttled)
| 5686 return;
| 5687 /*
| 5688 * if we're unable to extend our runtime we resched so that the active
| 5689 * hierarchy can be throttled
| 5690 */
| 5691 if (!assign_cfs_rq_runtime(cfs_rq) && likely(cfs_rq->curr))
| 5692 resched_curr_lazy(rq_of(cfs_rq));
| 5693 }
5680行: 本cpu对于本group的剩余额度 减去 本次task的运行时间
5682行:如果额度还有,直接返回,如果额度没剩余了,执行下边的代码
5691~5692行:尝试向上级申请额度,申请成功则返回,申请不到就会挂起此task
assign_cfs_rq_runtime:
->__assign_cfs_rq_runtime
c
static int __assign_cfs_rq_runtime(struct cfs_bandwidth *cfs_b,
s| 5638 struct cfs_rq *cfs_rq, u64 target_runtime)
p| 5639 {
p| 5640 u64 min_amount, amount = 0;
e| 5641
c| 5642 lockdep_assert_held(&cfs_b->lock);
c| 5643
c| 5644 /* note: this is a positive sum as runtime_remaining <= 0 */
c| 5645 min_amount = target_runtime - cfs_rq->runtime_remaining;
c| 5646
c| 5647 if (cfs_b->quota == RUNTIME_INF)
d| 5648 amount = min_amount;
s| 5649 else {
_| 5650 start_cfs_bandwidth(cfs_b);
t| 5651
_| 5652 if (cfs_b->runtime > 0) {
a| 5653 amount = min(cfs_b->runtime, min_amount);
_| 5654 cfs_b->runtime -= amount;
a| 5655 cfs_b->idle = 0;
c| 5656 }
t| 5657 }
t| 5658
t| 5659 cfs_rq->runtime_remaining += amount;
t| 5660
t| 5661 return cfs_rq->runtime_remaining > 0;
u| 5662 }
这个函数逻辑比较简单,默认申请5ms的cpu执行时间,由sched_cfs_bandwidth_slice传入为target_runtime(sysctl可配置)
如果设置了不限额,直接返回
5645行:计算quota缺口
5647行:如果 cgroup 没有设置限额(RUNTIME_INF),总部就是"无限印钞机",要多少给多少
5650~5657行: 如何设置了限额,就要判断下总的额度还够吗
5659行:重新赋值新申请的额度
5661行: 如果申请到了,返回true, 没申请到返回false。
2.3 节流
额度不够时(runtime_remaining<0)有两种处理方法:
- 打标记(resched)
- 直接挂起(throttle)
- 不仅update_curr会调用__account_cfs_rq_runtime,判断runtime_remaining是否大于0, 调度器有多处调用__account_cfs_rq_runtime。
- update_curr调用__account_cfs_rq_runtime,当额度不够时,会reschedule, 但其他地方调用__account_cfs_rq_runtime,如果额度不够,会有更严重的惩罚(throttle)。
- pick_task_fair和enqueue_entity 最终都会调用throttle_cfs_rq,如果额度不够,会将cgroup内所有的task全部挂起throttle。
例子:
pick_next_task_fair
->check_cfs_rq_runtime
->throttle_cfs_rq
throttle_cfs_rq::
c
5780 static bool throttle_cfs_rq(struct cfs_rq *cfs_rq)
5781 {
5782 struct rq *rq = rq_of(cfs_rq);
5783 struct cfs_bandwidth *cfs_b = tg_cfs_bandwidth(cfs_rq->tg);
5784 struct sched_entity *se;
5785 long task_delta, idle_task_delta, dequeue = 1;
5786
5787 raw_spin_lock(&cfs_b->lock);
5788 /* This will start the period timer if necessary */
5789 if (__assign_cfs_rq_runtime(cfs_b, cfs_rq, 1)) {
5790 /*
5791 * We have raced with bandwidth becoming available, and if we
5792 * actually throttled the timer might not unthrottle us for an
5793 * entire period. We additionally needed to make sure that any
5794 * subsequent check_cfs_rq_runtime calls agree not to throttle
5795 * us, as we may commit to do cfs put_prev+pick_next, so we ask
5796 * for 1ns of runtime rather than just check cfs_b.
5797 */
5798 dequeue = 0;
5799 } else {
5800 list_add_tail_rcu(&cfs_rq->throttled_list,
5801 &cfs_b->throttled_cfs_rq);
5802 }
5803 raw_spin_unlock(&cfs_b->lock);
5804
5805 if (!dequeue)
5806 return false; /* Throttle no longer required. */
5807
5808 se = cfs_rq->tg->se[cpu_of(rq_of(cfs_rq))];
5809
5810 /* freeze hierarchy runnable averages while throttled */
5811 rcu_read_lock();
5812 walk_tg_tree_from(cfs_rq->tg, tg_throttle_down, tg_nop, (void *)rq);
5813 rcu_read_unlock();
5814
5815 task_delta = cfs_rq->h_nr_running;
5816 idle_task_delta = cfs_rq->idle_h_nr_running;
5817 for_each_sched_entity(se) {
5818 struct cfs_rq *qcfs_rq = cfs_rq_of(se);
5819 /* throttled entity or throttle-on-deactivate */
5820 if (!se->on_rq)
5821 goto done;
5822
5823 dequeue_entity(qcfs_rq, se, DEQUEUE_SLEEP);
5824
5825 if (cfs_rq_is_idle(group_cfs_rq(se)))
5826 idle_task_delta = cfs_rq->h_nr_running;
5827
5828 qcfs_rq->h_nr_running -= task_delta;
5829 qcfs_rq->idle_h_nr_running -= idle_task_delta;
5830
5831 if (qcfs_rq->load.weight) {
5832 /* Avoid re-evaluating load for this entity: */
5833 se = parent_entity(se);
5834 break;
5835 }
5836 }
5837
5838 for_each_sched_entity(se) {
5839 struct cfs_rq *qcfs_rq = cfs_rq_of(se);
5840 /* throttled entity or throttle-on-deactivate */
5841 if (!se->on_rq)
5842 goto done;
5843
5844 update_load_avg(qcfs_rq, se, 0);
5845 se_update_runnable(se);
5846
5847 if (cfs_rq_is_idle(group_cfs_rq(se)))
5848 idle_task_delta = cfs_rq->h_nr_running;
5849
5850 qcfs_rq->h_nr_running -= task_delta;
5851 qcfs_rq->idle_h_nr_running -= idle_task_delta;
5852 }
5853
5854 /* At this point se is NULL and we are at root level*/
5855 sub_nr_running(rq, task_delta);
5856
5857 done:
5858 /*
5859 * Note: distribution will already see us throttled via the
5860 * throttled-list. rq->lock protects completion.
5861 */
5862 cfs_rq->throttled = 1;
5863 SCHED_WARN_ON(cfs_rq->throttled_clock);
5864 if (cfs_rq->nr_running)
5865 cfs_rq->throttled_clock = rq_clock(rq);
5866 return true;
5867 }
5868
5789行:调用__assign_cfs_rq_runtime,尝试申请/判断是否有额度,有额度直接返回;没额度走下面代码。
5812: walk_tg_tree_from(cfs_rq->tg, tg_throttle_down, tg_nop, (void *)rq)
作用:更新它们的 throttle_count。这会冻结 PELT(Per-Entity Load Tracking)平均负载的计算
意义:既然被挂起了,这些任务就不应该贡献负载压力。如果不冻结,它们的负载值会随着时间衰减,导致恢复运行时调度器误以为它们很轻。
5817~5828:
遍历整个cgroup,调用 dequeue_entity 将该 cgroup 对应的 se 从父 cgroup 的红黑树中移除
- 为什么有两段 for_each_sched_entity?
第一段 (5817-5836):处理带权重的出队。如果父节点的 load.weight 为 0,说明父节点已经没有其他任务了,可以快速跳出。
第二段 (5838-5852):处理不带权重的统计更新。即便不再执行出队(因为父节点已经不在队列里了),依然需要更新更高层级的 h_nr_running 统计数据,确保 rq->nr_running 的全局准确性。
2.4 周期如何实现
之前的文章提到了,用户态如何设置周期和每个周期所能使用的cpu额度,以及周期内达到cpu额度峰值,会将task 出队或挂起resched。
现在讲如何实现周期:
- 周期(Period)"是 cgroup 带宽控制中最核心的时间基准。你可以把它理解为"配额重置的节拍"
cgroup 是如何每个周期更新的?- 内核通过一个高精度定时器 period_timer 来驱动周期的流转。整个过程像一个自动循环的注水系统。
- 第一步:定时器触发:
每个 task_group 都有一个属于自己的 cfs_bandwidth 结构,里面跑着一个 period_timer。当时间到达 period 设置的间隔时,中断触发,进入 sched_cfs_period_timer(),会调用__refill_cfs_bandwidth_runtime 为group重新配额度。 - 第二步:Unthrottle
调用unthrottle_cfs_rq,将之前超额导致出队的进程重新入队。