容器技术的核心在于资源隔离与限制,而这一能力的底层支撑正是 Linux 内核的 cgroup(Control Group)机制。本文基于 Linux 6.4-rc1 源码,深入剖析 cgroup v2 的内核实现,涵盖统一层次框架、Memory/CPU/IO/PID 四大控制器的核心数据结构与关键路径,以及 cgroup namespace 与诊断方法。
一、cgroup 框架:内核如何组织资源控制层次
1.1 核心数据结构
cgroup 框架由四个核心数据结构构成,它们共同描述”哪些进程受哪些控制器以何种限制管理”这一模型。
struct cgroup_subsys_state —— subsystem per-cgroup 状态基类
每个 subsystem(controller)在每个 cgroup 中的状态都以 struct cgroup_subsys_state(css)作为基类(include/linux/cgroup-defs.h,第 155 行):
1 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 27 28 29 30
| struct cgroup_subsys_state { struct cgroup *cgroup;
struct cgroup_subsys *ss;
struct percpu_ref refcnt;
struct list_head sibling; struct list_head children;
struct list_head rstat_css_node;
int id; unsigned int flags;
u64 serial_nr;
atomic_t online_cnt;
struct work_struct destroy_work; struct rcu_work destroy_rwork;
struct cgroup_subsys_state *parent; };
|
各 subsystem 的私有结构(如 struct mem_cgroup、struct pids_cgroup)均将 struct cgroup_subsys_state css 置于第一个字段,从而可通过 container_of() 互相转换。flags 字段携带 CSS_ONLINE、CSS_DYING 等生命周期标志;percpu_ref refcnt 采用 per-CPU 引用计数,在热路径上避免 cacheline 争用。
struct cgroup —— cgroup 节点
struct cgroup(第 378 行)描述 cgroupfs 中的一个目录节点:
1 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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| struct cgroup { struct cgroup_subsys_state self;
unsigned long flags; int level; int max_depth;
int nr_descendants; int nr_dying_descendants; int max_descendants;
struct kernfs_node *kn; struct cgroup_file procs_file; struct cgroup_file events_file;
struct cgroup_file psi_files[NR_PSI_RESOURCES];
u16 subtree_control; u16 subtree_ss_mask;
struct cgroup_subsys_state __rcu *subsys[CGROUP_SUBSYS_COUNT];
struct cgroup_root *root; struct list_head cset_links; struct list_head e_csets[CGROUP_SUBSYS_COUNT];
struct cgroup *dom_cgrp;
struct cgroup_rstat_cpu __percpu *rstat_cpu;
struct psi_group *psi; struct cgroup_bpf bpf;
struct cgroup_freezer_state freezer;
struct cgroup *ancestors[]; };
|
几个关键设计点:
self 是一个 css(ss == NULL),使得 cgroup 本身也可以当作 css 操作,统一了迭代接口。
subsys[CGROUP_SUBSYS_COUNT] 存放各 subsystem 的 css 指针,通过 ssid 索引。
ancestors[] 柔性数组存储从 root 到自身的所有祖先指针,O(1) 判断 descendant 关系。
psi_files[] 直接内嵌 PSI 接口文件句柄,支持 memory.pressure/cpu.pressure/io.pressure。
struct cgroup_subsys —— subsystem 描述符
struct cgroup_subsys(第 654 行)是每个 controller 向框架注册自身的入口:
1 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 27 28 29 30 31 32 33 34 35 36 37
| struct cgroup_subsys { struct cgroup_subsys_state *(*css_alloc)(struct cgroup_subsys_state *parent_css); int (*css_online)(struct cgroup_subsys_state *css); void (*css_offline)(struct cgroup_subsys_state *css); void (*css_released)(struct cgroup_subsys_state *css); void (*css_free)(struct cgroup_subsys_state *css); void (*css_reset)(struct cgroup_subsys_state *css); void (*css_rstat_flush)(struct cgroup_subsys_state *css, int cpu);
int (*can_attach)(struct cgroup_taskset *tset); void (*cancel_attach)(struct cgroup_taskset *tset); void (*attach)(struct cgroup_taskset *tset); void (*post_attach)(void); int (*can_fork)(struct task_struct *task, struct css_set *cset); void (*cancel_fork)(struct task_struct *task, struct css_set *cset); void (*fork)(struct task_struct *task); void (*exit)(struct task_struct *task); void (*release)(struct task_struct *task); void (*bind)(struct cgroup_subsys_state *root_css);
bool early_init:1; bool implicit_on_dfl:1; bool threaded:1;
int id; const char *name; const char *legacy_name;
struct cgroup_root *root; struct idr css_idr; struct list_head cfts;
struct cftype *dfl_cftypes; struct cftype *legacy_cftypes;
unsigned int depends_on; };
|
生命周期回调遵循严格顺序:css_alloc → css_online → css_offline → css_released → css_free。can_fork 在 copy_process() 持有 cgroup_threadgroup_rwsem 时调用,是 PID controller 阻止 fork 的注入点。
struct css_set —— 进程的 cgroup 关联
struct css_set(第 212 行)是进程与 cgroup 层次绑定关系的核心:
1 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 27 28 29 30 31 32 33 34
| struct css_set { struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
refcount_t refcount;
struct css_set *dom_cset;
struct cgroup *dfl_cgrp;
int nr_tasks;
struct list_head tasks; struct list_head mg_tasks; struct list_head dying_tasks;
struct list_head e_cset_node[CGROUP_SUBSYS_COUNT];
struct list_head threaded_csets; struct list_head threaded_csets_node;
struct hlist_node hlist; struct list_head cgrp_links;
struct cgroup *mg_src_cgrp; struct cgroup *mg_dst_cgrp; struct css_set *mg_dst_cset;
bool dead; struct rcu_head rcu_head; };
|
css_set 的设计哲学是共享:相同 cgroup 组合的进程共用同一个 css_set,通过 css_set_table 哈希表查找。进程结构体 task_struct 中的 cgroups 字段即指向此结构。fork 时通过引用计数复用,exec 不改变 cgroup 成员,只有显式写入 cgroup.procs 才会触发迁移。
1.2 cgroupfs 的挂载与初始化
cgroup v2 在内核中注册为独立文件系统(kernel/cgroup/cgroup.c,第 2304 行):
1 2 3 4 5 6 7
| static struct file_system_type cgroup2_fs_type = { .name = "cgroup2", .init_fs_context = cgroup_init_fs_context, .parameters = cgroup2_fs_parameters, .kill_sb = cgroup_kill_sb, .fs_flags = FS_USERNS_MOUNT, };
|
FS_USERNS_MOUNT 允许在用户命名空间内挂载,这是容器内非 root 用户操作 cgroup 的基础。
cgroup_init()(第 6070 行)在 boot 阶段建立默认层次(cgrp_dfl_root),注册所有 subsystem,并将 init_css_set 加入哈希表:
1 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 27 28 29 30 31 32
| int __init cgroup_init(void) { struct cgroup_subsys *ss; int ssid;
BUG_ON(cgroup_init_cftypes(NULL, cgroup_base_files)); BUG_ON(cgroup_init_cftypes(NULL, cgroup_psi_files));
cgroup_rstat_boot();
cgroup_lock(); hash_add(css_set_table, &init_css_set.hlist, css_set_hash(init_css_set.subsys));
BUG_ON(cgroup_setup_root(&cgrp_dfl_root, 0)); cgroup_unlock();
for_each_subsys(ss, ssid) { if (ss->early_init) { struct cgroup_subsys_state *css = init_css_set.subsys[ss->id]; css->id = cgroup_idr_alloc(&ss->css_idr, css, 1, 2, GFP_KERNEL); } else { cgroup_init_subsys(ss, false); } }
WARN_ON(register_filesystem(&cgroup_fs_type)); WARN_ON(register_filesystem(&cgroup2_fs_type)); return 0; }
|
二、Memory Controller:内存计费的精确管控
2.1 struct mem_cgroup
Memory controller 的核心结构定义在 include/linux/memcontrol.h(第 207 行):
1 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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| struct mem_cgroup { struct cgroup_subsys_state css;
struct mem_cgroup_id id;
struct page_counter memory;
union { struct page_counter swap; struct page_counter memsw; };
struct page_counter kmem; struct page_counter tcpmem;
struct work_struct high_work;
unsigned long soft_limit;
struct vmpressure vmpressure;
bool oom_group; bool oom_lock; int under_oom; int swappiness; int oom_kill_disable;
struct cgroup_file events_file; struct cgroup_file events_local_file; struct cgroup_file swap_events_file;
struct mutex thresholds_lock; struct mem_cgroup_thresholds thresholds; struct mem_cgroup_thresholds memsw_thresholds;
struct list_head oom_notify;
struct memcg_vmstats *vmstats;
atomic_long_t memory_events[MEMCG_NR_MEMORY_EVENTS]; atomic_long_t memory_events_local[MEMCG_NR_MEMORY_EVENTS];
unsigned long socket_pressure;
struct mem_cgroup_per_node *nodeinfo[]; };
|
page_counter 是内存计量的核心原语,内含当前值、高水位(high)、最大值(max)三个阈值以及超出事件计数。vmstats 通过 per-CPU 计数器聚合统计,读取时惰性向上传播,避免高频路径的同步开销。
2.2 内存计费路径:mem_cgroup_charge
页面分配时的计费入口是 __mem_cgroup_charge()(mm/memcontrol.c,第 7036 行):
1 2 3 4 5 6 7 8 9 10
| int __mem_cgroup_charge(struct folio *folio, struct mm_struct *mm, gfp_t gfp) { struct mem_cgroup *memcg; int ret;
memcg = get_mem_cgroup_from_mm(mm); ret = charge_memcg(folio, memcg, gfp); css_put(&memcg->css); return ret; }
|
charge_memcg() 完成实际的三步操作(第 7015 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| static int charge_memcg(struct folio *folio, struct mem_cgroup *memcg, gfp_t gfp) { long nr_pages = folio_nr_pages(folio); int ret;
ret = try_charge(memcg, gfp, nr_pages); if (ret) goto out;
css_get(&memcg->css); commit_charge(folio, memcg);
local_irq_disable(); mem_cgroup_charge_statistics(memcg, nr_pages); memcg_check_events(memcg, folio_nid(folio)); local_irq_enable(); out: return ret; }
|
try_charge() 最终调用 try_charge_memcg(),该函数实现了批量记账(默认 MEMCG_CHARGE_BATCH = 64 页),先检查 memory.high 是否超出,再检查 memory.max,并在必要时触发页面回收或 OOM。
2.3 超限处理:memory.high 限速
memory.high 是软限制:超出后不立即 OOM,而是在进程从内核返回用户空间时施加惩罚性延迟。核心函数 mem_cgroup_handle_over_high()(第 2591 行):
1 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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| void mem_cgroup_handle_over_high(void) { unsigned long penalty_jiffies; unsigned long nr_reclaimed; unsigned int nr_pages = current->memcg_nr_pages_over_high; int nr_retries = MAX_RECLAIM_RETRIES; struct mem_cgroup *memcg; bool in_retry = false;
if (likely(!nr_pages)) return;
memcg = get_mem_cgroup_from_mm(current->mm); current->memcg_nr_pages_over_high = 0;
retry_reclaim: nr_reclaimed = reclaim_high(memcg, in_retry ? SWAP_CLUSTER_MAX : nr_pages, GFP_KERNEL);
penalty_jiffies = calculate_high_delay(memcg, nr_pages, mem_find_max_overage(memcg)); penalty_jiffies += calculate_high_delay(memcg, nr_pages, swap_find_max_overage(memcg)); penalty_jiffies = min(penalty_jiffies, MEMCG_MAX_HIGH_DELAY_JIFFIES);
if (penalty_jiffies <= HZ / 100) goto out;
if (nr_reclaimed || nr_retries--) { in_retry = true; goto retry_reclaim; }
psi_memstall_enter(&pflags); schedule_timeout_killable(penalty_jiffies); psi_memstall_leave(&pflags); out: css_put(&memcg->css); }
|
该设计保证超限进程不会无限期阻塞(schedule_timeout_killable + TASK_KILLABLE),同时通过 PSI 接口将内存停顿信息暴露给用户空间。
2.4 超限处理:memory.max 触发 OOM
当 memory.max 无法通过回收满足时,调用 mem_cgroup_oom()(第 1934 行):
1 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 27 28 29 30 31
| static bool mem_cgroup_oom(struct mem_cgroup *memcg, gfp_t mask, int order) { bool locked, ret;
if (order > PAGE_ALLOC_COSTLY_ORDER) return false;
memcg_memory_event(memcg, MEMCG_OOM);
if (READ_ONCE(memcg->oom_kill_disable)) { if (current->in_user_fault) { css_get(&memcg->css); current->memcg_in_oom = memcg; } return false; }
mem_cgroup_mark_under_oom(memcg); locked = mem_cgroup_oom_trylock(memcg);
if (locked) mem_cgroup_oom_notify(memcg);
mem_cgroup_unmark_under_oom(memcg); ret = mem_cgroup_out_of_memory(memcg, mask, order);
if (locked) mem_cgroup_oom_unlock(memcg); return ret; }
|
v2 中 OOM killer 默认启用,oom_kill_disable 在 v2 hierarchy 下无效。OOM 发生时会记录 MEMCG_OOM 事件到 memory.events,并通知通过 memory.oom.group 配置的受害者范围。
2.5 memory.stat 字段解析
memory.stat 的统计项在 memory_stats[] 数组中定义(第 1510 行),关键字段含义如下:
| 字段 |
来源 |
含义 |
anon |
NR_ANON_MAPPED |
匿名映射页(堆、栈、mmap private) |
file |
NR_FILE_PAGES |
page cache 中的文件页 |
kernel |
MEMCG_KMEM |
slab 及其他内核内存 |
kernel_stack |
NR_KERNEL_STACK_KB |
内核栈(单位 KB) |
pagetables |
NR_PAGETABLE |
页表自身占用 |
sock |
MEMCG_SOCK |
socket buffer |
shmem |
NR_SHMEM |
tmpfs/共享内存 |
file_mapped |
NR_FILE_MAPPED |
被 mmap 映射的文件页 |
slab_reclaimable |
NR_SLAB_RECLAIMABLE_B |
可回收 slab(字节) |
slab_unreclaimable |
NR_SLAB_UNRECLAIMABLE_B |
不可回收 slab(字节) |
inactive_anon / active_anon |
LRU 列表 |
匿名页冷热分层 |
inactive_file / active_file |
LRU 列表 |
文件页冷热分层 |
三、CPU Controller:调度带宽的精确管控
3.1 struct task_group 与 struct cfs_bandwidth
CPU controller 在调度子系统中的载体是 struct task_group(kernel/sched/sched.h,第 369 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| struct task_group { struct cgroup_subsys_state css;
struct sched_entity **se; struct cfs_rq **cfs_rq; unsigned long shares; int idle;
atomic_long_t load_avg ____cacheline_aligned;
struct rcu_head rcu; struct list_head list;
struct task_group *parent; struct list_head siblings; struct list_head children;
struct cfs_bandwidth cfs_bandwidth; };
|
带宽管理结构 struct cfs_bandwidth(第 342 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| struct cfs_bandwidth { raw_spinlock_t lock; ktime_t period; u64 quota; u64 runtime; u64 burst; u64 runtime_snap; s64 hierarchical_quota;
u8 idle; u8 period_active; u8 slack_started; struct hrtimer period_timer; struct hrtimer slack_timer; struct list_head throttled_cfs_rq;
int nr_periods; int nr_throttled; int nr_burst; u64 throttled_time; u64 burst_time; };
|
cpu.weight:写入 1–10000(默认 100),映射为 CFS shares,影响 task_group.shares,决定竞争 CPU 时的相对份额。
cpu.max:格式 quota period,如 50000 100000 表示每 100ms 只能使用 50ms,即 50% CPU 利用率上限。quota = max_uint64 时表示无限制。
3.2 配额切片分配
每个 CPU 本地的 cfs_rq 在需要 runtime 时从全局 cfs_bandwidth 申领一个切片(kernel/sched/fair.c,第 5236 行):
1 2 3 4 5
| static inline u64 sched_cfs_bandwidth_slice(void) { return (u64)sysctl_sched_cfs_bandwidth_slice * NSEC_PER_USEC; }
|
每次 cfs_rq 需要更多 runtime 时,调用 __assign_cfs_rq_runtime() 从全局池最多取 sched_cfs_bandwidth_slice() 纳秒。这个设计平衡了”全局配额不超支”与”避免每次调度都加全局锁”之间的矛盾。
__refill_cfs_bandwidth_runtime() 在每个 period 到期时由 hrtimer 回调触发(第 5248 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void __refill_cfs_bandwidth_runtime(struct cfs_bandwidth *cfs_b) { s64 runtime;
if (unlikely(cfs_b->quota == RUNTIME_INF)) return;
cfs_b->runtime += cfs_b->quota; runtime = cfs_b->runtime_snap - cfs_b->runtime; if (runtime > 0) { cfs_b->burst_time += runtime; cfs_b->nr_burst++; } cfs_b->runtime = min(cfs_b->runtime, cfs_b->quota + cfs_b->burst); cfs_b->runtime_snap = cfs_b->runtime; }
|
3.3 节流与解除节流
当 cfs_rq->runtime_remaining 耗尽时,throttle_cfs_rq()(第 5400 行)将该 cfs_rq 从父运行队列摘除:
1 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 27 28 29
| static bool throttle_cfs_rq(struct cfs_rq *cfs_rq) { struct rq *rq = rq_of(cfs_rq); struct cfs_bandwidth *cfs_b = tg_cfs_bandwidth(cfs_rq->tg);
raw_spin_lock(&cfs_b->lock); if (__assign_cfs_rq_runtime(cfs_b, cfs_rq, 1)) { dequeue = 0; } else { list_add_tail_rcu(&cfs_rq->throttled_list, &cfs_b->throttled_cfs_rq); } raw_spin_unlock(&cfs_b->lock);
if (!dequeue) return false;
walk_tg_tree_from(cfs_rq->tg, tg_throttle_down, tg_nop, (void *)rq);
for_each_sched_entity(se) { dequeue_entity(qcfs_rq, se, DEQUEUE_SLEEP); } }
|
当下一个 period 的 hrtimer 触发、全局 runtime 补充后,unthrottle_cfs_rq() 遍历 cfs_b->throttled_cfs_rq 链表,将所有节流的 cfs_rq 重新 enqueue 入父运行队列(第 5487 行)。
节流期间被阻塞的统计累计到 cfs_b->throttled_time,最终体现在 cpu.stat 文件的 throttled_usec 字段。
四、IO Controller(blkcg):块设备 IO 的分层调度
4.1 struct blkcg 与 struct blkcg_gq
IO controller 的架构是双层结构:per-cgroup 的 blkcg 和 per-(cgroup, queue) 的 blkcg_gq(block/blk-cgroup.h,第 55、93 行):
1 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 27 28 29 30 31 32 33 34 35 36 37 38 39
| struct blkcg_gq { struct request_queue *q; struct list_head q_node; struct hlist_node blkcg_node; struct blkcg *blkcg;
struct blkcg_gq *parent;
struct percpu_ref refcnt; bool online;
struct blkg_iostat_set __percpu *iostat_cpu; struct blkg_iostat_set iostat;
struct blkg_policy_data *pd[BLKCG_MAX_POLS];
atomic_t use_delay; atomic64_t delay_nsec;
struct rcu_head rcu_head; };
struct blkcg { struct cgroup_subsys_state css; spinlock_t lock; refcount_t online_pin;
struct radix_tree_root blkg_tree; struct blkcg_gq __rcu *blkg_hint; struct hlist_head blkg_list;
struct blkcg_policy_data *cpd[BLKCG_MAX_POLS];
struct list_head all_blkcgs_node;
struct llist_head __percpu *lhead; };
|
blkcg_gq 是 IO controller 最核心的数据结构:每当一个 cgroup 的进程向某个块设备发出 IO 时,内核查找(或创建)对应的 blkg,所有 IO 统计和策略数据都附加在这里。iostat_cpu 使用 per-CPU 计数器追踪,通过 cgroup_rstat_flush 惰性聚合到 iostat。
4.2 io.stat 统计读取
io.stat 的读取函数 blkcg_print_stat() 迭代该 cgroup 下的所有 blkg,调用 blkcg_print_one_stat()(第 1052 行):
1 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 27 28 29 30 31
| static void blkcg_print_one_stat(struct blkcg_gq *blkg, struct seq_file *s) { struct blkg_iostat_set *bis = &blkg->iostat; u64 rbytes, wbytes, rios, wios, dbytes, dios; unsigned seq;
seq_printf(s, "%s ", blkg_dev_name(blkg));
do { seq = u64_stats_fetch_begin(&bis->sync); rbytes = bis->cur.bytes[BLKG_IOSTAT_READ]; wbytes = bis->cur.bytes[BLKG_IOSTAT_WRITE]; dbytes = bis->cur.bytes[BLKG_IOSTAT_DISCARD]; rios = bis->cur.ios[BLKG_IOSTAT_READ]; wios = bis->cur.ios[BLKG_IOSTAT_WRITE]; dios = bis->cur.ios[BLKG_IOSTAT_DISCARD]; } while (u64_stats_fetch_retry(&bis->sync, seq));
if (rbytes || wbytes || rios || wios) { seq_printf(s, "rbytes=%llu wbytes=%llu rios=%llu wios=%llu " "dbytes=%llu dios=%llu", rbytes, wbytes, rios, wios, dbytes, dios); } for (i = 0; i < BLKCG_MAX_POLS; i++) { if (blkg->pd[i] && blkcg_policy[i]->pd_stat_fn) blkcg_policy[i]->pd_stat_fn(blkg->pd[i], s); } seq_puts(s, "\n"); }
|
io.weight 和 io.max 分别由 BFQ(Budget Fair Queue)和 throttle policy 实现,各自在 blkg_policy_data 中维护私有状态,通过 pd_stat_fn 回调输出统计。
五、PID Controller:进程数量的硬边界
5.1 struct pids_cgroup
PID controller 是所有 controller 中逻辑最为清晰的(kernel/cgroup/pids.c,第 41 行):
1 2 3 4 5 6 7 8 9 10 11 12
| struct pids_cgroup { struct cgroup_subsys_state css;
atomic64_t counter; atomic64_t limit; int64_t watermark;
struct cgroup_file events_file;
atomic64_t events_limit; };
|
5.2 分层计费:pids_try_charge
PID 计费采用分层原子操作——从当前 cgroup 向上遍历直到 root,逐层累加并检查 limit(第 158 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| static int pids_try_charge(struct pids_cgroup *pids, int num) { struct pids_cgroup *p, *q;
for (p = pids; parent_pids(p); p = parent_pids(p)) { int64_t new = atomic64_add_return(num, &p->counter); int64_t limit = atomic64_read(&p->limit);
if (new > limit) goto revert;
pids_update_watermark(p, new); } return 0;
revert: for (q = pids; q != p; q = parent_pids(q)) pids_cancel(q, num); pids_cancel(p, num);
return -EAGAIN; }
|
5.3 fork 时的检查:pids_can_fork
pids_can_fork() 通过 cgroup_subsys.can_fork 回调注入 copy_process()(第 238 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| static int pids_can_fork(struct task_struct *task, struct css_set *cset) { struct cgroup_subsys_state *css; struct pids_cgroup *pids; int err;
if (cset) css = cset->subsys[pids_cgrp_id]; else css = task_css_check(current, pids_cgrp_id, true);
pids = css_pids(css); err = pids_try_charge(pids, 1); if (err) { if (atomic64_inc_return(&pids->events_limit) == 1) { pr_info("cgroup: fork rejected by pids controller in "); pr_cont_cgroup_path(css->cgroup); pr_cont("\n"); } cgroup_file_notify(&pids->events_file); } return err; }
|
PID controller 注册为 threaded = true(第 386 行),意味着它在 v2 threaded 模式下也能工作,以线程为粒度计数。
六、namespace 与 cgroup 的关系
6.1 struct nsproxy:命名空间的集合
每个进程通过 task_struct.nsproxy 指向其所属的命名空间集合(include/linux/nsproxy.h,第 31 行):
1 2 3 4 5 6 7 8 9 10 11
| struct nsproxy { atomic_t count; struct uts_namespace *uts_ns; struct ipc_namespace *ipc_ns; struct mnt_namespace *mnt_ns; struct pid_namespace *pid_ns_for_children; struct net *net_ns; struct time_namespace *time_ns; struct time_namespace *time_ns_for_children; struct cgroup_namespace *cgroup_ns; };
|
注意 pid_ns 是通过 task_active_pid_ns() 访问,而非直接从 nsproxy 取——这是历史遗留设计。nsproxy 被共享:相同命名空间集合的进程指向同一 nsproxy,任意命名空间改变时才 copy-on-write。
6.2 cgroup namespace 的核心机制
cgroup namespace 解决了”容器内进程看到的 cgroup 路径应以自身根为基准”这一隔离需求。copy_cgroup_ns()(kernel/cgroup/namespace.c,第 50 行):
1 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 27 28 29 30
| struct cgroup_namespace *copy_cgroup_ns(unsigned long flags, struct user_namespace *user_ns, struct cgroup_namespace *old_ns) { struct cgroup_namespace *new_ns; struct ucounts *ucounts; struct css_set *cset;
if (!(flags & CLONE_NEWCGROUP)) { get_cgroup_ns(old_ns); return old_ns; }
if (!ns_capable(user_ns, CAP_SYS_ADMIN)) return ERR_PTR(-EPERM);
ucounts = inc_cgroup_namespaces(user_ns);
spin_lock_irq(&css_set_lock); cset = task_css_set(current); get_css_set(cset); spin_unlock_irq(&css_set_lock);
new_ns = alloc_cgroup_ns(); new_ns->user_ns = get_user_ns(user_ns); new_ns->ucounts = ucounts; new_ns->root_cset = cset;
return new_ns; }
|
new_ns->root_cset 是关键:它记录了调用 unshare(CLONE_NEWCGROUP) 时进程所在的 css_set,此后在该 namespace 内查看 /proc/self/cgroup 路径时,内核将以 root_cset 对应的 cgroup 作为 /,从而隐藏父层次。这确保了:容器内进程无法看到或修改 cgroup 层次中位于自身根之上的节点。
七、cgroup v1 vs v2 的核心差异
7.1 统一层次(Unified Hierarchy)
v1 允许将不同 controller 挂载到不同独立层次,导致同一进程可以在 memory 层次和 cpu 层次中处于不同的组织结构,管理极为复杂。v2 强制单一统一层次:所有 controller 共享一棵 cgroup 树,进程在所有 controller 中的层次关系始终一致。
| 特性 |
v1 |
v2 |
| 层次数量 |
多个(每个 controller 独立) |
一个(统一层次) |
| cgroup.procs |
per-hierarchy |
全局唯一 |
| 线程控制 |
cgroup.tasks |
cgroup.threads(threaded 模式) |
| 资源保护 |
无标准化机制 |
memory.min/memory.low delegation |
| 压力接口 |
无 |
PSI(*.pressure 文件) |
| 权限委托 |
复杂,不安全 |
通过 cgroup.subtree_control 显式委托 |
7.2 cgroup.threads vs cgroup.procs
v2 默认以进程为粒度(cgroup.procs):同一进程的所有线程必须在同一 cgroup。这与”资源以进程为单位”的语义一致。
cgroup.threads 是 threaded 模式下的接口:在 threaded subtree 中,可以将单独线程迁移到子 cgroup,此时被配置为 threaded = true 的 controller(如 cpu、pids)以线程粒度计量,而 domain 级 controller(如 memory)仍以 domain 祖先为计量单位。
7.3 资源保护与 delegation
v2 通过 memory.min(保证)和 memory.low(尽力保护)提供了标准化的资源保护语义,这是 v1 所没有的。cgroup.subtree_control 文件控制哪些 controller 对子 cgroup 可见,结合 CGRP_ROOT_NS_DELEGATE 标志,可以安全地将 cgroup 管理权下放给非特权用户(如容器运行时)。
v2 cgroup 节点内嵌了 psi_files[],提供 memory.pressure、cpu.pressure、io.pressure 接口。每个文件输出三个窗口(10s/60s/300s)的压力比例,格式如:
1 2
| some avg10=0.00 avg60=0.00 avg300=0.00 total=0 full avg10=0.00 avg60=0.00 avg300=0.00 total=0
|
some 表示至少有一个任务因该资源停顿,full 表示所有可运行任务都停顿。这两个维度可以区分”有竞争”与”完全饥饿”。
八、诊断方法
8.1 层次结构查看
1 2 3 4 5
| systemd-cgls
systemd-cgtop
|
8.2 内存统计
1 2 3 4 5 6 7 8
| cat /sys/fs/cgroup/<group>/memory.stat
cat /sys/fs/cgroup/<group>/memory.current
cat /sys/fs/cgroup/<group>/memory.events
|
8.3 CPU 节流诊断
1 2
| cat /sys/fs/cgroup/<group>/cpu.stat
|
关键指标:**节流比率 = throttled_usec / usage_usec**。若该比率持续高于 5%,说明 cpu.max 配置过低,进程频繁被节流。容器中常见的 CPU throttling 排查步骤:
1 2 3
| cat /sys/fs/cgroup/system.slice/docker-<id>.scope/cpu.stat
|
8.4 IO 统计
1 2 3 4
| cat /sys/fs/cgroup/<group>/io.stat
|
8.5 PSI 压力监控
1 2 3 4 5 6 7 8
| cat /proc/pressure/memory
cat /sys/fs/cgroup/<group>/memory.pressure
cat /sys/fs/cgroup/<group>/cpu.pressure
|
PSI 的 total 字段(累计微秒)适合告警:当单位时间内 total 增量超过阈值时触发扩容或限流。
8.6 bpftrace 动态追踪
追踪内存计费路径(需要 root 权限):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| bpftrace -e ' kprobe:__mem_cgroup_charge { $folio = (struct folio *)arg0; printf("charge: comm=%s pages=%d\n", comm, $folio->_folio_nr_pages); }'
bpftrace -e ' kretprobe:pids_can_fork /retval != 0/ { printf("fork rejected: comm=%s pid=%d\n", comm, pid); }'
bpftrace -e ' kprobe:throttle_cfs_rq { @throttle_count[comm] = count(); }'
|
总结
cgroup v2 在内核实现层面构建了一套高度正交的资源管控体系:
- 框架层:
cgroup_subsys_state 作为基类、css_set 作为进程绑定快照、cgroup_subsys 的回调机制,三者共同构成可扩展的 controller 插件框架。
- Memory controller:以
page_counter 为计量单位,分层计费;memory.high 用惩罚延迟实现软限制,memory.max 触发 OOM 实现硬限制,两者共同覆盖”弹性保护”与”绝对边界”两种需求。
- CPU controller:
cfs_bandwidth 的 period/quota 机制通过 hrtimer 精确控制时间切片,per-CPU 本地池设计将全局锁争用降到最低,throttle_cfs_rq/unthrottle_cfs_rq 实现了毫秒级的细粒度节流。
- IO controller:
blkcg/blkcg_gq 双层结构适配多设备场景,per-CPU iostat 配合 rstat 惰性聚合使 IO 统计开销接近零。
- PID controller:最轻量的 controller,分层原子操作保证层次语义的同时将 fork 路径的开销控制在 O(depth)。
- cgroup namespace:以
root_cset 固定 namespace 创建时刻的视角,实现了容器内 cgroup 路径的虚拟化,是容器安全隔离的重要一环。
理解这些机制的内核实现,不仅有助于精准配置容器资源,更能在出现 OOM、CPU throttling、IO 积压等问题时快速定位根因。
源码参考(Linux 6.4-rc1)
include/linux/cgroup-defs.h
include/linux/memcontrol.h
include/linux/nsproxy.h
kernel/cgroup/cgroup.c
kernel/cgroup/pids.c
kernel/cgroup/namespace.c
kernel/sched/sched.h(struct task_group、struct cfs_bandwidth)
kernel/sched/fair.c(throttle_cfs_rq、unthrottle_cfs_rq、sched_cfs_bandwidth_slice)
mm/memcontrol.c(mem_cgroup_handle_over_high、mem_cgroup_oom、charge_memcg)
block/blk-cgroup.h(struct blkcg、struct blkcg_gq)
block/blk-cgroup.c(blkcg_print_stat)