Skip to content

第三十二章:因果之链

筑基后期

涉及内核源码:

林小源在研究命名空间之后,发现了容器的另一半秘密:cgroup。

命名空间解决的是"隔离"——让进程看到不同的世界。cgroup 解决的是"限制"——让进程不能无节制地消耗资源。

他在内核的数据结构中看到了一棵树。不是普通的树——树的每个节点都挂着一条锁链,锁链上刻着数字:CPU 50%、内存 512MB、I/O 带宽有限。

"这是什么?"林小源问。

"cgroup。"树的根节点回答。它的声音低沉、稳重,像一个经验丰富的管家。"资源之链。每一个节点限制一组进程的资源使用。"

"谁把你挂上去的?"

"管理员。"根节点说,"他们通过 把进程绑定到我身上。从那一刻起,这些进程就受我的约束。"

"但你不是很多棵树。"林小源忽然说。

根节点看了他一眼,像是在确认这个 idle 小修士是否真的看懂了。

"在 cgroup v2 里,天道只许一棵统一的树。"根节点说,"cgroup core 只负责把进程组织到层级里,CPU、memory、I/O、pids 这些 controller 才负责分配各自的资源。每个进程在这棵树上只有一个归属,fork 出来的孩子默认继承父进程所在的 cgroup;后来把父进程迁走,也不会把已经出生的后代一并拖走。"

林小源这才明白,所谓因果之链不是一根绳子绑住一个进程,而是一棵统一层级把进程的血缘、归属和资源规则分开记录。命名空间让进程以为自己活在另一个世界;cgroup 则告诉它:不管你看见什么,你消耗的灵气都要从这棵树上扣。

c
/*
 * 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");

林小源看着那棵树,注意到一个规律:子节点的限制永远不能超过父节点。根节点 CPU 100%,子节点最多 100%。如果一个子节点试图设置 CPU 150%——拒绝。

"这是因果。"根节点说,"种什么因,得什么果。父节点给了你多少资源,你就只能用多少。你可以再分给你的子节点,但你不能超出父节点给你的上限。"

破关试炼

单树归属之试

cgroup v2 中,进程资源归属主要挂在几棵统一层级树上?

答对后才能继续滑动和进入下一章。

林小源在研究 cgroup 的过程中,发现了一个有趣的设计。

cgroup 不仅能限制资源,还能记账。内核会记录每个 cgroup 使用了多少 CPU 时间、多少内存、多少 I/O 带宽。这些数据可以用来监控、计费、或者做调度决策。

"你不只是管家,"林小源对根节点说,"你还是会计。"

"当然。"根节点说,"管理员需要知道每个容器消耗了多少资源。计费、监控、容量规划——都依赖这些数据。我记录每一毫秒的 CPU 时间,每一页的内存使用,每一次 I/O 操作。"

根节点伸出一根藤蔓,藤蔓上开出几枚小小的玉牌:cgroup.procscgroup.controllerscgroup.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.pressurememory.pressureio.pressure。泉水并不显示谁用了多少灵气,而是显示谁在等待、谁被堵住、谁被迫停在门外。

"这是 PSI,Pressure Stall Information。"根节点说,"资源消耗告诉你谁吃了多少,压力信息告诉你谁因为抢不到资源而停了多久。some 表示至少有一部分非 idle 任务在等,full 表示所有非 idle 任务都被堵住。看 avg10avg60avg300,可以知道最近十秒、六十秒、三百秒的压力走势;看 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 的功能:

  1. 资源限制 — 限制进程组的 CPU、内存、I/O 使用
  2. 资源记账 — 记录进程组的资源使用量
  3. 进程控制 — 暂停/恢复进程组
  4. 资源隔离 — 不同 cgroup 的资源使用互不影响

cgroup 的层次结构:

  • 树形结构,子 cgroup 继承父 cgroup 的限制
  • 子 cgroup 的限制不能超过父 cgroup
  • 每个 cgroup 可以有多个子 cgroup

cgroup v1 vs v2:

  • v1: 每个资源控制器有独立的层次结构
  • v2: 统一的层次结构,所有控制器共享同一棵树

cgroup v2 的核心边界:

  • cgroup.procs 用来列出或迁移进程
  • cgroup.controllers 展示当前层级可用的 controller
  • cgroup.subtree_control 把 controller 授权给下一层
  • 上层没授权,下层不能凭空启用 controller
  • domain controller 通常要求中间分配节点不要直接承载内部进程

资源观测不只看"用了多少",还要看"等了多久"。PSI 通过 cpu.pressurememory.pressureio.pressure 暴露 CPU、内存和 I/O 压力;delay accounting 则通过 taskstats 追踪任务在 CPU、块 I/O、swapin、reclaim 等路径上的等待时间。

容器 = 命名空间(隔离)+ cgroup(资源限制)

命名空间让进程看不到彼此,cgroup 让进程不能互相伤害。


以修仙之名,悟内核之道