手写迷你 Docker:用 Namespace + Cgroup v2 实现一个简单容器

本文基于一份约 400 行的 C 源码,拆解一个"迷你 Docker"的实现:它没有镜像分层、没有 veth 网络、没有 seccomp,但已经具备容器最核心的几块拼图:Namespace 隔离、pivot_root 切换根文件系统、uid_map/gid_map 用户映射,以及 cgroup v2 内存限制。读完后,你会明白 Docker、runc 这类工具底层到底在做什么。


一、整体目标

这个程序的目标很明确:

bash

bash 复制代码
./container_memory_limit /path/to/rootfs

启动后,它会:

  1. 用 clone 创建子进程,并给子进程一套新的 Namespace;
  2. 父进程给子进程设置 cgroup v2 内存上限,例如 64MB;
  3. 父进程写 uid_map / gid_map,让容器内 root 映射到宿主普通用户;
  4. 子进程把 rootfs 挂载成新根,挂载 /proc,执行 pivot_root;
  5. 子进程执行 /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 内存限制的效果。


五、局限与改进

这份代码已经能跑通核心链路,但离生产级容器还有距离:

  1. 没有 PID 1 信号处理:bash 退出后容器就结束,僵尸进程可能没人回收。
  2. 没有 seccomp / capabilities 降权:容器内权限仍然较大。
  3. 网络未配置 :CLONE_NEWNET 创建了新网络栈,但没有 veth、网桥、IP,容器内基本没网。
  4. rootfs 需手工准备:没有镜像拉取、解压、分层。
  5. cgroup 清理较简单 :退出时写 cgroup.kill 再 rmdir,异常场景可能残留。
  6. 依赖 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;
}
相关推荐
一木 之林3 小时前
DeepSeek Agent 开发(三)
java·大数据·linux
大猫会长3 小时前
调用codebuddy集成的supabase授权白屏解决方法
linux·运维·服务器
羔羊++4 小时前
29_实验二十八_buildroot配置与编译
linux
LoneEon4 小时前
CentOS7 部署 Nacos3.x 集群实战:从注册中心到 AI 管理中心
linux·人工智能·nacos
传人4 小时前
为什么 Xshell 中可以通过java -version查询到java 版本号码,Electerm 却查不到?
java·linux·服务器
鬼手点金5 小时前
opencode-全能开发者配置
java·linux·服务器·前端·javascript·学习·前向传播
xixiaoyunya6 小时前
虚拟机备份怎么做:镜像级与文件级搭配的完整实操方案
linux·运维·网络
墨家句子6 小时前
服务器被反复尝试登录之后:fail2ban 加密钥登录的五道加固
linux·后端
阿明66 小时前
进程替换【Linux】
linux·运维·服务器