容器 11:改一个文件为何可能增加整层
10 的 layer digest描述分发的变更集;运行时让应用读取的却是文件系统路径。把 OCI 的 layer tar 与 Linux OverlayFS 的 lower/upper/merged 看成同一件东西,就无法解释删除文件后为什么旧层仍占空间。 镜像内容与联合挂载 一个 OCI 层记录相对于下层的增加、修改、删除。顺序应用变化才能得到最终文件树;压缩层的 digest 与解压后 DiffID 属于制品层证据。OverlayFS 则是 Linux 内核的具体叠加文件系统:只读的 lowerdir、可写的 upperdir 和工作目录一起提供 merged 视图。应用读 /app/config 时读的是合并后当前可见的文件;首次写 lower 中的文件可能 copy-up 到 upper,随后修改 upper 副本。删除 lower 中的文件会在 upper 中留下 whiteout 语义以隐藏它,但不会从已经分发的旧 OCI blob 删除字节。不能把 union mount 当作容器镜像唯一可用的 snapshotter 实现。 两种“层”分别在什么时候出现 OCI...
容器 10:OCI 镜像到底保存了什么
09已经能让自编探针作为进程退出,但无法用一个可分发制品重建其根文件系统。OCI 镜像表示的是若干内容寻址的字节和元数据,而非正在运行的进程。以这一区别为出发点读 `index.json`,比背一串 Docker 命令更容易定位“拿到的是哪个制品”。 从索引走到字节 本地 OCI image layout 的入口是 oci-layout 与 index.json。索引中的 descriptor 用 media type、digest、size 指向 manifest,可能列有平台信息;manifest 再指向 image config 与一组层。config 描述操作系统、架构、根文件系统的 diff_ids 及进程默认参数等;层可以是 tar gzip 的变更集。给定 sha256:<hex>,实际 blob 路径是 blobs/sha256/<hex>,应以文件真实字节核验 digest 和 size,不能只看 JSON 里字符串长得像摘要。 一个特别容易混淆的点是压缩层 descriptor 的 digest 与 config 中 DiffID:前者...
容器 09:组合隔离机制时启动和清理的顺序
前八篇分别讨论进程、namespace、身份、资源和权限。如果只把它们列在命令行上,很容易遗漏“父进程何时写映射”“谁等待子进程”“初始化失败后对象如何退出”。这篇用一个受限教学启动器展示顺序,并明确它没有实现的隔离,避免误当成生产容器。 一个可核查的局部链路 源码共用源码包内的 teaching_launcher.c 固定执行同目录内自编译的 process_probe,没有接受用户指定的命令行、镜像或任意路径。启动器记录外侧 UID/GID,先 unshare(CLONE_NEWUSER),将内部 UID/GID 0 映射到原外侧身份;在写 GID 映射之前向 setgroups 写 deny。随后建立新的 UTS、IPC namespace,在私有 UTS 中设置测试 hostname。fork 后只有子进程 exec 自编程序,父进程 waitpid 并返回子进程状态。设置顺序的核心是写映射时“当前进程处于哪个 namespace”,而不是把一串 unshare 选项当成 magic flag。 从当前源码画出真实的控制流 第一步在外侧读取实际 UID/GID;这两个数字...
容器 08:namespace 之外还需要什么权限边界
05 的 UID 映射说明谁拥有某个身份,06说明资源预算。但“内部是 root、有独立 hostname”不等于可信边界:进程发起系统调用时仍会进入宿主内核。允许执行什么操作,还要逐项解释 capability、seccomp、`no_new_privs` 与实际启用的 LSM。 把拒绝归还给负责的层 capability 将特权操作拆成较细粒度的授权,内核权限检查会结合针对的 user namespace。CAP_SYS_ADMIN 在新 user namespace 中的有效位不等于初始 namespace 的万能通行证,挂载还受文件系统与上层策略约束。seccomp BPF 按系统调用及可检查参数限制调用面,不识别“这个路径属于秘密”这样的完整安全语义;内核文档明确说过滤器本身不是 sandbox。no_new_privs 禁止 exec 后取得比此前更多的特权,不能据此说所有操作都会拒绝。SELinux、AppArmor 等 LSM 是否启用、具体 profile 如何拒绝,要按专用 VM 的配置和审计日志核查,不能套用另一台机器的默认值。 从系统调用走到目标对象 进程...
容器 07:内存超限与 I/O 变慢不是一种失败
06已经区分 CPU 带宽与相对竞争。内存、进程数量和块设备 I/O 也通过不同控制器暴露各自的失败证据;把“容器慢/死了”统一解释为 OOM,会错过问题所在。 内存约束不是单个数字 cgroup v2 中 memory.current 提供当前使用量,memory.max 为硬上限,memory.high 引导回收与节流;memory.events 的 high、max、oom、oom_kill 记录不同事件。超过 memory.high 不意味着立即杀进程;达到 memory.max 也不能不看回收成败就声称发生了 OOM kill。计数器是累积的,需比较实验前后,记录关联进程和层级。先在专用 VM 给单一小组设置有界内存与申请上限,绝不在共享宿主引发全局压力;每轮记录进程退出状态、memory.events、memory.current 和 dmesg 是否有权限取得相应事件。 pids.max 限制组内任务数量,达到时新的 fork 可以失败,并非旧进程自动被终止。io.max 规定特定设备的 BPS/IOPS 上限,需要先确认设备号、后端以及页缓存是否掩盖了真实块 I/...
容器 06:CPU 配额与竞争权重为什么不同
05 的身份映射不会阻止一个进程消耗 CPU。隔离“看见什么”、规定“谁是谁”和限制“能用多少”是三个独立问题。cgroup v2 的层级与控制器负责第三个问题;套用 v2 的文件名到本机的 v1 只读层级,会得到看似合理但根本没生效的配置。 带宽、竞争与亲和性 cpu.max 是带宽额度和周期:例如在支持该接口的 cgroup 中写 50000 100000 表示每 100000 微秒最多使用 50000 微秒配额,具体跨多个 CPU 的行为还要看周期和并发。达到额度会节流,读取 cpu.stat 的 nr_throttled、throttled_usec 观察变化;延迟增加却不一定是因为节流,还可能是排队、存储或网络。cpu.weight 是父层级内的相对权重,主要在同一竞争条件下分配份额,空闲时并不等于硬上限。cpuset.cpus.effective 表示可运行 CPU 集,CPU 集缩小不等于改变权重;还需比较宿主拓扑与亲和性。 控制器的可见性与委派也有层级条件。只有父层启用对应的 cgroup.subtree_control、将可写子层委派给实验用户并满足内核规则,才...
容器 05:容器里的 root 究竟是谁
程序在内部打印 uid=0,并不意味着它在宿主初始 user namespace 中获得了宿主 root 的身份。前提是内核如何把内部 UID/GID 映射到外侧身份,目录所有权从哪一个 user namespace 解释,以及该操作的权限检查针对哪个对象。先把04 的路径/挂载视图与身份映射分开;rootfs 的路径变化不能自动改变用户映射。 UID 是映射关系,不是全局标签 /proc/<pid>/uid_map 的每行含“内部起始 UID、外部起始 UID、数量”,gid_map 同理。进入新的 user namespace 后,内部 root 可以对应外侧非特权 UID,具体权限由映射、文件系统、被检查的 namespace 及内核能力决定;映射不意味着访问所有外侧 root 拥有的目录。写 gid_map 通常还涉及 /proc/.../setgroups 的限制,不能盲目照搬 uid_map 的写法。把进程设为容器内普通 USER 与以非特权身份运行 daemon 并使用 user namespace(rootless)是两个不同维度。 读映射时...
容器 04:换了根目录,为什么仍能看到旧文件
/ 是进程解析路径的起点,不是磁盘的物理边界。仅改变根目录无法证明进程已失去对旧文件系统的引用;父进程打开的文件描述符、当前工作目录和挂载传播均可能影响结果。此前02只改变 UTS/IPC 视图,这一篇把根、挂载树与已打开 FD 分开。 三种不同操作 mount namespace 让进程拥有自己的挂载点集合;新建它时通常复制原先的挂载列表,不能说新 namespace 自动生成了全新的文件系统。挂载传播定义一侧后续挂载事件是否能影响另一侧,因此先检查 /proc/self/mountinfo 的 shared/private 信息,再只在专用私有挂载树做写入实验。chroot 主要改变调用者的根目录路径解析起点;已经打开的旧根 FD 不会因此自动关闭,也不能把它当成安全沙箱。pivot_root 移动根挂载并把旧根移到 put_old,需要符合 mount point 和传播约束;切换之后还要决定何时卸载旧根,清理所有仍引用旧树的 FD。 路径、挂载和文件描述符不是同一个东西 调用 open("/etc/issue") 时,绝对路径从调用进程的 root 起...
容器 03:PID 1 为什么改变信号与回收行为
在 PID namespace 中第一个子进程的 PID 是 1。这个数字不仅是用户界面的显示值:该进程承担接收孤儿进程、处理终止事件的职责。如果它结束,内核会终止此 PID namespace 中的其他进程。先掌握01 的 wait/信号和02 的 namespace 身份,否则容易把“应用没有退出”误判成运行时故障。 PID 号码与进程的两个视角 PID namespace 按层级组织。内侧看到的 PID 1 在外侧仍有宿主 PID,外侧能够观察内部进程,内部不能因此查看外侧所有进程。/proc 是一个挂载出来的进程信息视图;仅调用 unshare 而沿用旧 /proc 挂载,可能读到旧 namespace 的 PID,进而产生“shell 的 $$ 是 1,/proc/self/status 却有不同号码”的矛盾。应在私有挂载环境重新挂载 procfs,才用它检查当前 PID 视图;没有这个权限,明确标记观察限制,而不是补造一致的 PID 输出。 PID 1 对某些未安装处理函数的默认信号行为有特殊规则。它并不是“永远不会响应 TERM”:安装了 TERM han...
容器 02:namespace 改变的是哪些资源视图
在一个终端里改了主机名,另一个终端为什么没变?关键不是 shell 的变量,而是两个进程引用的 UTS namespace 不同。沿01 的进程生命周期继续,把隔离看成“进程持有哪些内核对象”,而不是把容器想象成一台小型物理机。 比较视图,不比较字符串 clone 可让新进程进入新建的 namespace;unshare 使调用者与先前共享的指定 namespace 分离;setns 把调用者加入已有 namespace,但有具体的权限、类型和多线程前提。UTS 隔离 hostname 与 domainname,IPC 隔离相应的进程间通信对象;PID、mount、net 与 user 各有自己的资源集合。/proc/<pid>/ns/<type> 的链接目标包含可比对的 namespace inode。两个进程具有相同 UTS inode,表示指向同一 UTS namespace;两个进程恰好都输出 hostname=demo,却可能在不同 UTS 对象里。不能跨类型比较数字本身。 普通读者并不必在宿主得到所有 capability。unshare -U...











