本文基于一份约 400 行的 C 源码,拆解一个"迷你 Docker"的实现:它没有镜像分层、没有 veth 网络、没有 seccomp,但已经具备容器最核心的几块拼图:Namespace 隔离、pivot_root 切换根文件系统、uid_map/gid_map 用户映射,以及 cgroup v2 内存限制。读完后,你会明白 Docker、runc 这类工具底层到底在做什么。
一、整体目标
这个程序的目标很明确:
bash
bash
./container_memory_limit /path/to/rootfs
启动后,它会:
- 用
clone创建子进程,并给子进程一套新的 Namespace; - 父进程给子进程设置 cgroup v2 内存上限,例如 64MB;
- 父进程写
uid_map/gid_map,让容器内 root 映射到宿主普通用户; - 子进程把
rootfs挂载成新根,挂载/proc,执行pivot_root; - 子进程执行
/bin/bash,进入一个"像容器一样"的 shell。
它模拟了 Docker 的核心动作,但把复杂度降到了最低。
二、核心原理
1. Namespace:隔离视图
Linux 容器不是虚拟机,它没有虚拟硬件,而是靠 Namespace 让进程看到不同的系统视图。
源码中的关键一行:
c
objectivec
int flags = CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUTS |
CLONE_NEWIPC | CLONE_NEWNET | CLONE_NEWUSER | SIGCHLD;
pid_t pid = clone(child_main, child_stack + STACK_SIZE, flags, &args);
这些 flag 的含义:
CLONE_NEWNS:新的 mount namespace,容器内挂载不影响宿主;CLONE_NEWPID:新的 PID namespace,容器内第一个进程 PID 为 1;CLONE_NEWUTS:新的 hostname/domainname,可设置容器主机名;CLONE_NEWIPC:新的 IPC namespace,隔离信号量、消息队列;CLONE_NEWNET:新的网络 namespace,隔离网卡、路由表;CLONE_NEWUSER:新的用户 namespace,让普通用户也能在容器内成为 root;SIGCHLD:子进程退出时父进程可waitpid。
注意 clone 的栈参数是 child_stack + STACK_SIZE,因为栈向下增长,必须传栈顶地址。
2. Cgroup v2:资源限制
Namespace 负责"看不到",Cgroup 负责"用不了太多"。源码使用 cgroup v2 的 memory.max 限制内存。
关键路径通常长这样:
text
bash
/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/container_<pid>
在这个目录下写:
bash
arduino
echo 67108864 > memory.max
echo 0 > memory.swap.max
memory.max 是硬限制,超过会触发 OOM Killer;memory.swap.max=0 禁用 swap,否则内存超限可能被换出,限制就不硬了。
3. pivot_root:切换根文件系统
Docker 容器看到的 / 并不是宿主的 /,而是镜像 rootfs。源码用 pivot_root 完成切换:
c
scss
mount(rootfs, rootfs, NULL, MS_BIND | MS_REC, NULL);
mkdir("/path/rootfs/proc", 0755);
mount("proc", "/path/rootfs/proc", "proc", MS_NOSUID | MS_NODEV | MS_NOEXEC, NULL);
mkdir("/path/rootfs/put_old", 0755);
syscall(SYS_pivot_root, rootfs, put_old);
chdir("/");
umount2("/put_old", MNT_DETACH);
为什么要先 bind mount rootfs 到自身?因为 pivot_root 要求新根是一个挂载点。mount(rootfs, rootfs, MS_BIND) 把它变成挂载点。
为什么先挂 /proc?因为切换后新根里需要 /proc,否则 ps、top 等命令不可用。
4. uid_map / gid_map:用户映射
在 CLONE_NEWUSER 下,容器内可以拥有 root,但实际映射到宿主普通用户。父进程写:
c
arduino
fprintf(f, "0 %d 1", getuid());
意思是:容器内 UID 0 映射到宿主当前 UID。这样容器内 id 显示 uid=0(root),但宿主机上仍然是普通用户,降低风险。
子进程一开始会 usleep(200 * 1000),等待父进程写完映射,否则可能显示 uid=65534。
三、代码走读
1. 父进程设置 cgroup 内存限制
源码中的 setup_memory_limit(pid) 是内存限制核心。它先读 /proc/self/cgroup:
c
ini
FILE *f = fopen("/proc/self/cgroup", "r");
fgets(self_cgroup, sizeof(self_cgroup), f);
拿到类似:
text
bash
0::/user.slice/user-1000.slice/session-2.scope
然后推导 systemd 委托根:
c
c
snprintf(needle, sizeof(needle), "user-%u.slice", (unsigned)uid);
char *p = strstr(cg_path, needle);
...
safe_snprintf(delegated_rel, sizeof(delegated_rel),
"%.*s/user@%u.service",
(int)prefix_len, cg_path, (unsigned)uid);
目标路径是:
text
sql
/user.slice/user-1000.slice/user@1000.service
接着创建容器专属 cgroup:
c
perl
safe_snprintf(dir, sizeof(dir), "%s/container_%d",
delegated_abs, (int)pid);
if (mkdir(dir, 0755) < 0 && errno != EEXIST) {
fprintf(stderr, "[parent] mkdir %s failed: %s\n", dir, strerror(errno));
return;
}
启用 memory 控制器:
c
lua
safe_snprintf(path, sizeof(path),
"%s/cgroup.subtree_control", delegated_abs);
write_file(path, "+memory");
写入内存限制:
c
scss
safe_snprintf(path, sizeof(path), "%s/memory.max", dir);
snprintf(val, sizeof(val), "%lu", limit);
write_file(path, val);
禁用 swap:
c
lua
safe_snprintf(path, sizeof(path), "%s/memory.swap.max", dir);
write_file(path, "0");
最后把子进程 PID 放进 cgroup:
c
scss
safe_snprintf(path, sizeof(path), "%s/cgroup.procs", dir);
snprintf(val, sizeof(val), "%d", (int)pid);
write_file(path, val);
这样,子进程及其后代就受 64MB 内存限制。超过后会被内核 OOM Kill。
源码中还做了一步"把父进程自己移入委托根":
c
scss
safe_snprintf(path, sizeof(path),
"%s/cgroup.procs", delegated_abs);
snprintf(val, sizeof(val), "%d", getpid());
write_file(path, val);
这是为了满足 cgroup v2 的 containment rule:父进程要能把子进程写进某个 cgroup,通常需要父进程和目标 cgroup 在同一委托子树内。实际系统中如果 systemd 没委托 memory 控制器,这一步或后续写 memory.max 会失败,需要配置 user@.service。
2. 父进程写 uid_map / gid_map
clone 成功后,父进程写:
c
scss
snprintf(path, sizeof(path), "/proc/%d/uid_map", (int)pid);
f = fopen(path, "w");
fprintf(f, "0 %d 1", getuid());
fclose(f);
再写 gid_map:
c
scss
snprintf(path, sizeof(path), "/proc/%d/gid_map", (int)pid);
f = fopen(path, "w");
fprintf(f, "0 %d 1", getgid());
fclose(f);
注意:写 map 必须发生在子进程 exec 之前,且子进程要等待。源码里子进程 usleep(200 * 1000) 就是干这个。
3. 子进程初始化容器
子进程入口 child_main 主要做:
c
scss
// 让挂载事件不传播到宿主
mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL);
// 把 rootfs 变成挂载点,供 pivot_root 使用
mount(rootfs, rootfs, NULL, MS_BIND | MS_REC, NULL);
// 在新根下准备 /proc
mkdir(proc_path, 0755);
mount("proc", proc_path, "proc",
MS_NOSUID | MS_NODEV | MS_NOEXEC, NULL);
// 准备 put_old,pivot_root 后旧根会挂到这里
mkdir(put_old, 0755);
syscall(SYS_pivot_root, rootfs, put_old);
// 切到新根
chdir("/");
// 卸载旧根
umount2("/put_old", MNT_DETACH);
rmdir("/put_old");
然后设置环境变量:
c
erlang
setenv("JAVA_HOME", "/opt/jdk", 1);
setenv("PATH", "/opt/jdk/bin:/usr/bin:/bin", 1);
setenv("LD_LIBRARY_PATH", "/opt/jdk/lib:/opt/jdk/lib/jli", 1);
读取 /etc/hostname 并设置主机名:
c
ini
FILE *f = fopen("/etc/hostname", "r");
fgets(hostname, sizeof(hostname), f);
syscall(SYS_sethostname, hostname, strlen(hostname));
最后执行 bash:
c
arduino
char *argv[] = { "/bin/bash", NULL };
execv("/bin/bash", argv);
此时 bash 成为容器内 PID 1,用户进入了一个隔离的 shell。
四、编译与运行
编译:
bash
gcc -Wall -O2 -o container_memory_limit container_memory_limit.c
准备一个 rootfs,例如包含 /bin/bash、/bin/ls、/proc 等。运行:
bash
bash
./container_memory_limit /home/yym/rootfs
进入容器后可以验证:
bash
ruby
echo $$
# 通常是 1
cat /proc/self/cgroup
# 可以看到位于 container_<pid> 下
grep Cap /proc/self/status
# 查看 capabilities
在宿主机查看内存限制:
bash
bash
cat /sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/container_<pid>/memory.max
# 67108864
在容器内制造内存压力:
bash
css
stress --vm 1 --vm-bytes 100M
超过 64MB 后,进程会被 OOM Kill。这就是 cgroup 内存限制的效果。
五、局限与改进
这份代码已经能跑通核心链路,但离生产级容器还有距离:
- 没有 PID 1 信号处理:bash 退出后容器就结束,僵尸进程可能没人回收。
- 没有 seccomp / capabilities 降权:容器内权限仍然较大。
- 网络未配置 :
CLONE_NEWNET创建了新网络栈,但没有 veth、网桥、IP,容器内基本没网。 - rootfs 需手工准备:没有镜像拉取、解压、分层。
- cgroup 清理较简单 :退出时写
cgroup.kill再rmdir,异常场景可能残留。 - 依赖 systemd 委托 :如果
user@.service没委托 memory 控制器,内存限制会失败。
但作为学习项目,它把 Docker 最核心的几件事讲清楚了:Namespace 隔离视图,Cgroup 限制资源,pivot_root 切换根,uid_map 映射用户,最后 exec 用户进程。
六、总结
Docker 并不神秘。它本质上是一个"高级进程启动器":
- 用 Namespace 让进程看不到别的进程、网络、挂载;
- 用 Cgroup 让它不能用太多 CPU、内存;
- 用 pivot_root 让它看到另一个根文件系统;
- 用 uid_map 让容器内 root 映射到宿主普通用户;
- 最后 exec 一个 shell 或应用。
这份 container_memory_limit.c 虽然短,但已经是一个可运行的迷你 Docker 原型。理解它之后,再去看 runc、containerd、Docker 的源码,就不会觉得无从下手了。 #编译命令 gcc -o containerk_memory_limit container_memory_limit.c #运行命令 systemd-run --user --scope --unit=my-container ./container_memory_limit /home/yym/container
##源码
c
yym@yym:~$ cat container_memory_limit.c
#define _GNU_SOURCE
#include <sched.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdarg.h>
#ifndef PATH_MAX
#define PATH_MAX 4096
#endif
#define STACK_SIZE (1024*1024)
#define MEM_LIMIT_BYTES (64UL * 1024 * 1024) // 64M(10M 对 bash 太小,先放大验证链路)
char *child_stack;
static char g_cgroup_path[PATH_MAX] = {0};
struct container_args {
char *rootfs;
};
static int write_file(const char *path, const char *val) {
int fd = open(path, O_WRONLY | O_CLOEXEC);
if (fd < 0) return -1;
ssize_t len = strlen(val);
ssize_t n = write(fd, val, len);
close(fd);
return (n == len) ? 0 : -1;
}
static int safe_snprintf(char *buf, size_t size, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(buf, size, fmt, ap);
va_end(ap);
if (n < 0 || (size_t)n >= size) {
if (size > 0) buf[0] = '\0';
return -1;
}
return 0;
}
/* ============================================================
* 内存限制:在 systemd 委托的 user@<uid>.service 子树下创建
* 容器 cgroup,写 memory.max,再把子进程放进去。
* ============================================================ */
static void setup_memory_limit(pid_t pid) {
char dir[PATH_MAX];
char path[PATH_MAX];
char val[64];
char self_cgroup[PATH_MAX];
char delegated_rel[PATH_MAX]; // 相对 /sys/fs/cgroup 的路径
char delegated_abs[PATH_MAX]; // 绝对路径
unsigned long limit = MEM_LIMIT_BYTES;
uid_t uid = getuid();
struct stat st;
// ---------- 1. 读取当前进程所在 cgroup 路径 ----------
FILE *f = fopen("/proc/self/cgroup", "r");
if (!f) {
fprintf(stderr, "[parent] fopen /proc/self/cgroup failed: %s\n", strerror(errno));
return;
}
if (!fgets(self_cgroup, sizeof(self_cgroup), f)) {
fclose(f);
fprintf(stderr, "[parent] fgets /proc/self/cgroup failed\n");
return;
}
fclose(f);
// 去掉 "0::" 前缀和换行
char *cg_path = strchr(self_cgroup, ':');
if (!cg_path) { fprintf(stderr, "[parent] malformed cgroup line\n"); return; }
cg_path = strchr(cg_path + 1, ':');
if (!cg_path) { fprintf(stderr, "[parent] malformed cgroup line\n"); return; }
cg_path++;
cg_path[strcspn(cg_path, "\n")] = '\0';
printf("[parent] self cgroup: %s\n", cg_path);
// ---------- 2. 从路径推导委托根 ----------
// 输入样例:
// /user.slice/user-1000.slice/session-2.scope
// /user.slice/user-1000.slice/user@1000.service/app.slice/xxx.scope
// 目标:/user.slice/user-1000.slice/user@1000.service
char needle[64];
snprintf(needle, sizeof(needle), "user-%u.slice", (unsigned)uid);
char *p = strstr(cg_path, needle);
if (!p) {
fprintf(stderr, "[parent] cannot find '%s' in cgroup path: %s\n", needle, cg_path);
return;
}
// 找到 "/user-1000.slice" 的结束位置
char *after = p + strlen(needle);
// 计算前缀:从开头到 after(含),即 "/user.slice/user-1000.slice"
size_t prefix_len = (size_t)(after - cg_path);
if (safe_snprintf(delegated_rel, sizeof(delegated_rel),
"%.*s/user@%u.service",
(int)prefix_len, cg_path, (unsigned)uid) < 0) {
fprintf(stderr, "[parent] delegated path too long\n");
return;
}
if (safe_snprintf(delegated_abs, sizeof(delegated_abs),
"/sys/fs/cgroup%s", delegated_rel) < 0) {
fprintf(stderr, "[parent] absolute delegated path too long\n");
return;
}
printf("[parent] delegated cgroup: %s\n", delegated_abs);
// ---------- 3. 确认目录存在且 memory 控制器可用 ----------
if (stat(delegated_abs, &st) != 0 || !S_ISDIR(st.st_mode)) {
fprintf(stderr, "[parent] delegated cgroup missing: %s\n", delegated_abs);
return;
}
char ctrl_path[PATH_MAX];
if (safe_snprintf(ctrl_path, sizeof(ctrl_path),
"%s/cgroup.controllers", delegated_abs) == 0) {
FILE *cf = fopen(ctrl_path, "r");
if (cf) {
char buf[256] = {0};
if (fgets(buf, sizeof(buf), cf) && !strstr(buf, "memory")) {
fprintf(stderr,
"[parent] memory controller not delegated: %s\n"
"[parent] enable via: systemctl edit user@.service\n",
buf);
fclose(cf);
return;
}
fclose(cf);
}
}
// ---------- 4. 关键一步:把父进程自己移入委托根 ----------
// 解决 cgroup v2 containment rule:只有当父进程和目标 cgroup
// 在同一个委托子树内,才能把子进程写进目标 cgroup。
if (safe_snprintf(path, sizeof(path),
"%s/cgroup.procs", delegated_abs) == 0) {
snprintf(val, sizeof(val), "%d", getpid());
if (write_file(path, val) == 0) {
printf("[parent] moved self into %s\n", delegated_abs);
} else {
// 通常不需要,但若失败也不致命,继续尝试
printf("[parent] note: move self into delegated cgroup failed: %s\n",
strerror(errno));
}
}
// ---------- 5. 在委托根下创建容器子 cgroup ----------
if (safe_snprintf(dir, sizeof(dir), "%s/container_%d",
delegated_abs, (int)pid) < 0) {
fprintf(stderr, "[parent] container cgroup path too long\n");
return;
}
if (mkdir(dir, 0755) < 0 && errno != EEXIST) {
fprintf(stderr, "[parent] mkdir %s failed: %s\n", dir, strerror(errno));
return;
}
// ---------- 6. 在委托根上启用 memory 控制器 ----------
if (safe_snprintf(path, sizeof(path),
"%s/cgroup.subtree_control", delegated_abs) == 0) {
if (write_file(path, "+memory") < 0) {
// 已经启用或没权限都继续,写 memory.max 才是关键
printf("[parent] note: enable subtree_control +memory failed: %s\n",
strerror(errno));
}
}
// ---------- 7. 写 memory.max ----------
if (safe_snprintf(path, sizeof(path), "%s/memory.max", dir) < 0) {
fprintf(stderr, "[parent] memory.max path too long\n");
rmdir(dir);
return;
}
snprintf(val, sizeof(val), "%lu", limit);
if (write_file(path, val) < 0) {
fprintf(stderr, "[parent] write %s failed: %s\n", path, strerror(errno));
rmdir(dir);
return;
}
// ---------- 7.1 禁用 swap(关键补充) ----------
if (safe_snprintf(path, sizeof(path), "%s/memory.swap.max", dir) == 0) {
if (write_file(path, "0") == 0) {
printf("[parent] memory.swap.max = 0 (swap disabled for container)\n");
} else {
printf("[parent] note: set memory.swap.max=0 failed: %s\n",
strerror(errno));
}
}
// ---------- 8. 把子进程放进 cgroup ----------
if (safe_snprintf(path, sizeof(path), "%s/cgroup.procs", dir) < 0) {
fprintf(stderr, "[parent] cgroup.procs path too long\n");
rmdir(dir);
return;
}
snprintf(val, sizeof(val), "%d", (int)pid);
if (write_file(path, val) < 0) {
fprintf(stderr, "[parent] write %s failed: %s\n", path, strerror(errno));
rmdir(dir);
return;
}
// ---------- 9. 记录路径供退出时清理 ----------
if (safe_snprintf(g_cgroup_path, sizeof(g_cgroup_path), "%s", dir) < 0) {
fprintf(stderr, "[parent] g_cgroup_path too long, cleanup skipped\n");
g_cgroup_path[0] = '\0';
}
printf("[parent] cgroup v2 memory.max = %lu bytes\n", limit);
printf("[parent] cgroup path = %s\n", dir);
}
/* ============================================================
* 子进程:容器初始化
* ============================================================ */
int child_main(void *arg) {
struct container_args *args = (struct container_args *)arg;
// 让父进程有时间写 uid_map / gid_map(改善 uid=65534 显示)
usleep(200 * 1000);
printf("\n========== container start ==========\n");
printf("[child] pid=%d\n", getpid());
printf("[child] uid=%d gid=%d\n", getuid(), getgid());
system("grep Cap /proc/self/status");
printf("[child] make mount private\n");
if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) < 0) {
perror("mount private");
return -1;
}
printf("[child] mount private success\n");
char *rootfs = args->rootfs;
printf("[child] bind rootfs %s\n", rootfs);
if (mount(rootfs, rootfs, NULL, MS_BIND | MS_REC, NULL) < 0) {
perror("bind rootfs");
return -1;
}
printf("[child] bind success\n");
char proc_path[PATH_MAX];
if (safe_snprintf(proc_path, sizeof(proc_path), "%s/proc", rootfs) < 0) {
fprintf(stderr, "[child] proc_path too long\n");
return -1;
}
mkdir(proc_path, 0755);
printf("[child] mount proc on new root\n");
if (mount("proc", proc_path, "proc",
MS_NOSUID | MS_NODEV | MS_NOEXEC, NULL) < 0) {
perror("mount proc before pivot");
if (mount("proc", proc_path, "proc", 0, NULL) < 0) {
perror("mount proc before pivot (simple)");
return -1;
}
}
printf("[child] proc mounted on new root\n");
char put_old[PATH_MAX];
if (safe_snprintf(put_old, sizeof(put_old), "%s/put_old", rootfs) < 0) {
fprintf(stderr, "[child] put_old too long\n");
return -1;
}
mkdir(put_old, 0755);
printf("[child] pivot_root\n");
if (syscall(SYS_pivot_root, rootfs, put_old) < 0) {
perror("pivot_root");
return -1;
}
printf("[child] pivot_root success\n");
chdir("/");
printf("[child] umount old root\n");
if (umount2("/put_old", MNT_DETACH) < 0) {
perror("umount old root");
}
rmdir("/put_old");
setenv("JAVA_HOME", "/opt/jdk", 1);
setenv("PATH", "/opt/jdk/bin:/usr/bin:/bin", 1);
setenv("LD_LIBRARY_PATH", "/opt/jdk/lib:/opt/jdk/lib/jli", 1);
char hostname[64] = {0};
FILE *f = fopen("/etc/hostname", "r");
if (f) {
if (fgets(hostname, sizeof(hostname), f)) {
hostname[strcspn(hostname, "\n")] = '\0';
syscall(SYS_sethostname, hostname, strlen(hostname));
printf("[child] set hostname from file: %s\n", hostname);
}
fclose(f);
} else {
perror("fopen /etc/hostname");
}
printf("[child] exec bash\n");
char *argv[] = { "/bin/bash", NULL };
execv("/bin/bash", argv);
perror("exec bash");
return -1;
}
/* ============================================================
* 父进程
* ============================================================ */
int main(int argc, char **argv) {
if (argc != 2) {
printf("usage:\n%s rootfs\n", argv[0]);
return -1;
}
child_stack = malloc(STACK_SIZE);
if (!child_stack) {
perror("malloc");
return -1;
}
struct container_args args;
args.rootfs = argv[1];
int flags = CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUTS |
CLONE_NEWIPC | CLONE_NEWNET | CLONE_NEWUSER | SIGCHLD;
printf("[parent] clone with CLONE_NEWUSER\n");
pid_t pid = clone(child_main, child_stack + STACK_SIZE, flags, &args);
if (pid < 0) {
perror("clone");
free(child_stack);
return -1;
}
printf("[parent] child pid = %d\n", pid);
// 内存限制
setup_memory_limit(pid);
// 设置 uid_map / gid_map
char path[256];
FILE *f;
snprintf(path, sizeof(path), "/proc/%d/uid_map", (int)pid);
f = fopen(path, "w");
if (f == NULL) {
perror("fopen uid_map");
} else {
fprintf(f, "0 %d 1", getuid());
fclose(f);
printf("[parent] uid_map written: 0 -> %d\n", getuid());
}
snprintf(path, sizeof(path), "/proc/%d/gid_map", (int)pid);
f = fopen(path, "w");
if (f == NULL) {
perror("fopen gid_map");
} else {
fprintf(f, "0 %d 1", getgid());
fclose(f);
printf("[parent] gid_map written: 0 -> %d\n", getgid());
}
// 验证
printf("\n[parent] Verifying uid_map content:\n");
snprintf(path, sizeof(path), "/proc/%d/uid_map", (int)pid);
f = fopen(path, "r");
if (f != NULL) {
char line[256];
while (fgets(line, sizeof(line), f) != NULL) {
printf(" uid_map: %s", line);
}
fclose(f);
} else {
perror("fopen uid_map for read");
}
printf("[parent] Verifying gid_map content:\n");
snprintf(path, sizeof(path), "/proc/%d/gid_map", (int)pid);
f = fopen(path, "r");
if (f != NULL) {
char line[256];
while (fgets(line, sizeof(line), f) != NULL) {
printf(" gid_map: %s", line);
}
fclose(f);
} else {
perror("fopen gid_map for read");
}
// 等待
int status;
waitpid(pid, &status, 0);
printf("[parent] container exit with status %d\n", status);
free(child_stack);
// 清理 cgroup
if (g_cgroup_path[0]) {
// 如果还有进程(僵尸等),先尝试用 cgroup.kill 强杀
char kill_path[PATH_MAX];
if (safe_snprintf(kill_path, sizeof(kill_path),
"%s/cgroup.kill", g_cgroup_path) == 0) {
write_file(kill_path, "1");
}
if (rmdir(g_cgroup_path) < 0) {
fprintf(stderr, "[parent] rmdir %s failed: %s\n",
g_cgroup_path, strerror(errno));
} else {
printf("[parent] cgroup %s removed\n", g_cgroup_path);
}
g_cgroup_path[0] = '\0';
}
return 0;
}