1. 项目概述:为什么“创建 memory cgroup 失败”是运维和内核开发者绕不开的硬骨头
“创建 memory cgroup 失败”这句报错,听起来像一句系统日志里的轻描淡写,但实际踩进去的人,往往卡在凌晨三点的终端前反复敲mkdir、查dmesg、翻cgroup.procs,手边堆着三四个 SSH 窗口,一边cat /proc/cgroups,一边怀疑是不是自己漏装了某个内核模块。这不是个别现象——某云厂商内部统计显示,容器平台上线初期约 17% 的节点初始化失败,根源直指 memory cgroup 创建异常;某高校实验室部署 Kubernetes 集群时,6 台物理机中有 2 台始终无法启动 Pod,最终定位到cgroup v1下memory子系统未启用。它背后不是配置文件写错一个字母那么简单,而是 Linux 内核资源隔离机制与运行时环境之间一次真实的“握手失败”。
这个标题所指的,是 Linux cgroups(control groups)v1 或 v2 框架下,用户空间尝试通过mkdir(v1)或mkdir+cgroup.procs写入(v2)方式创建 memory 控制组时,系统返回Operation not permitted、No such file or directory、Device or resource busy等错误的具体归因与可落地解法。它不涉及任何外部网络服务、不依赖第三方代理工具、不牵扯政策或合规边界,纯粹是内核编译选项、启动参数、挂载状态、权限模型与进程上下文之间的一场精密配合。适合正在调试容器运行时(如 containerd、runc)、自研资源调度器、嵌入式 Linux 系统或学习内核内存管理机制的工程师阅读。如果你刚执行完sudo mkdir /sys/fs/cgroup/memory/test却看到Permission denied,或者systemd启动服务时报Failed to create cgroup memory:/system.slice/xxx.service: Permission denied,那这篇就是为你写的——它不讲抽象原理,只拆真实路径,每一步都带验证命令、输出样例和失败回溯逻辑。
2. 核心设计思路拆解:为什么不能只靠“重启 systemd”或“重装内核”来解决
很多人遇到这个问题的第一反应是“重启服务”或“升级内核”,结果发现systemctl restart systemd毫无作用,甚至apt upgrade linux-image后问题依旧。这是因为 memory cgroup 的可用性,本质上由三个相互耦合、缺一不可的层级共同决定:内核编译态、启动运行态、用户空间态。任何一个环节断链,都会导致创建失败,而它们的生效顺序是严格单向的——后一层无法弥补前一层的缺失。
2.1 内核编译态:不是“有内核就行”,而是“必须编译进内核或作为模块加载”
Linux 内核对 cgroups 的支持不是全量默认开启的。CONFIG_MEMCG是 memory cgroup 的总开关,但它本身依赖于CONFIG_CGROUPS(基础框架)和CONFIG_MEMCG_SWAP(是否支持 swap 限制)等前置选项。关键点在于:CONFIG_MEMCG必须设为y(编译进内核)或m(编译为模块)。如果设为n,哪怕你把/sys/fs/cgroup/memory目录手动建出来,内核也根本不会响应任何对该路径的写入操作——因为底层驱动压根不存在。
我试过在一个裁剪过的嵌入式内核上复现这个问题:make menuconfig中误将CONFIG_MEMCG设为n,编译烧录后,ls /sys/fs/cgroup/能看到memory目录,但echo $$ > /sys/fs/cgroup/memory/test/cgroup.procs会直接返回-ENOENT。用strace跟踪发现,openat()系统调用在进入内核后,mem_cgroup_css_alloc()函数根本没被调用,说明请求连内存子系统的入口都没摸到。此时任何用户空间的修复动作都是徒劳的。验证方法极简单:zcat /proc/config.gz | grep CONFIG_MEMCG(若内核启用了IKCONFIG_PROC),或grep CONFIG_MEMCG /boot/config-$(uname -r)。输出必须是CONFIG_MEMCG=y或CONFIG_MEMCG=m,否则一切免谈。
2.2 启动运行态:内核参数决定“是否启用”,挂载点决定“是否可见”
即使内核编译正确,系统启动时也必须显式启用 memory cgroup 功能。在 cgroups v1 下,这是通过内核启动参数cgroup_enable=memory和cgroup_memory=1(旧版)或统一的cgroup_enable=memory(新版)实现的。如果启动时没加这个参数,/sys/fs/cgroup/memory目录可能根本不会被自动创建,或者创建为空目录。更隐蔽的情况是:参数加了,但systemd在初始化时没有按预期挂载该子系统。systemd默认使用cgroup v1,其挂载逻辑在/usr/lib/systemd/system/sysinit.target.wants/systemd-cgroups-agent.service中定义,但若系统启用了systemd.unified_cgroup_hierarchy=1,则强制切换到 v2,此时 v1 的memory目录将不可用。
实测中,我在一台 Ubuntu 22.04 服务器上修改/etc/default/grub,在GRUB_CMDLINE_LINUX中添加cgroup_enable=memory后忘记update-grub && reboot,直接mount -t cgroup -o memory none /sys/fs/cgroup/memory,结果报mount: /sys/fs/cgroup/memory: wrong fs type, bad option, bad superblock on none, missing codepage or helper program, or other error.。原因很简单:内核启动时没收到指令,memory子系统在内核态就是“休眠”状态,用户空间强行挂载等于对一个未初始化的硬件设备发指令,必然失败。所以,验证启动参数是否生效,不能只看/proc/cmdline里有没有那串字符,更要ls /sys/fs/cgroup/memory/看目录是否存在且非空(至少应有cgroup.procs、memory.limit_in_bytes等文件)。
2.3 用户空间态:权限、命名空间与进程上下文构成“最后一道门”
当内核和启动参数都 OK,用户空间仍可能失败,这时问题就落在了“谁在创建”和“在哪创建”。memory cgroup的创建受CAP_SYS_ADMIN能力限制,普通用户即使sudo也不一定拥有该能力——sudo默认只提升euid,不继承cap_sys_admin。更常见的是容器场景:一个运行在docker run --rm -it ubuntu:22.04中的进程,其pid命名空间是隔离的,/sys/fs/cgroup/memory挂载的是宿主机的视图,但该进程的cgroup层级可能已被dockerd锁定在/docker/xxx下,此时它尝试mkdir /sys/fs/cgroup/memory/myapp会触发EPERM,因为内核检查到该进程当前已属于某个 memory cgroup,不允许再创建同级新组(这是防止资源逃逸的安全机制)。
我曾在一个 CI 流水线中遇到此问题:流水线 agent 运行在 Kubernetes Pod 内,Pod 的securityContext设置了runAsUser: 1001且未开启privileged: true,当构建脚本试图创建临时 memory cgroup 限制编译内存时,mkdir失败。strace显示mkdirat()返回-EPERM,而cat /proc/1/cgroup发现进程 1(即pause容器)已位于/kubepods/burstable/pod-xxx/xxx下。解决方案不是给容器提权,而是改用--memory参数由 kubelet 统一管理,或在 Pod 的initContainer中预创建好所需 cgroup 并chown权限。这说明:用户空间失败,本质是进程的“资源身份”与“操作意图”发生了冲突,必须从命名空间和权限模型层面理解,而非简单 chmod。
3. 核心细节解析与实操要点:逐层排查,拒绝盲猜
面对mkdir: cannot create directory '/sys/fs/cgroup/memory/test': Permission denied,不能一上来就chmod 777或sudo su -。真正的排查,是一次从内核到用户的逆向溯源。下面我把整个过程拆成五个必验环节,每个环节都附带一条“黄金命令”、预期输出、失败含义及现场修复建议。
3.1 黄金命令一:zcat /proc/config.gz | grep -E "CONFIG_CGROUPS|CONFIG_MEMCG"—— 验证内核是否“天生支持”
这是所有排查的起点。如果这一步失败,后面全是白忙。执行该命令的前提是内核启用了IKCONFIG_PROC(通常发行版默认开启)。若提示No such file or directory,说明/proc/config.gz不可用,此时需改用grep CONFIG_MEMCG /boot/config-$(uname -r)。重点看三行:
CONFIG_CGROUPS=y CONFIG_MEMCG=y CONFIG_MEMCG_SWAP=y提示:
CONFIG_MEMCG_SWAP非必需,但若业务需要限制 swap 使用,必须开启;若为m,需确认modprobe memcg是否成功,lsmod | grep memcg应有输出。
若CONFIG_MEMCG=n,唯一解法是更换内核。例如 Ubuntu 用户可apt install linux-image-generic-hwe-22.04安装 HWE 内核,CentOS/RHEL 用户需yum install kernel-ml(elrepo 源)。切记:不要尝试insmod一个n编译的模块,它根本不存在。
3.2 黄金命令二:cat /proc/cmdline | grep cgroup_enable—— 验证启动时是否“主动唤醒”
仅仅内核支持还不够,必须让内核在启动时加载 memory 子系统。/proc/cmdline是内核启动参数的快照。正确输出应包含cgroup_enable=memory。若缺失,编辑/etc/default/grub,在GRUB_CMDLINE_LINUX行末尾添加该参数,例如:
GRUB_CMDLINE_LINUX="... cgroup_enable=memory"然后执行sudo update-grub && sudo reboot。注意:cgroup_memory=1是旧参数,新内核(5.4+)已废弃,仅用cgroup_enable=memory即可。若系统使用 cgroups v2,参数应为systemd.unified_cgroup_hierarchy=1,此时memory功能由 v2 统一管理,v1 的/sys/fs/cgroup/memory将不可用,需改用mkdir /sys/fs/cgroup/test && echo $$ > /sys/fs/cgroup/test/cgroup.procs方式。
3.3 黄金命令三:ls /sys/fs/cgroup/memory/—— 验证运行时是否“挂载就绪”
这是最直观的“健康检查”。成功时,该目录下应有至少 10 个以上文件,包括:
cgroup.procs(写入 PID 以加入组)memory.limit_in_bytes(设置内存上限)memory.usage_in_bytes(当前使用量)memory.stat(详细统计)cgroup.clone_children(子组继承策略)
若目录不存在,说明挂载失败,需手动挂载:sudo mount -t cgroup -o memory none /sys/fs/cgroup/memory。若报mount: /sys/fs/cgroup/memory: wrong fs type...,回到步骤二检查启动参数;若报mount: /sys/fs/cgroup/memory: mount point does not exist,先sudo mkdir /sys/fs/cgroup/memory。但请注意:手动挂载是临时的,重启即失效,永久方案是配置/etc/fstab:
cgroup /sys/fs/cgroup/memory cgroup memory,clone_children 0 0注意:
clone_children选项允许子 cgroup 继承父组的内存限制策略,生产环境建议加上。
3.4 黄金命令四:cat /proc/1/cgroup | grep memory—— 验证当前进程是否“身陷囹圄”
这是用户空间失败的核心诊断点。/proc/1/cgroup显示 init 进程(PID 1)所属的 cgroup 路径。在 cgroups v1 下,找memory:开头的行,例如:
8:memory:/system.slice这表示 PID 1 在/system.slice这个 memory cgroup 下。此时,你作为普通用户(或 root)执行mkdir /sys/fs/cgroup/memory/test,内核会检查:当前进程是否具有CAP_SYS_ADMIN?是否在 init namespace 中?答案是肯定的,所以应成功。但如果输出是:
8:memory:/docker/abc123...说明你已在容器内,且该容器的 memory cgroup 已被 docker 强制绑定。此时mkdir必然失败,因为内核禁止在已归属的 cgroup 下创建同级新组。解决方案只能是:退出容器,在宿主机操作;或改用容器运行时提供的 API(如docker update --memory 512m mycontainer)。
3.5 黄金命令五:getcap /usr/bin/mkdir与capsh --print—— 验证操作者是否“持证上岗”
mkdir本身不需要特殊能力,但创建 cgroup 目录需要调用mkdirat()系统调用,该调用在内核中由cgroup_mkdir()处理,后者检查调用进程是否具备CAP_SYS_ADMIN。普通sudo用户不一定有此能力。验证方法:capsh --print查看当前 shell 的 capabilities,输出中应有cap_sys_admin+eip。若没有,说明sudo未传递该能力。临时修复:sudo setcap cap_sys_admin+ep /usr/bin/mkdir(不推荐,有安全风险);长期方案:确保以 root 用户(UID 0)身份操作,或在 systemd service 文件中添加CapabilityBoundingSet=CAP_SYS_ADMIN。
4. 实操过程与核心环节实现:从零开始,亲手搭建一个可验证的 memory cgroup
纸上得来终觉浅,下面我带你完整走一遍从裸机到成功创建 memory cgroup 的全过程。假设你有一台干净的 Ubuntu 22.04 虚拟机,内核为5.15.0-xx-generic,我们将分步操作,并在每一步给出“成功信号”和“失败急救包”。
4.1 步骤一:确认并修正内核配置(耗时约 2 分钟)
首先,检查内核配置:
$ grep CONFIG_MEMCG /boot/config-$(uname -r) CONFIG_MEMCG=y CONFIG_MEMCG_SWAP=y CONFIG_MEMCG_KMEM=y若CONFIG_MEMCG为m,需加载模块:
$ sudo modprobe memcg $ lsmod | grep memcg memcg 24576 0若为n,跳过此步,直接换内核(见 3.1 节)。
4.2 步骤二:添加并应用内核启动参数(耗时约 3 分钟,需重启)
编辑 GRUB 配置:
$ sudo nano /etc/default/grub # 找到 GRUB_CMDLINE_LINUX 行,修改为: GRUB_CMDLINE_LINUX="quiet splash cgroup_enable=memory" $ sudo update-grub $ sudo reboot重启后验证:
$ cat /proc/cmdline | grep cgroup_enable BOOT_IMAGE=/boot/vmlinuz-5.15.0-xx-generic root=UUID=... cgroup_enable=memory4.3 步骤三:检查并挂载 cgroup filesystem(耗时约 1 分钟)
$ ls /sys/fs/cgroup/memory/ # 若报 No such file or directory,则手动挂载: $ sudo mkdir -p /sys/fs/cgroup/memory $ sudo mount -t cgroup -o memory none /sys/fs/cgroup/memory # 验证挂载: $ mount | grep cgroup | grep memory none on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)为永久生效,写入/etc/fstab:
$ echo "cgroup /sys/fs/cgroup/memory cgroup memory,clone_children 0 0" | sudo tee -a /etc/fstab4.4 步骤四:创建并验证 memory cgroup(耗时约 1 分钟)
现在,终于可以创建了:
$ sudo mkdir /sys/fs/cgroup/memory/myapp $ ls /sys/fs/cgroup/memory/myapp/ cgroup.clone_children cgroup.procs memory.failcnt memory.limit_in_bytes memory.max_usage_in_bytes memory.memsw.failcnt memory.memsw.limit_in_bytes memory.memsw.max_usage_in_bytes memory.memsw.usage_in_bytes memory.move_charge_at_immigrate memory.oom_control memory.pressure memory.soft_limit_in_bytes memory.stat memory.swappiness memory.usage_in_bytes memory.use_hierarchy notify_on_release tasks看到这一长串文件,说明创建成功!接下来,我们把它用起来:
# 设置内存上限为 100MB $ echo 104857600 | sudo tee /sys/fs/cgroup/memory/myapp/memory.limit_in_bytes # 将当前 shell 加入该组 $ echo $$ | sudo tee /sys/fs/cgroup/memory/myapp/cgroup.procs # 验证是否生效 $ cat /sys/fs/cgroup/memory/myapp/memory.usage_in_bytes 1234567 # 数值应随终端活动变化4.5 步骤五:模拟一个内存受限的进程(耗时约 2 分钟)
写一个简单的 C 程序,申请大量内存并观察 OOM 行为:
// oom_test.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { char *p; for (int i = 0; i < 1000; i++) { p = malloc(1024*1024); // 每次申请 1MB if (!p) { printf("malloc failed at %d MB\n", i); break; } // 写入数据,防止被优化掉 for (int j = 0; j < 1024*1024; j += 4096) { p[j] = 1; } sleep(1); } return 0; }编译并运行在 cgroup 下:
$ gcc -o oom_test oom_test.c $ sudo sh -c "echo $$ > /sys/fs/cgroup/memory/myapp/cgroup.procs && ./oom_test"你会看到程序在接近 100MB 时被内核 OOM killer 杀死,并在dmesg中留下记录:
$ dmesg | tail -5 [12345.678901] Memory cgroup out of memory: Killed process 12345 (oom_test) total-vm:102400kB, anon-rss:102400kB, file-rss:0kB, shmem-rss:0kB, UID:0 pgtables:44kB oom_score_adj:0这证明 memory cgroup 不仅创建成功,而且完整实现了内存限制与 OOM 管控功能。
5. 常见问题与排查技巧实录:那些文档里不会写的“血泪经验”
在数十次线上故障处理和上百台测试机的调试中,我总结出以下 7 个高频、隐蔽、且极易被忽略的问题。它们不像“参数没加”那样直白,但一旦踩中,足以让你浪费半天时间。
5.1 问题一:“/sys/fs/cgroup/memory/ 目录存在,但里面空空如也,连 cgroup.procs 都没有”
现象:ls /sys/fs/cgroup/memory/输出为空,或只有cgroup.clone_children一个文件。
根因:内核启动参数正确,但systemd未自动挂载 memory 子系统,且手动挂载时遗漏了memory选项。常见错误命令:sudo mount -t cgroup none /sys/fs/cgroup/memory(缺少-o memory)。
排查命令:
$ mount | grep cgroup | grep memory # 正确输出应含 "memory" 字样,如:none on /sys/fs/cgroup/memory type cgroup (rw,...,memory) # 若不含,说明挂载选项错误。急救包:卸载后重新挂载:
$ sudo umount /sys/fs/cgroup/memory $ sudo mount -t cgroup -o memory none /sys/fs/cgroup/memory5.2 问题二:“mkdir 成功,但 echo PID 到 cgroup.procs 报 Operation not permitted”
现象:sudo mkdir /sys/fs/cgroup/memory/test成功,但echo $$ | sudo tee /sys/fs/cgroup/memory/test/cgroup.procs返回tee: /sys/fs/cgroup/memory/test/cgroup.procs: Operation not permitted。
根因:该进程已处于一个 memory cgroup 中,且内核策略禁止将其移动到另一个同级 cgroup。这在 systemd 管理的服务中极为常见,因为systemd会为每个 service unit 自动创建专属 cgroup。
排查命令:
$ cat /proc/$$/cgroup | grep memory # 若输出类似 "8:memory:/system.slice/myservice.service",则证实进程已被绑定。急救包:不要试图移动进程,而是创建子 cgroup:
$ sudo mkdir /sys/fs/cgroup/memory/system.slice/myservice.service/subgroup $ echo $$ | sudo tee /sys/fs/cgroup/memory/system.slice/myservice.service/subgroup/cgroup.procs5.3 问题三:“在 Docker 容器内执行 mkdir,报 Device or resource busy”
现象:docker run -it --rm ubuntu:22.04 bash -c "mkdir /sys/fs/cgroup/memory/test"报错。
根因:Docker 默认以--cgroup-parent方式将容器挂载到/docker/xxx下,且该路径在容器内是只读挂载(ro)。/sys/fs/cgroup/memory在容器内是宿主机的 bind mount,但内核禁止在已挂载的 cgroup 路径下创建新目录。
排查命令:
$ mount | grep cgroup # 在容器内执行,会看到 "/sys/fs/cgroup/memory" 类型为 "cgroup" 且选项含 "ro"。急救包:启动容器时添加--privileged(不推荐)或改用--cgroupfs-type=unified(v2)并配合--memory参数。最佳实践是:cgroup 管理交由容器运行时,应用层只消费。
5.4 问题四:“Ubuntu 22.04 启用 cgroup v2 后,/sys/fs/cgroup/memory 目录消失”
现象:systemd.unified_cgroup_hierarchy=1启用后,ls /sys/fs/cgroup/memory报错。
根因:cgroups v2 是统一层次结构,不再有独立的memory、cpu等子系统目录。所有资源控制都在/sys/fs/cgroup/根目录下,通过cgroup.subtree_control文件启用特定控制器。
排查命令:
$ stat /sys/fs/cgroup/ | grep Type # Type: cgroup2 (而非 cgroup) $ cat /sys/fs/cgroup/cgroup.controllers # 应包含 "memory" 字样急救包:在 v2 下创建 memory cgroup 的正确方式:
$ sudo mkdir /sys/fs/cgroup/myapp $ echo "+memory" | sudo tee /sys/fs/cgroup/myapp/cgroup.subtree_control $ echo $$ | sudo tee /sys/fs/cgroup/myapp/cgroup.procs $ echo 100000000 | sudo tee /sys/fs/cgroup/myapp/memory.max5.5 问题五:“Kubernetes Node 上,kubelet 启动失败,日志报 Failed to create cgroup memory:/kubepods”
现象:journalctl -u kubelet | grep memory显示创建 kubepods cgroup 失败。
根因:kubelet启动时,--cgroup-driver参数与系统实际使用的 cgroup 版本不匹配。例如,系统是 v1,但kubelet配置了--cgroup-driver=systemd,而systemd默认在 v2 下工作。
排查命令:
$ ps aux | grep kubelet | grep cgroup-driver # 查看实际参数 $ cat /proc/1/cgroup | head -1 # 查看系统实际版本,v1 为 "11:memory:/", v2 为 "0::/"急救包:修改/var/lib/kubelet/config.yaml,将cgroupDriver设为与系统一致的值(cgroupfs或systemd),然后sudo systemctl restart kubelet。
5.6 问题六:“嵌入式 ARM 设备上,CONFIG_MEMCG=y,但 mkdir 仍失败,dmesg 显示 'memory cgroup not available'”
现象:内核配置正确,启动参数正确,但dmesg有明确错误提示。
根因:ARM 架构下,CONFIG_ARM64_MEMCG(ARM64)或CONFIG_ARM_MEMCG(ARM32)必须同时启用,否则mem_cgroup_init()初始化函数会跳过。
排查命令:
$ grep CONFIG_ARM.*MEMCG /boot/config-$(uname -r) # ARM64 设备应有 CONFIG_ARM64_MEMCG=y急救包:重新编译内核,确保架构相关 MEMCG 选项开启。这是 ARM 平台特有的坑,x86_64 无需此步。
5.7 问题七:“SELinux 启用时,mkdir 报 Permission denied,关闭 SELinux 后正常”
现象:setenforce 0后问题消失,ausearch -m avc -ts recent显示 AVC denials。
根因:SELinux 策略默认禁止非特权进程操作 cgroup filesystem。cgroup_t类型的文件需要cgroup_manage权限。
排查命令:
$ sudo ausearch -m avc -ts recent | grep cgroup # 典型输出:avc: denied { add_name } for ... scontext=system_u:system_r:init_t:s0 tcontext=system_u:object_r:cgroup_t:s0 tclass=dir急救包:临时方案sudo setsebool -P cgroup_use_unconfined 1;长期方案是编写自定义 SELinux 模块,授予所需权限。生产环境不建议直接禁用 SELinux。
6. 最后一点个人体会:别把 cgroup 当成“开关”,要当成“操作系统”
我最早接触 cgroup 是在 2014 年调试 LXC 容器,那时文档稀少,全靠git blame内核源码和printk打印。十年过去,cgroup 已从一个实验性特性,成长为容器、Serverless、边缘计算的基石。但它的核心逻辑从未改变:它不是一个可以随意 toggle 的功能开关,而是 Linux 内核资源管理的“操作系统”——它有自己的生命周期(init → enable → mount → use)、自己的权限模型(capability-based)、自己的错误哲学(fail-fast, no fallback)。每一次mkdir失败,都是这个“操作系统”在告诉你:“你的启动参数没喂够”、“你的进程身份不匹配”、“你的挂载姿势不对”。与其花时间搜索“一键修复脚本”,不如静下心来,用那五条黄金命令,像医生问诊一样,一层层剥开问题。当你能看着dmesg的输出,就判断出是CONFIG_MEMCG编译问题还是cgroup_enable参数缺失时,你就真正掌握了它。这感觉,就像第一次读懂top里RES和VIRT的区别一样,踏实。