🎧 Listen to this article: हिंदी · English · தமிழ் · తెలుగు · ಕನ್ನಡ · മലയാളം · ଓଡ଼ିଆ · 日本語 · 中文
🌍 Read this in your language: हिंदी · தமிழ் · తెలుగు · ಕನ್ನಡ · മലയാളം · ଓଡ଼ିଆ · 日本語 · 中文
知与行之间的差距
你可以在 Docker 考试中拿高分,对各种术语——namespaces、cgroups、镜像(images)、图层(layers)、PID 1、Kubernetes Pods——侃侃而谈,但在真正尝试构建容器时依然不知所措。这是最近在 DEV Community 上分享故事的一位开发者带来的教训,也提醒了我们理论与实践往往脱节。这一点在当下尤为重要,因为容器化无处不在——Docker、Kubernetes 以及成千上万的部署系统都建立在这些概念之上,而直到你在现实中亲身体验并撞墙之前,它们感觉一直很抽象。
第一个命令:让人保持谦逊的开始
一切始于一个简单的尝试:使用 unshare 命令。第一次尝试是:
bash sudo unshare -p 1 test
错误: unshare: failed to execute 1: No such file or directory
参数标志(flags)错了。该命令要求系统执行一个名为“1”的程序,但它并不存在。在构建任何东西之前,甚至在遇到真正的问题之前,键盘和文档对于应该发生什么就已经有了不同的理解。
第 1 部分:Namespaces 与 PID 1
第一次真正的尝试是在一个新的 PID(进程 ID)namespace 中运行一个进程,并证明它将自己视为 PID 1——即进程树的根。于是运行了以下命令:
bash sudo unshare --pid bash
然后在该 shell 内部:
bash echo $$
预期输出:1。实际输出:25184。
这没有奏效。那不是 PID 1;它只是来自宿主机的父进程 ID。规则很简单但反直觉:PID namespaces 适用于子进程,而不是调用 unshare的进程。你需要进行 fork。诞生在新 namespace 中的第一个子进程会成为 PID 1。
所以可行的版本是:
bash sudo unshare --pid --fork bash echo $$
现在输出是 1。Shell 认为自己是进程树的根。从内部看,一切感觉都不一样了。
接下来的惊喜随之而来。在内部运行 ps 显示:
PID PPID COMMAND 25310 25304 bash 25344 25310 ps
但 Shell 却声称自己是 PID 1。这讲不通。启示是: ps 并不会向内核询问纯粹的“存在哪些进程?”问题。它读取的是文件。如果 /proc 依然指向宿主机的进程文件系统,你的工具就会向你撒谎。它们将显示宿主机的编号方案。
修复方法是在 namespace 内部重新挂载 /proc :
bash mount -t proc proc /proc ps -o pid,ppid,comm
现在它显示:
PID PPID COMMAND 1 0 bash 7 1 ps
就在那一刻,顿悟来了。Namespace 提供了真正的隔离,但在文件系统视图改变之前,工具无法看到这一点。隔离与可见性是两码事。
UTS namespace——用来控制主机名(hostname)——理解起来更直观。在宿主机上运行 hostname 显示一个名称。在新的 UTS namespace(通过 sudo unshare --uts bash创建)内部,修改它后显示出不同的内容。回到宿主机,它又恢复成原样。一台机器,一个内核,三种不同的视图。
第 2 部分:文件系统交接
在搞定 namespaces 之后,下一个版本应该赋予进程它自己的文件系统:一个带有 BusyBox 和 shell 的 rootfs(根文件系统)。这非常有容器的感觉。
第一个错误很直接:
exec /bin/sh: no such file or directory
Shell 不在脚本所说的位置。这个问题被修复了。但接着:
./rootfs/bin/busybox: cannot execute: required file not found
这个错误残酷之处在于文件明明就在那里。你可以列出它,也可以看到它,但内核依然拒绝运行它。使用 file 命令揭示了真相:
ELF 64-bit LSB pie executable, ARM aarch64, dynamically linked, interpreter /lib/ld-musl-aarch64.so.1, stripped
二进制文件确实在。但它所需的解释器(动态链接器)在旧世界中不可用。Linux 并不是说这个文件不存在,而是在说“从这里,我无法加载这个 ELF 所需的解释器。”
解决办法不是将 BusyBox 静态化,而是将 Alpine 作为新的根文件系统,以便解释器位于正确的路径上。但首先,这种过渡需要一个桥梁——一个可以在文件系统切换之前和切换期间运行的工具。BusyBox 有两种形式:一种用于 Alpine 内部的动态版本,另一种用于交接的静态版本:
bash /bin/busybox pivot_root . put_old
静态版本的 BusyBox 是关键,它可以在系统完全切换世界之前执行 pivot_root 。
在 Alpine 成为新的 root 之后,出现了更多的意外。Bash 依然记得旧文件系统中的命令路径。当它尝试运行 mount时,它去 /usr/bin/mount 中寻找,而那个世界刚刚被驱逐:
bash: /usr/bin/mount: No such file or directory
修复方法是 hash -r,它可以清除命令缓存。Bash 在旧世界里做出了决定,并且无法放下它。
第 3 部分:Mac 带来的复杂状况
设置环境并不是普通的 Linux 笔记本电脑。它是 Apple Silicon Mac → 特权 Ubuntu 容器 → 从 macOS 挂载的仓库。这意味着无论是否有人希望如此,virtiofs(用于虚拟化的文件系统透传)都参与了进来。
在现在已是根文件系统的 Alpine 内部,在 Mac 共享挂载上通过符号链接(symlinks)执行可能会失败并提示“Permission denied”,而直接调用 BusyBox 则可以正常工作:
bash ls
sh: ls: Permission denied
/bin/busybox ls
(works)
文件就在那里。通过那些符号链接执行才是奇怪的地方。修复方法朴实无华却很正确:将 rootfs 移动到容器原生的路径下,然后从那里重试。不要强求 Mac 共享挂载像普通的 Linux 那样工作。
第 4 部分:pivot_root 有它自己的规矩
即使在经历了这一切之后, pivot_root 本身给出的教训还没结束。错误信息:
pivot_root: invalid argument
新的 root 必须是一个挂载点。旧的 root 需要有去处。所以流程是:
- 将新的 root 绑定挂载(bind-mount)到自身
- 创建一个
oldroot目录 - 调用
pivot_root(newroot, oldroot) - 将工作目录更改为新的 root
- 卸载旧的 root
当它终于成功运行起来时,回报虽微小却很完美:
bash cat /etc/os-release NAME="Alpine Linux" ID=alpine VERSION_ID=3.24.1
这只是一个文本文件。但现在它是容器成功工作的证明。
总结
从零开始构建容器教会了课程永远无法讲透的东西:懂得理论与亲眼看着它实时失败之间的距离。Namespaces、cgroups 以及其余概念都是真实的。它们的工作方式与文档所载完全一致。但是,从“我理解这个概念”到“我能让它跑起来”的路径,充斥着错误信息、Bash 缓存、文件系统权限,以及一种认知:如果文件系统不对,你的工具就会向你撒谎。
优点
- 动手实践能够以单独学习无法比拟的方式深化理解
- 真实的错误能教会你文档中经常跳过的限制条件
- 从零构建揭示了现代容器工具是如何隐藏复杂性的
- 理解 namespaces、文件系统和解释器,能让 Docker 和 Kubernetes 不再那么神秘
缺点
- 学习曲线陡峭,包含许多细小且令人困惑的错误
- 环境因素(如 macOS 上的 virtiofs)增加了不可预测的复杂性
- 与直接使用 Docker 相比,这个过程非常缓慢
- 许多边缘情况(Bash 哈希缓存、符号链接权限)在文档中并不明显
注意事项
本文具有教育性质,描述了从第一性原理构建容器时的真实学习体会。文中展示的命令和概念与源材料一致。这不是一个生产级别的容器运行时;它是一个用于理解容器工作原理的教学工具。在生产环境依赖任何容器系统之前,请参阅官方文档和最佳实践。在你的环境中实施任何内容之前,请对照原始来源核实所有论述。
常见问题
- 什么是 PID namespace?为什么它在容器中很重要?
- 为什么调用 unshare 的进程不会自动成为 PID 1?
- /proc 文件系统与容器有什么关系?
- pivot_root 与 chroot 有何不同?
- 为什么重新挂载 /proc 会改变 ps 显示的内容?
- BusyBox 在容器构建中的作用是什么?
- 在切换到新的根文件系统时,为什么动态解释器很重要?
- virtiofs 如何使 macOS 上的容器设置变得复杂?
标签
#containers #linux #docker #devops #namespaces #cgroups #filesystem #learning
API Security Testing Checklist
A practical workflow for testing authentication, authorization, input handling, business logic, and evidence without losing track of scope.
Free. No spam — unsubscribe in one click.


Responses
Sign in to leave a response.