本节摘要:Docker 与 Podman 最根本的差异可以用一张进程树图讲完:Docker 的容器进程是 dockerd 的后代,Podman 的容器进程是用户命令进程的后代。本节用同一台机器上的对照实验验证这一点,再推演这个差异的三层工程后果——单点行为、权限继承、退出码语义。这是全册技术含量最高的"一图流",后续各章都会回到它。
空谈架构不如直接观察。准备一台装了两种引擎的 Linux 机器(顺序无所谓,两者互不干扰——这本身就是个信号),分别用 detach 模式启动一个长驻容器,然后看进程树:
# Docker 侧:启动容器 docker run -d --name web-d nginx:alpine # 9f8c...(容器 ID) # Podman 侧:rootless 启动同名容器 podman run -d --name web-p nginx:alpine # 观察进程树(截选关键行) ps auxf | grep -E "dockerd|conmon|nginx|podman" | grep -v grep # root 1187 dockerd # root 1423 \_ containerd-shim ... /docker/9f8c.../web-d # root 1501 \_ nginx: master process # 1000 2105 conmon ... -c web-p # 231073 2155 crun ... web-p # 1000 2160 \_ nginx: master process
几行输出里藏着全部要点。Docker 一侧:nginx 的父进程是 containerd-shim,再往上是 root 的 dockerd——容器进程的血统来自守护进程。Podman 一侧:容器里的 nginx 在宿主机上显示为 UID 231073(映射出来的高位 UID,见第 2.2 节),它的监控者 conmon 属于普通用户 1000;没有 root 进程参与。
更尖锐的对照是"客户端退出后谁认识这些容器"。Docker 客户端进程早已退出,容器仍归 dockerd 管;Podman 客户端进程也退出了,容器归 conmon 与用户级状态文件管,与任何"Podman 主进程"无关——因为根本没有主进程。
进程树形状决定了故障半径。Docker 的模型里,所有容器的管理关系汇聚到 dockerd 一点,单进程故障的影响面是"全部容器"。Podman 的模型里管理关系是一棵棵独立的子树:每个容器有自己的 conmon,故障半径天然是"一个容器"。
这与第 1.1 节的账单直接呼应,但注意别把话说满——去掉单点不等于去掉所有共享组件。rootless Podman 里所有用户级的容器操作要经过用户级 systemd 实例(user session)保持会话与子进程的托管,如果用户会话整体注销,容器进程的存活依赖 loginctl 的配置(第 7 章给方案)。对比要诚实,这才是工程判断的基础。
Fork-Exec 模型下,子进程默认继承父进程的权限上下文。这个朴素的 Unix 规则解释了 Podman 的安全格局:普通用户的 podman run fork 出的整条链(conmon、crun、容器进程)都以普通用户身份起家,容器内需要的特权(比如容器里当 root)由用户命名空间映射制造,而不是由宿主机真特权支撑。权限的"来源"被收窄到用户本身。
Docker 一侧则相反:dockerd 以 root 运行,容器进程的血统是 root,所谓隔离全靠命名空间与安全配置"往下限制"。一个是"从普通身份向上借用",一个是"从 root 身份向下限制"——两种哲学的分野,在第 6 章安全审计里还会以 SELinux、capabilities 的形态再次出现。
最容易被忽略但最影响脚本体感的一层。Fork-Exec 下,容器主进程的退出码沿进程链直接回到 podman run 的调用者:
# 前台运行一个注定失败的容器,看退出码 podman run --rm alpine:3.19 sh -c 'exit 42' echo $? # 42 # 退出码原样返回。bash 脚本可以像判断普通命令一样判断容器结果 # 同样的事在 Docker 里 docker run --rm alpine:3.19 sh -c 'exit 42' echo $? # 42 # 这里 Docker 也返回 42——但路径不同:退出码经 dockerd 转述给客户端。 # 差别在信号处理:容器内进程收到的信号由 shim 转发, # 而 Podman 的转发链短一层(conmon 直接转发), # 停止延迟与信号丢失概率在长链路上更难排查
CI 脚本、Makefile、Ansible 任务里这类语义差异最常被踩到。原则记一条:Podman 把容器当子进程对待,所以一切"进程能给你的东西"(退出码、信号、终端)都更接近原生语义。
顺着这条原则再推一步,还有一个日常会遇到的体现:终端与会话的交互。在 Podman 里以交互模式进入容器(podman run -it),你的终端直连容器主进程的标准输入输出,ctrl-c 发出的 SIGINT 沿进程链抵达容器进程,行为与在宿主机 shell 里终止一个前台程序一致。Docker 的同款操作要经过客户端与守护进程两层中转,绝大多数时候无感,但在容器进程吞信号、需要连环 ctrl-c 的场景里,你会先看到客户端与守护进程的拉扯(容器标记为正在停止),而不是进程对信号的直接反应。这类细节单独看都小,叠加起来就是两种"体感":一个像操作进程,一个像操作远程服务。
