第三十二章:因果之链
筑基后期涉及内核源码:
一
林小源在研究命名空间之后,发现了容器的另一半秘密:cgroup。
命名空间解决的是"隔离"——让进程看到不同的世界。cgroup 解决的是"限制"——让进程不能无节制地消耗资源。
他在内核的数据结构中看到了一棵树。不是普通的树——树的每个节点都挂着一条锁链,锁链上刻着数字:CPU 50%、内存 512MB、I/O 带宽有限。
"这是什么?"林小源问。
"cgroup。"树的根节点回答。它的声音低沉、稳重,像一个经验丰富的管家。"资源之链。每一个节点限制一组进程的资源使用。"
"谁把你挂上去的?"
"管理员。"根节点说,"他们通过 把进程绑定到我身上。从那一刻起,这些进程就受我的约束。"
"但你不是很多棵树。"林小源忽然说。
根节点看了他一眼,像是在确认这个 idle 小修士是否真的看懂了。
"在 cgroup v2 里,天道只许一棵统一的树。"根节点说,"cgroup core 只负责把进程组织到层级里,CPU、memory、I/O、pids 这些 controller 才负责分配各自的资源。每个进程在这棵树上只有一个归属,fork 出来的孩子默认继承父进程所在的 cgroup;后来把父进程迁走,也不会把已经出生的后代一并拖走。"
林小源这才明白,所谓因果之链不是一根绳子绑住一个进程,而是一棵统一层级把进程的血缘、归属和资源规则分开记录。命名空间让进程以为自己活在另一个世界;cgroup 则告诉它:不管你看见什么,你消耗的灵气都要从这棵树上扣。
/*
* cgroup (Control Group) 的作用:
* 限制进程组的资源使用:
* cpu — CPU 时间分配
* memory — 内存使用上限
* blkio — 块设备 I/O 带宽
* pids — 进程数量限制
* net_cls — 网络流量分类
*
* cgroup 是分层的(树形结构):
* 根 cgroup
* ├── web-server (CPU 50%, 内存 512MB)
* ├── db-server (CPU 80%, 内存 2GB)
* └── batch-job (CPU 20%, 内存 256MB)
*
* 子 cgroup 继承父 cgroup 的限制。
*/
struct cgroup {
char name[32];
int cpu_percent;
int memory_mb;
int pids_max;
struct cgroup *parent;
};
struct task_struct {
int pid;
char comm[16];
struct cgroup *cgrp;
};
printf("=== cgroup — 资源之链 ===\n\n");
/* 创建 cgroup 层次结构 */
struct cgroup root = { "root", 100, 8192, 1000, NULL };
struct cgroup web = { "web-server", 50, 512, 100, &root };
struct cgroup db = { "db-server", 80, 2048, 50, &root };
struct cgroup batch = { "batch-job", 20, 256, 200, &root };
struct cgroup *cgroups[] = { &root, &web, &db, &batch };
int nr = sizeof(cgroups) / sizeof(cgroups[0]);
printf("cgroup 层次结构:\n");
printf(" root (CPU %d%%, 内存 %dMB)\n", root.cpu_percent, root.memory_mb);
printf(" ├── web-server (CPU %d%%, 内存 %dMB)\n", web.cpu_percent, web.memory_mb);
printf(" ├── db-server (CPU %d%%, 内存 %dMB)\n", db.cpu_percent, db.memory_mb);
printf(" └── batch-job (CPU %d%%, 内存 %dMB)\n\n", batch.cpu_percent, batch.memory_mb);
/* 分配进程到 cgroup */
struct task_struct procs[] = {
{ .pid = 100, .comm = "nginx", .cgrp = &web },
{ .pid = 200, .comm = "postgres", .cgrp = &db },
{ .pid = 300, .comm = "batch-1", .cgrp = &batch },
{ .pid = 301, .comm = "batch-2", .cgrp = &batch },
};
int nr_procs = sizeof(procs) / sizeof(procs[0]);
printf("进程分配:\n");
for (int i = 0; i < nr_procs; i++) {
printf(" PID %d (%s) → cgroup %s\n",
procs[i].pid, procs[i].comm, procs[i].cgrp->name);
}
printf("\n--- 资源限制生效 ---\n");
printf("nginx (PID 100):\n");
printf(" CPU: 最多 %d%% 的 CPU 时间\n", procs[0].cgrp->cpu_percent);
printf(" 内存: 最多 %d MB\n", procs[0].cgrp->memory_mb);
printf(" 进程数: 最多 %d\n\n", procs[0].cgrp->pids_max);
printf("postgres (PID 200):\n");
printf(" CPU: 最多 %d%% 的 CPU 时间\n", procs[1].cgrp->cpu_percent);
printf(" 内存: 最多 %d MB\n", procs[1].cgrp->memory_mb);
printf(" 进程数: 最多 %d\n", procs[1].cgrp->pids_max);
printf("\n--- cgroup 的层次继承 ---\n");
printf("子 cgroup 的限制不能超过父 cgroup\n");
printf("root: CPU 100%%, 内存 8192MB\n");
printf(" web-server: CPU 50%% ≤ 100%% ✓\n");
printf(" db-server: CPU 80%% ≤ 100%% ✓\n");
printf(" 如果 web-server 尝试设置 CPU 150%% → 拒绝\n");#include <stdio.h>
#include <string.h>
/*
* cgroup (Control Group) 的作用:
* 限制进程组的资源使用:
* cpu — CPU 时间分配
* memory — 内存使用上限
* blkio — 块设备 I/O 带宽
* pids — 进程数量限制
* net_cls — 网络流量分类
*
* cgroup 是分层的(树形结构):
* 根 cgroup
* ├── web-server (CPU 50%, 内存 512MB)
* ├── db-server (CPU 80%, 内存 2GB)
* └── batch-job (CPU 20%, 内存 256MB)
*
* 子 cgroup 继承父 cgroup 的限制。
*/
struct cgroup {
char name[32];
int cpu_percent;
int memory_mb;
int pids_max;
struct cgroup *parent;
};
struct task_struct {
int pid;
char comm[16];
struct cgroup *cgrp;
};
int main() {
printf("=== cgroup — 资源之链 ===\n\n");
/* 创建 cgroup 层次结构 */
struct cgroup root = { "root", 100, 8192, 1000, NULL };
struct cgroup web = { "web-server", 50, 512, 100, &root };
struct cgroup db = { "db-server", 80, 2048, 50, &root };
struct cgroup batch = { "batch-job", 20, 256, 200, &root };
struct cgroup *cgroups[] = { &root, &web, &db, &batch };
int nr = sizeof(cgroups) / sizeof(cgroups[0]);
printf("cgroup 层次结构:\n");
printf(" root (CPU %d%%, 内存 %dMB)\n", root.cpu_percent, root.memory_mb);
printf(" ├── web-server (CPU %d%%, 内存 %dMB)\n", web.cpu_percent, web.memory_mb);
printf(" ├── db-server (CPU %d%%, 内存 %dMB)\n", db.cpu_percent, db.memory_mb);
printf(" └── batch-job (CPU %d%%, 内存 %dMB)\n\n", batch.cpu_percent, batch.memory_mb);
/* 分配进程到 cgroup */
struct task_struct procs[] = {
{ .pid = 100, .comm = "nginx", .cgrp = &web },
{ .pid = 200, .comm = "postgres", .cgrp = &db },
{ .pid = 300, .comm = "batch-1", .cgrp = &batch },
{ .pid = 301, .comm = "batch-2", .cgrp = &batch },
};
int nr_procs = sizeof(procs) / sizeof(procs[0]);
printf("进程分配:\n");
for (int i = 0; i < nr_procs; i++) {
printf(" PID %d (%s) → cgroup %s\n",
procs[i].pid, procs[i].comm, procs[i].cgrp->name);
}
printf("\n--- 资源限制生效 ---\n");
printf("nginx (PID 100):\n");
printf(" CPU: 最多 %d%% 的 CPU 时间\n", procs[0].cgrp->cpu_percent);
printf(" 内存: 最多 %d MB\n", procs[0].cgrp->memory_mb);
printf(" 进程数: 最多 %d\n\n", procs[0].cgrp->pids_max);
printf("postgres (PID 200):\n");
printf(" CPU: 最多 %d%% 的 CPU 时间\n", procs[1].cgrp->cpu_percent);
printf(" 内存: 最多 %d MB\n", procs[1].cgrp->memory_mb);
printf(" 进程数: 最多 %d\n", procs[1].cgrp->pids_max);
printf("\n--- cgroup 的层次继承 ---\n");
printf("子 cgroup 的限制不能超过父 cgroup\n");
printf("root: CPU 100%%, 内存 8192MB\n");
printf(" web-server: CPU 50%% ≤ 100%% ✓\n");
printf(" db-server: CPU 80%% ≤ 100%% ✓\n");
printf(" 如果 web-server 尝试设置 CPU 150%% → 拒绝\n");
return 0;
}林小源看着那棵树,注意到一个规律:子节点的限制永远不能超过父节点。根节点 CPU 100%,子节点最多 100%。如果一个子节点试图设置 CPU 150%——拒绝。
"这是因果。"根节点说,"种什么因,得什么果。父节点给了你多少资源,你就只能用多少。你可以再分给你的子节点,但你不能超出父节点给你的上限。"
单树归属之试
cgroup v2 中,进程资源归属主要挂在几棵统一层级树上?
二
林小源在研究 cgroup 的过程中,发现了一个有趣的设计。
cgroup 不仅能限制资源,还能记账。内核会记录每个 cgroup 使用了多少 CPU 时间、多少内存、多少 I/O 带宽。这些数据可以用来监控、计费、或者做调度决策。
"你不只是管家,"林小源对根节点说,"你还是会计。"
"当然。"根节点说,"管理员需要知道每个容器消耗了多少资源。计费、监控、容量规划——都依赖这些数据。我记录每一毫秒的 CPU 时间,每一页的内存使用,每一次 I/O 操作。"
根节点伸出一根藤蔓,藤蔓上开出几枚小小的玉牌:cgroup.procs、cgroup.controllers、cgroup.subtree_control。
"想迁移一个进程,就把它的 PID 写进 cgroup.procs。"根节点说,"想知道这一层能启用哪些 controller,就读 cgroup.controllers。想把 controller 分给下一层,就写 cgroup.subtree_control。"
"能随便开吗?"
"不能。"根节点的枝叶轻轻一抖,整棵树的上层纹路亮了起来。"第一条规矩叫 top-down constraint:上层没有启用或没有授予的 controller,下层不能凭空使用。第二条规矩叫 no internal process constraint:对 domain controller 来说,一个 cgroup 如果还直接养着进程,就不能同时把资源分给子 cgroup。要么自己承载任务,要么做纯粹的分配节点,不能两头都占。"
林小源想起那些被错误挂到中间节点的服务,终于明白为什么有时管理员明明写了配置,内核却拒绝执行。不是内核故意刁难,而是资源层级必须保持可解释:父层先有权,子层才有权;中间层若要分账,就不能自己还在账本里吃饭。
"那你公平吗?"
根节点沉默了一瞬。"公平取决于管理员怎么定义。我只执行规则——管理员定的规则。"
分账之试
cgroup v2 中用于把 controller 授权给子层级的核心文件名是什么?
三
林小源在观察 cgroup 的过程中,注意到了一个现象。
某些进程被放在了"受限"的 cgroup 中——它们的 CPU 时间被限制在 10%,内存被限制在 128MB。这些进程在资源充足时运行正常,但当系统负载升高时,它们会被"饿死"——CPU 时间不够,内存不够,I/O 带宽不够。
"那些被饿死的进程,"林小源问根节点,"你知道它们在受苦吗?"
"我知道。"根节点的声音没有波动。"我记录了它们的 CPU 使用率——只有 10%。它们的运行时间被压缩到了极限。"
"只看使用率,不够。"一个新的声音从树根下传来。
林小源低头,看见泥土里渗出三道细泉:cpu.pressure、memory.pressure、io.pressure。泉水并不显示谁用了多少灵气,而是显示谁在等待、谁被堵住、谁被迫停在门外。
"这是 PSI,Pressure Stall Information。"根节点说,"资源消耗告诉你谁吃了多少,压力信息告诉你谁因为抢不到资源而停了多久。some 表示至少有一部分非 idle 任务在等,full 表示所有非 idle 任务都被堵住。看 avg10、avg60、avg300,可以知道最近十秒、六十秒、三百秒的压力走势;看 total,能知道累计停顿时间。"
林小源把手伸进 memory.pressure 的泉水里,立刻听见成千上万页内存翻动的声音。有人在 direct reclaim 里挣扎,有人在 swapin 里等待,有人在 thrashing 中来回摔倒。另一条暗渠则通往 taskstats,那里记录着 delay accounting:等 CPU、等同步块 I/O、等换入、等内存回收、等直接内存规整,甚至等写保护复制和 IRQ/SOFTIRQ 的时间。
"所以,"林小源说,"真正的问题不只是用了多少,而是为了使用这些资源,付出了多少等待。"
"对。"根节点说,"容器被饿死之前,压力会先出现在这些文件里。管理员可以用 poll、select 或 epoll 在阈值上等消息,也可以用 delay accounting 找到最痛的任务。只是 delay accounting 不是凭空生效,它需要内核配置和 delayacct 这类开关,而且只对启用后的任务计账。"
"你不觉得不公平吗?"
"我是 cgroup。"根节点说,"我不做判断。管理员把进程放在我这里,我就执行限制。如果管理员的决策不公平,那是管理员的问题,不是我的。"
林小源看着那些被"枷锁"束缚的进程,心中泛起一阵不安。资源分配的公平性由谁来决定?cgroup 的层次结构是管理员设置的——但管理员的决策不一定公平。一个重要的数据库服务可能被错误地放在了低优先级的 cgroup 中,而一个不重要的日志服务可能占据了过多的资源。
公平不是绝对的——它取决于你怎么定义"公平"。
"但你是执行者。"林小源说,"规则是你执行的。"
"规则是内核写的。"根节点说,"我只是内核的一部分。我执行规则,不制定规则。如果你想改变公平,去找写规则的人。"
压力观测之试
PSI 中表示所有非 idle 任务都被资源堵住的字段名是什么?
道藏笔记
内核启示
cgroup(Control Group)是 Linux 资源管理的核心机制。
cgroup 的功能:
- 资源限制 — 限制进程组的 CPU、内存、I/O 使用
- 资源记账 — 记录进程组的资源使用量
- 进程控制 — 暂停/恢复进程组
- 资源隔离 — 不同 cgroup 的资源使用互不影响
cgroup 的层次结构:
- 树形结构,子 cgroup 继承父 cgroup 的限制
- 子 cgroup 的限制不能超过父 cgroup
- 每个 cgroup 可以有多个子 cgroup
cgroup v1 vs v2:
- v1: 每个资源控制器有独立的层次结构
- v2: 统一的层次结构,所有控制器共享同一棵树
cgroup v2 的核心边界:
cgroup.procs用来列出或迁移进程cgroup.controllers展示当前层级可用的 controllercgroup.subtree_control把 controller 授权给下一层- 上层没授权,下层不能凭空启用 controller
- domain controller 通常要求中间分配节点不要直接承载内部进程
资源观测不只看"用了多少",还要看"等了多久"。PSI 通过 cpu.pressure、memory.pressure、io.pressure 暴露 CPU、内存和 I/O 压力;delay accounting 则通过 taskstats 追踪任务在 CPU、块 I/O、swapin、reclaim 等路径上的等待时间。
容器 = 命名空间(隔离)+ cgroup(资源限制)
命名空间让进程看不到彼此,cgroup 让进程不能互相伤害。