Linux 进程管理深度剖析(四):cgroup v2 资源控制内核实现

容器技术的核心在于资源隔离与限制,而这一能力的底层支撑正是 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 {
/* PI: the cgroup that this css is attached to */
struct cgroup *cgroup;

/* PI: the cgroup subsystem that this css is attached to */
struct cgroup_subsys *ss;

/* reference count - access via css_[try]get() and css_put() */
struct percpu_ref refcnt;

/* siblings list anchored at the parent's ->children */
struct list_head sibling;
struct list_head children;

/* flush target list anchored at cgrp->rstat_css_list */
struct list_head rstat_css_node;

int id;
unsigned int flags;

u64 serial_nr;

/* Incremented by online self and children. */
atomic_t online_cnt;

struct work_struct destroy_work;
struct rcu_work destroy_rwork;

struct cgroup_subsys_state *parent;
};

各 subsystem 的私有结构(如 struct mem_cgroupstruct pids_cgroup)均将 struct cgroup_subsys_state css 置于第一个字段,从而可通过 container_of() 互相转换。flags 字段携带 CSS_ONLINECSS_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 {
/* self css with NULL ->ss, points back to this cgroup */
struct cgroup_subsys_state self;

unsigned long flags; /* CGRP_FREEZE, CGRP_KILL 等 */
int level; /* 在层次中的深度,root = 0 */
int max_depth;

int nr_descendants;
int nr_dying_descendants;
int max_descendants;

struct kernfs_node *kn; /* cgroupfs 目录节点 */
struct cgroup_file procs_file; /* cgroup.procs 文件句柄 */
struct cgroup_file events_file; /* cgroup.events 文件句柄 */

/* handles for "{cpu,memory,io,irq}.pressure" */
struct cgroup_file psi_files[NR_PSI_RESOURCES];

u16 subtree_control; /* 已通过 cgroup.subtree_control 启用的 subsystem 位掩码 */
u16 subtree_ss_mask; /* 实际生效的 subsystem 位掩码 */

/* Private pointers for each registered subsystem */
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; /* threaded 模式中指向 domain 祖先 */

/* per-cpu recursive resource statistics */
struct cgroup_rstat_cpu __percpu *rstat_cpu;

struct psi_group *psi;
struct cgroup_bpf bpf;

struct cgroup_freezer_state freezer;

/* All ancestors including self */
struct cgroup *ancestors[];
};

几个关键设计点:

  • self 是一个 cssss == 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; /* v2 hierarchy 的控制文件 */
struct cftype *legacy_cftypes; /* v1 hierarchy 的控制文件 */

unsigned int depends_on;
};

生命周期回调遵循严格顺序:css_alloccss_onlinecss_offlinecss_releasedcss_freecan_forkcopy_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 {
/* 每个 subsystem 一个 css,数组不可变(创建后) */
struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];

refcount_t refcount;

/* threaded 模式:指向最近 domain 祖先的 cset */
struct css_set *dom_cset;

/* default hierarchy 关联的 cgroup */
struct cgroup *dfl_cgrp;

int nr_tasks;

struct list_head tasks; /* 使用此 cset 的所有任务链表 */
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; /* css_set_table 哈希链 */
struct list_head cgrp_links;

/* migration context */
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);
}
/* ... 注册 cftype 并设置 subsys_mask ... */
}

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;

/* Private memcg ID,用于标识生命周期超过 cgroup 的对象 */
struct mem_cgroup_id id;

/* 计量资源:Both v1 & v2 */
struct page_counter memory;

union {
struct page_counter swap; /* v2 only */
struct page_counter memsw; /* v1 only */
};

struct page_counter kmem; /* v1 only */
struct page_counter tcpmem; /* v1 only */

/* Range enforcement for interrupt charges */
struct work_struct high_work;

unsigned long soft_limit;

/* vmpressure 通知 */
struct vmpressure vmpressure;

bool oom_group; /* OOM 时是否杀死整个 cgroup */
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;

/* memory.stat 统计 */
struct memcg_vmstats *vmstats;

/* memory.events */
atomic_long_t memory_events[MEMCG_NR_MEMORY_EVENTS];
atomic_long_t memory_events_local[MEMCG_NR_MEMORY_EVENTS];

unsigned long socket_pressure;

/* per-node 信息 */
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); /* 1. 尝试向 page_counter 记账 */
if (ret)
goto out;

css_get(&memcg->css); /* 2. 持有 css 引用 */
commit_charge(folio, memcg); /* 3. 将 folio->memcg_data 指向 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:
/* 先尝试回收:优先 reclaim 而非 throttle */
nr_reclaimed = reclaim_high(memcg,
in_retry ? SWAP_CLUSTER_MAX : nr_pages,
GFP_KERNEL);

/* 计算惩罚 jiffies,随超出量非线性增长 */
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;

/* reclaim 有进展则继续重试,而非 throttle */
if (nr_reclaimed || nr_retries--) {
in_retry = true;
goto retry_reclaim;
}

/* 进入可杀死的 sleep,配合 PSI 记录内存压力 */
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)) {
/* v1 兼容:延迟到 page fault 返回时处理 */
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); /* 通知 eventfd 监听者 */

mem_cgroup_unmark_under_oom(memcg);
ret = mem_cgroup_out_of_memory(memcg, mask, order); /* 选择受害者并 kill */

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_groupkernel/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;

/* per-CPU CFS 调度实体和运行队列 */
struct sched_entity **se;
struct cfs_rq **cfs_rq;
unsigned long shares; /* cpu.weight 映射的权重值 */
int idle; /* SCHED_IDLE group */

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; /* 配额周期,默认 100ms */
u64 quota; /* 每周期可用 CPU 时间(纳秒) */
u64 runtime; /* 当前周期剩余配额 */
u64 burst; /* 允许的突发量 */
u64 runtime_snap;
s64 hierarchical_quota;

u8 idle;
u8 period_active;
u8 slack_started;
struct hrtimer period_timer; /* 每个 period 结束时补充 runtime */
struct hrtimer slack_timer; /* 收集 CPU 本地池归还的剩余 runtime */
struct list_head throttled_cfs_rq; /* 节流中的 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;
/* 默认 sysctl_sched_cfs_bandwidth_slice = 5000 us,即 5ms */
}

每次 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++;
}
/* burst 限制:不能超过 quota + 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);
/* 若此时恰好 bandwidth 刚恢复,则不需要节流 */
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);

/* 从父 cfs_rq 中 dequeue 调度实体 */
for_each_sched_entity(se) {
dequeue_entity(qcfs_rq, se, DEQUEUE_SLEEP);
/* ... 更新 h_nr_running ... */
}
/* ... */
}

当下一个 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_gqblock/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
/* per-(cgroup, request_queue) 的关联数据 */
struct blkcg_gq {
struct request_queue *q; /* 关联的块设备队列 */
struct list_head q_node;
struct hlist_node blkcg_node;
struct blkcg *blkcg;

struct blkcg_gq *parent; /* 父 blkg */

struct percpu_ref refcnt;
bool online;

struct blkg_iostat_set __percpu *iostat_cpu; /* per-CPU IO 统计 */
struct blkg_iostat_set iostat; /* 聚合后的 IO 统计 */

struct blkg_policy_data *pd[BLKCG_MAX_POLS]; /* 各 policy 的私有数据 */

atomic_t use_delay;
atomic64_t delay_nsec; /* IO 延迟注入(io.latency 使用) */

struct rcu_head rcu_head;
};

/* per-cgroup 的 blkcg */
struct blkcg {
struct cgroup_subsys_state css;
spinlock_t lock;
refcount_t online_pin;

struct radix_tree_root blkg_tree; /* 按设备号索引的 blkg 树 */
struct blkcg_gq __rcu *blkg_hint; /* 最近访问的 blkg 缓存 */
struct hlist_head blkg_list; /* 所有 blkg 链表 */

struct blkcg_policy_data *cpd[BLKCG_MAX_POLS]; /* per-cgroup policy 数据 */

struct list_head all_blkcgs_node;

struct llist_head __percpu *lhead; /* 待 flush 的 per-CPU iostat 更新列表 */
};

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)); /* 设备名如 "8:0" */

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);
}
/* 各 policy(bfq/throttle)追加自身统计 */
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.weightio.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;

/* 使用 64 位以安全表示 PIDS_MAX = PID_MAX_LIMIT + 1 */
atomic64_t counter; /* 当前使用的 PID 数量(含所有子 cgroup) */
atomic64_t limit; /* 上限(pids.max) */
int64_t watermark; /* 历史最高水位(pids.peak) */

struct cgroup_file events_file; /* pids.events 文件句柄 */

atomic64_t events_limit; /* fork 因超限失败的次数 */
};

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; /* -EAGAIN 导致 fork 返回 EAGAIN */
}

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; /* cgroup 命名空间 */
};

注意 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; /* 不需要新 namespace,复用现有的 */
}

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; /* 将当前 css_set 设为新 namespace 的根 */

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.threadsthreaded 模式下的接口:在 threaded subtree 中,可以将单独线程迁移到子 cgroup,此时被配置为 threaded = true 的 controller(如 cpupids)以线程粒度计量,而 domain 级 controller(如 memory)仍以 domain 祖先为计量单位。

7.3 资源保护与 delegation

v2 通过 memory.min(保证)和 memory.low(尽力保护)提供了标准化的资源保护语义,这是 v1 所没有的。cgroup.subtree_control 文件控制哪些 controller 对子 cgroup 可见,结合 CGRP_ROOT_NS_DELEGATE 标志,可以安全地将 cgroup 管理权下放给非特权用户(如容器运行时)。

7.4 PSI(Pressure Stall Information)

v2 cgroup 节点内嵌了 psi_files[],提供 memory.pressurecpu.pressureio.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 管理的 cgroup 层次
systemd-cgls

# 实时显示各 cgroup CPU/内存/IO 使用
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

# 内存相关事件(low/high/max/oom/oom_kill)
cat /sys/fs/cgroup/<group>/memory.events

8.3 CPU 节流诊断

1
2
# CPU 统计:usage_usec、user_usec、system_usec、throttled_usec、nr_throttled
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
# throttled_usec 持续增长 → 增大 cpu.max 的 quota 或 period

8.4 IO 统计

1
2
3
4
# IO 读写字节数和 IOPS(per 设备)
cat /sys/fs/cgroup/<group>/io.stat
# 示例输出:
# 8:0 rbytes=1048576 wbytes=2097152 rios=256 wios=512 dbytes=0 dios=0

8.5 PSI 压力监控

1
2
3
4
5
6
7
8
# 系统级 memory 压力(全局 PSI,非 cgroup)
cat /proc/pressure/memory

# cgroup 级 memory 压力
cat /sys/fs/cgroup/<group>/memory.pressure

# cpu 压力(v2 cgroup 独有)
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
# 追踪每次内存计费,打印 cgroup 名称和页数
bpftrace -e '
kprobe:__mem_cgroup_charge {
$folio = (struct folio *)arg0;
printf("charge: comm=%s pages=%d\n",
comm, $folio->_folio_nr_pages);
}'

# 追踪 PID controller 阻止 fork 事件
bpftrace -e '
kretprobe:pids_can_fork /retval != 0/ {
printf("fork rejected: comm=%s pid=%d\n", comm, pid);
}'

# 追踪 CPU 节流事件
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 controllercfs_bandwidth 的 period/quota 机制通过 hrtimer 精确控制时间切片,per-CPU 本地池设计将全局锁争用降到最低,throttle_cfs_rq/unthrottle_cfs_rq 实现了毫秒级的细粒度节流。
  • IO controllerblkcg/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.hstruct task_groupstruct cfs_bandwidth
  • kernel/sched/fair.cthrottle_cfs_rqunthrottle_cfs_rqsched_cfs_bandwidth_slice
  • mm/memcontrol.cmem_cgroup_handle_over_highmem_cgroup_oomcharge_memcg
  • block/blk-cgroup.hstruct blkcgstruct blkcg_gq
  • block/blk-cgroup.cblkcg_print_stat