因为容器需独立命名空间,fork无法隔离而clone支持CLONE_NEWPID等标志;必须root权限、正确组合flags、挂载/proc、用pivot_root替代chroot,并手动实现PID 1 init循环。

为什么直接用 clone 而不是 fork
因为容器需要独立的命名空间(如 PID、mount、UTS),而 fork 创建的子进程共享父进程的命名空间,无法满足隔离要求。clone 允许传入 CLONE_NEWPID、CLONE_NEWNS 等标志,是进入新命名空间的唯一入口。
常见错误是漏掉 CLONE_NEWCGROUP 或 CLONE_NEWUSER 导致挂载失败或权限拒绝,尤其在较新内核上;另外必须配合 unshare 或提前调用 setns 才能真正切换上下文。
- 必须以 root 或具备
CAP_SYS_ADMIN能力运行,否则clone会返回EPERM - 使用
syscall.Syscall6调用clone时,第三个参数(flags)要按位或组合,比如syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWUTS - 子进程启动后需立即调用
syscall.Sethostname和syscall.Mount,否则 hostname 不生效、/proc 挂载点仍指向宿主
如何让子进程真正成为 PID 1 并接管 init 行为
在 CLONE_NEWPID 下,子进程在新命名空间中看到自己的 PID 是 1,但它默认不会自动回收僵尸进程——Go 运行时本身不处理 SIGCHLD,也不会调用 wait4。若不手动处理,子进程 fork 出的后代退出后会变成僵尸。
解决方式不是依赖 Go 的 exec.Command,而是用 syscall.Clone 后在子进程中自行实现最小 init 循环:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
for {
var wstatus syscall.WaitStatus
pid, err := syscall.Wait4(-1, &wstatus, 0, nil)
if err == nil && pid > 0 {
// 清理僵尸
} else if err != syscall.EINTR {
break
}
}
- 不能用
os/exec启动用户命令,必须用syscall.Exec直接替换当前进程镜像,否则 PID 1 进程会退出,整个命名空间崩溃 - 如果想支持信号转发(如
kill -9 1),需在 init 循环里监听os.Signal并显式调用syscall.Kill转发给业务进程 - 某些发行版(如 Alpine)的
/sbin/init依赖/dev/initctl,需提前mknod或跳过
rootfs 挂载失败的三个典型原因和对应检查项
mount: permission denied (are you root?) 或 invalid argument 错误大多不是权限问题,而是挂载选项或文件系统状态不匹配。
- 未执行
syscall.Mount("", "/path/to/rootfs", "bind", syscall.MS_BIND|syscall.MS_REC, "")就直接 bind mount 子目录:必须先递归绑定根目录,再在其下做私有挂载 - 忘记
syscall.Mount("none", "/path/to/rootfs/proc", "proc", 0, ""):/proc 不是自动挂载的,且必须在 chroot 或 pivot_root 后立即挂载,否则ps等工具失效 - 目标路径(如
/tmp/mycontainer/rootfs)不在同一文件系统上:pivot_root 要求 newroot 和 putold 在同一挂载点,否则报EINVAL;建议统一用 tmpfs 或 overlayfs 测试
为什么 pivot_root 比 chroot 更适合容器运行时
chroot 只改变根路径视图,不隔离挂载命名空间,宿主挂载点仍可见;pivot_root 则把新 rootfs 提升为当前命名空间的真正根,并将旧 root 移到指定子目录(如 /old),后续可安全 umount。
但 Go 标准库没有封装 pivot_root,必须用 syscall.Syscall 调用:
syscall.Syscall(syscall.SYS_PIVOT_ROOT, uintptr(unsafe.Pointer(&newroot[0])), uintptr(unsafe.Pointer(&putold[0])), 0)
- 调用前必须确保
newroot和putold都是绝对路径,且putold是newroot的子目录(如"/mnt/rootfs"和"/mnt/rootfs/old") - 调用后要立刻
syscall.Chdir("/"); syscall.Umount("/old", 0),否则旧 root 占用无法释放 - 很多教程忽略
MS_PRIVATE设置:在 pivot_root 前对 newroot 执行syscall.Mount("", newroot, "", syscall.MS_PRIVATE, ""),否则子挂载会泄漏到宿主
/proc/self/ns/ 下 inode 号是否变化;挂载是否隔离用 findmnt --uniq;PID 是否重置用 ls /proc/[0-9]*/comm | head -n3。这些比任何日志都直接。

















