Linux内核分析之文件系统-05
38.1 OverlayFS 原理与层次结构
OverlayFS 的挂载参数只有三个:lowerdir=(可多个逗号分隔)、upperdir=(恰好一个)、workdir=(与 upper 同文件系统的工作目录)。合并视图的语义由这套参数严格推导:多层只读底本 + 单层可写覆盖 + 事务辅助目录。本节拆解层模型与合并规则。
38.1.1 层模型与数据结构
// fs/overlayfs/ovl_entry.h:33-56(节选)
struct ovl_layer {
/* ovl_free_fs() relies on @mnt being the first member! */
struct vfsmount *mnt; /* 本层挂载 (直接复用 VFS 挂载!) */
/* Trap in ovl inode cache */
struct inode *trap; /* 防"层下文件被换"的陷阱 inode */
struct ovl_sb *fs;
/* Index of this layer in fs root (upper idx == 0) */
int idx; /* 层序号 (upper=0, 从下往上) */
/* One fsid per unique underlying sb (upper fsid == 0) */
int fsid; /* 底层文件系统去重 id */
/* xwhiteouts were found on this layer */
bool has_xwhiteouts;
};
struct ovl_path {
const struct ovl_layer *layer;
struct dentry *dentry;
};
struct ovl_entry {
unsigned int __numlower;
struct ovl_path __lowerstack[] __counted_by(__numlower); /* 逐层命中路径 */
};
层就是 vfsmount——OverlayFS 不理解磁盘布局,它把每个 lower/upper 目录当一次挂载持有,合并逻辑完全工作在 VFS dentry 之上(33 章抽象的又一次胜利)。fsid 对相同底层文件系统的多层去重(同一镜像的多个层通常同盘),让"跨层 copy-up 是否可省/是否可重定向"有依据。ovl_entry 记录一个文件的"逐层命中栈"——合并查找的结果物。
38.1.2 合并语义速查
读 (lookup): 从最上层往下找第一个命中者
目录例外: 所有层的同名目录"合并"显示 (目录可叠)
写 (普通文件):
在 lower → copy-up: 复制到 upper 再改 (38.2.2)
在 upper → 直接改
删除:
upper 有 → 删之
仅 lower 有 → upper 建"白障" (whiteout) 遮挡
新建: 直接 upper
rename: 跨层需要 copy-up + 白障 组合 (目录还有 redirect)
元数据 (chmod/chown/时间戳): lower 文件需 copy-up
(metacopy 特性可只拷元数据, 数据留在 lower — 38.2.2)
目录的合并例外是 OverlayFS 与"简单覆盖"的本质差异:/usr/lib 在多个镜像层都有内容,合并视图必须并集显示、逐文件再判定归属——这使目录查找必须扫描全部层(38.2.1 节),而文件查找在第一命中即止。
38.1.2 workdir 与索引
// fs/overlayfs/super.c:311(workdir 建立)
static struct dentry *ovl_workdir_create(struct ovl_fs *ofs, ...)
// :455
static bool ovl_workdir_ok(struct dentry *workdir, struct dentry *upperdir)
workdir 是 OverlayFS 的事务工作区:copy-up 的临时文件、目录 rename 的中间状态、"索引"(index,记录 lower 文件的身份映射,支持 NFS 导出与移动后重连)都经它落地。ovl_workdir_ok 的检查(workdir 与 upper 同文件系统、非上层嵌套)保证操作可原子——workdir 与 upper 跨设备会让"临时名→最终名"的 rename 无法原子完成,这是挂载参数最常报错的地方。
// fs/overlayfs/ovl_entry.h:58-80(挂载级状态, 节选)
struct ovl_fs {
unsigned int numlayer;
unsigned int numfs;
unsigned int numdatalayer; /* "data-only"层 (推送镜像新特性) */
struct ovl_layer *layers;
struct ovl_sb *fs;
struct dentry *workbasedir;
struct dentry *workdir;
long namelen;
...
};
numdatalayer 支持data-only lower 层(只含文件数据、无目录结构的层)——镜像分发新格式的内核对接点,配合 redirect xattr(ovl_entry.h:162-164)让大文件数据留在专用层。
小结
OverlayFS 的模型是"多层 vfsmount 的查找合并 + 单层 upper 承写 + workdir 事务区":层即挂载、fsid 去重、ovl_entry 保存逐层命中栈;合并语义按类型分流(文件覆盖、目录并集、删除白障、修改 copy-up);workdir 与 upper 必须同文件系统以保 rename 原子性。下一节进入实现:合并查找、copy-up 的完整流程与扩展属性协议。
38.2 OverlayFS 实现 —— upperdir/lowerdir/workdir
OverlayFS 的实现复杂度集中在三处:合并查找(ovl_lookup 在多层间裁决归属与重定向)、copy-up(写穿透的完整协议)、白障与扩展属性(删除/重命名的一致性编码)。本节逐一解剖。
38.2.1 合并查找:ovl_lookup
lookup("/usr/lib/x") 的多层裁决:
[1] 自上而下逐层 lookup:
upper: 有? → 命中, 记录 (upper, dentry_upper)
lower3/2/1: 同样记录, 目录则继续收全部命中
(38.1.2 节: 文件首中即止, 目录收集并集)
[2] 白障检查: 命中前若遇 whiteout → 该层以下失效
[3] redirect xattr (namei.c:28-31 解析结构):
目录被 rename 过的层会带 "trusted.overlay.redirect"
→ 后续层按重定向后的名字继续找 (合并目录的
移动一致性)
[4] 建 merged dentry/inode (ovl_inode, ovl_entry.h:159):
缓存三层路径 (ovl_entry), inode 的 i_op 全部换成
ovl_* 实现 — 用户进程看到的是"普通文件"
origin xattr: upper 文件记录"我来自 lower 的哪个
inode" — NFS 文件句柄/export (export.c 的
ovl_lookup_real 系列) 与"去重升级"的依据
查找的性能策略:merged dentry/inode 全量缓存(33.1.3 节 dcache 语义),稳态读写零重查;d_revalidate 钩子在 lower 层是 NFS 时做服务器端确认——OverlayFS 把"多层缓存一致性"问题转化回了"单层缓存一致性"的叠加。
38.2.2 copy-up:写穿透协议
// fs/overlayfs/copy_up.c:1280(入口)与 :1123(单层推进)
int ovl_copy_up(struct dentry *dentry)
static int ovl_copy_up_one(struct dentry *parent, struct dentry *dentry, ...)
// fs/overlayfs/dir.c:347
static int ovl_create_upper(struct dentry *dentry, struct inode *inode, ...)
copy-up 的步骤 (echo > /etc/hosts 触发):
[0] 沿父链向上: 父目录也全在 lower → 先逐层
copy-up 父目录 (需要能挂白障/建同名目录)
[1] 在 upper 建目标 (ovl_create_upper, dir.c:347):
workdir 建临时文件 → 复制数据+元数据+扩展属性
→ rename 到最终名 (workdir 同 fs 的原子性!,
38.1.2 节检查的原因)
[2] metacopy 变体 (特性开启):
只拷元数据 inode, 数据留在 lower —
upper 文件带 "trusted.overlay.metacopy" xattr
读数据时按 origin 回 lower 取
→ 大文件 chmod/chown 的成本从 GB 级降到 0
[3] 数据一致性: 复制期间原 lower 文件只读语义不变;
完成后 merged 指向 upper
失败恢复: workdir 临时名残留 → 下次挂载清理
(jbd2 式的"先写后改名"事务感, 但无日志兜底 —
依赖 rename 原子性)
38.2.3 白障与扩展属性协议
OverlayFS 的 xattr 语汇 (trusted.overlay.*):
whiteout: 字符设备 0:0 (旧) 或 xattr 白障 (新,
has_xwhiteouts, ovl_entry.h:46)
opaque: 目录不透明 — 中断下层同名目录的并集
(目录整体替换的语义)
redirect: 目录移动后的跨层指路 (38.2.1)
origin/index: 身份追溯 (数据回源/句柄导出)
metacopy: 元数据-only 上层文件的标记
rename 的两难: 把 /lower 的文件改名, 下层旧名必须
同时"消失" — 实现为 upper 新名 + upper 旧名白障,
目录则叠加 redirect; 跨层目录 rename 需 workdir
多步事务 + 失败回滚 (dir.c 的 ovl_rename_start, :1120)
权限模型:所有 xattr 用 trusted. 前缀——仅 root/CAP_SYS_ADMIN 可读写,用户态容器内不可伪造合并语义;userxattr 挂载选项(非特权容器)降级为 user.overlay.*,配合 33.2 节 FS_USERNS_MOUNT 让无 root 的容器也能挂 overlay。
小结
OverlayFS 的实现三面:合并查找以"逐层命中栈 + 白障剪枝 + redirect 指路"维护并集语义,merged inode 全量缓存复用 dcache;copy-up 以"workdir 临时件 + rename 落定"实现原子穿透,metacopy 变体把元数据修改的成本降到零拷贝;trusted.overlay.* 扩展属性族(whiteout/opaque/redirect/origin/metacopy)是全部删除/移动/溯源语义的编码载体。下一节看这套机制在容器镜像上的工程化。
38.3 OverlayFS 与容器镜像
容器镜像的分层分发(Docker/OCI image manifest 的层 tar 包)与 OverlayFS 的多层只读底本是天然的对应:每个镜像层解包成一个 lower 目录,容器启动即一次 overlay 挂载。本节从内核视角解释这套工程:层如何摆放、运行层如何隔离、页缓存在共享基础镜像时的红利,以及已知的坑。
38.3.1 镜像层到挂载参数的映射
docker run 的内核动作 (概化):
镜像 alpine (3 层) + 容器运行层:
lowerdir=/var/lib/docker/overlay2/l/C1/diff,\
/var/lib/docker/overlay2/l/C2/diff,\
/var/lib/docker/overlay2/l/C3/diff
upperdir=/var/lib/docker/overlay2/<id>/diff
workdir =/var/lib/docker/overlay2/<id>/work
层的获取:
拉取: 每层 tar 解包到独立目录 (按 manifest 顺序)
共享: 相同层的 digest 相同 → hardlink/overlay 挂载
去重: 同盘多层 fsid 相同 (38.1.1 节) → 优化生效
容器 rootfs: 挂载点即容器内 "/" (mount namespace,
9.3 节) — 容器内一切路径穿越这层 overlay
写时复制的量化:容器启动零拷贝(无 copy-up,全部只读命中);echo > /etc/hostname 触发一次 30KB 级 copy-up;yum install 的大量写全落 upper 层——运行层与镜像层的物理隔离让"删除容器=删目录"成为完整卸载。
38.3.2 共享与隔离的红利
页缓存共享 (36.1 节的红利):
同一基础镜像跑 100 个容器:
libc.so 的页缓存在 lower 层文件上 —
页缓存按 (inode, offset) 唯一 → 100 容器
**共享同一份 libc 页缓存页**
→ 内存占用 ≈ 1 份库代码 + N 份容器私有数据
对照: 每容器独立解包完整镜像 (无 overlay) →
libc 页缓存 ×100 (与 Shared_Clean 消失)
隔离面:
容器间 upper 层互不可见 (各自挂载参数)
lower 只读挂载在宿主侧管控
idmapped mount (33.2 节 FS_ALLOW_IDMAP) +
userns 挂载 (FS_USERNS_MOUNT, 38.2.3 userxattr)
→ 无特权守护进程也能安全挂 overlay
38.3.3 已知的坑与观测
运维必知的坑:
[1] copy-up 打破 fd 语义: 进程以 fd 持有 lower 文件,
另一进程写触发 copy-up → fd 仍指向 lower 旧 inode!
(fd 是 inode 的引用 — 33.1.4 节语义的边界案例)
容器内 "truncate 后老 fd 读到旧数据" 皆此因
[2] rename 目录 + 并发查找: redirect xattr 的窗口
(38.2.3) — 高频目录改名负载慎用
[3] 稀疏/特殊文件: 设备文件白障化、套接字 copy-up
的历史缺陷各版本不一
[4] upper 所在文件系统的 d_type 必须 supported
(ext4/xfs ok; 某些旧 xfs 配置拒挂)
观测:
mount | grep overlay 参数全览
/proc/self/mountinfo 层序与挂载点
du lower/upper 各层 空间归因
xattr 检查 trusted.overlay.* 语义标注现场
与 12 章 cgroup 的联动:容器内 overlay 的写落在 upper 所在文件系统,io/内存限速经 12.5 节 io 控制器按容器 cgroup 生效——回写(36.3 节)与节流在块层汇合(35.2.3 节 iocost)。第六部分的终局视角:一个容器进程的每次文件访问,穿过 VFS 对象(33 章)→ overlay 合并(本卷)→ lower 的页缓存(36 章)→ 底层 ext4(34 章)→ JBD2 事务与块层(35 章)——全书第六部分正好是这条完整栈的逐层解剖。
小结
容器镜像把 OverlayFS 从内核特性推成基础设施:镜像层=lower(digest 去重、只读共享)、容器层=upper(零拷贝启动、删容器即卸载)、页缓存按 inode 共享让百容器共享基础库;工程边界在"fd 指向 lower 的旧 inode"(copy-up 语义陷阱)、目录 rename 窗口与 upper 文件系统要求。OverlayFS 与 mount namespace、cgroup 限速的组合正是"容器 = 内核原语组合"的缩影——第六部分至此完成。