
大家好,我是情报小哥~
今天为大伙儿聊一个 Linux 里非常实用的技术点:OverlayFS 联合文件系统。
说实话,你大概率已经用过它了,只是你自己不知道而已。
但凡你跑过 Docker 容器、玩过嵌入式 Linux 固件,或者用过 Live CD,背后都站着 OverlayFS 这个"幕后玩家"。
比如说你有一份重要的工程代码,放在 /opt/project 目录下。
现在做个实验:改几行代码测试一下,但绝对不能动原始文件。
这简单,cp -r 复制一份呗~
但如果这个目录有 2GB 呢?复制一份就要占用额外的 2GB 磁盘。很蠢对吧?
有没有一种办法:看起来你拥有了一份完整的、可以随意修改的副本,但实际上不占用额外空间?
有,这就是 OverlayFS 的核心思想。

OverlayFS 是一种 Linux 联合文件系统(Union Filesystem),它把多个目录"叠加"在一起,对外呈现为一个统一的目录。
底层不变,上层记录修改。这就是 OverlayFS。
Linux 内核从 3.18 版本开始正式合入了 OverlayFS,到现在十多年了,稳定性没得说。
OverlayFS 的文件系统结构,就三个角色:
| lowerdir | |||
| upperdir | |||
| merged | |||
| workdir |
看这张图:

merged 目录是你实际 cd、ls、vim 操作的地方。
至于你修改的内容最终落到 upperdir 还是 lowerdir,内核帮你搞定了,你完全不用管。
OverlayFS 最核心的设计,就是这个 Copy-on-Write(写时复制) 机制。也就是你不改,我不拷;你要改,我先拷再改。比如有四种操作:
读一个文件时,内核先看 upperdir 里有没有。有,就读 upperdir 的;没有,就读 lowerdir 的。
逻辑很简单:上层优先。
这个最简单,直接在 upperdir 里创建新文件就行。lowerdir 完全不受影响。
这是 Copy-on-Write 的精髓所在。
假设 lowerdir 里有个文件 hello.txt,内容是 "Hello World"。
你现在 echo "Modified" > merged/hello.txt。
内核的处理流程是:
发现 lowerdir 的 hello.txt要被修改先把整个文件复制到 upperdir(这叫 "copy-up") 然后在 upperdir 的副本上执行修改 lowerdir 里的原始 hello.txt纹丝不动
修改的代价是文件的一次完整拷贝。
面试经常问:OverlayFS 修改大文件的开销有多大?
其实第一次修改时,整个文件要从 lowerdir 拷到 upperdir。文件越大,第一次写的延迟越高。
lowerdir 里的文件是只读的,你没法真的"删掉"它。
那 OverlayFS 怎么让你在 merged 目录里看不到被删的文件呢?
内核在 upperdir 里创建一个特殊的字符设备文件(主设备号 0,次设备号 0),名字跟原文件一样,这就是 whiteout。
当你在 merged 目录里 ls 时,内核看到 upperdir 里有这个 whiteout 文件,就知道"哦,这个文件被删了",于是在 merged 中屏蔽掉 lowerdir 对应的文件。比如:
# 在 merged 中删除一个 lowerdir 的文件rm merged/test.txt# 然后去 upperdir 看看,会发现一个同名的特殊文件ls -la upperdir/# c--------- 1 root root 0, 0 Jul 2 10:00 test.txt文件类型是 c(字符设备),这就是 whiteout。
# 创建实验目录mkdir -p /tmp/overlay-test/{lower,upper,work,merged}# 在 lowerdir 中放点东西echo"I am in lowerdir" > /tmp/overlay-test/lower/original.txtecho"I am also in lowerdir" > /tmp/overlay-test/lower/shared.txtmkdir /tmp/overlay-test/lower/subdirecho"deep file" > /tmp/overlay-test/lower/subdir/deep.txt现在的目录结构:
/tmp/overlay-test/├── lower/│ ├── original.txt│ ├── shared.txt│ └── subdir/│ └── deep.txt├── upper/ (空)├── work/ (空)└── merged/ (空,即将作为挂载点)mount -t overlay overlay \ -o lowerdir=/tmp/overlay-test/lower,upperdir=/tmp/overlay-test/upper,workdir=/tmp/overlay-test/work \ /tmp/overlay-test/merged参数解释:
-t overlay:指定文件系统类型overlay:设备名(OverlayFS 不需要真实设备,随便写个名字)-o:挂载选项,lowerdir/upperdir/workdir 三个参数搞定OverlayFS 支持多个下层目录,用冒号 : 分隔:
mount -t overlay overlay \ -o lowerdir=/layerA:/layerB:/layerC,upperdir=/upper,workdir=/work \ /merged注意:最左边优先级最高!
查找顺序是:upperdir → /layerA → /layerB → /layerC
这个特性有什么用?举个例子:
# 项目基础环境(layerC)# ├── 操作系统基础库# 项目依赖(layerB)# ├── 第三方库# 项目代码(layerA)# ├── 你写的代码mount -t overlay overlay \ -o lowerdir=/project-code:/dependencies:/base-os,upperdir=/upper,workdir=/work \ /merged多层叠加,各管各的,互不干扰。Docker 镜像的分层存储,玩的就是这个。
这是 OverlayFS 最有用的,没有之一。
Docker 镜像由多个只读层(layer)堆叠而成,每个 Dockerfile 指令生成一层:
容器层 (upperdir, 可读写) ↑ 你在容器里写的所有东西═══════════════════════════════镜像层 3: RUN apt install nginx ┐镜像层 2: COPY app /app ├ 全部只读 (lowerdir)镜像层 1: FROM ubuntu:22.04 ┘当你 docker run 一个容器时:
有两个好处:
省磁盘:同一台机器上跑 10 个 nginx 容器,它们共享同一套基础镜像层,只有各自的 thin layer 是独立的。
# 查看 Docker 使用的存储驱动docker info | grep "Storage Driver"# Storage Driver: overlay2启动快:不需要真的"复制"一个 rootfs,只是 mount 一下。这也是容器为什么可以秒级启动的核心原因之一。
现在 Docker 默认用的是 overlay2,它是 OverlayFS 的改进版本。
相比初代 overlay,overlay2 主要解决了 inode 耗尽 的问题——初代 overlay 中每个文件在 upperdir 和 lowerdir 各消耗一个 inode,overlay2 通过多层 lowerdir 的方式把这个问题解决了。
做嵌入式 Linux 的小伙伴,这个场景应该很亲切。
很多嵌入式设备的 rootfs 用 SquashFS(一种压缩的只读文件系统),然后叠加一层 tmpfs 或者 JFFS2 作为可写层:
# 嵌入式设备启动脚本示例mount -t squashfs /dev/mtdblock0 /rom # 只读 rootfsmount -t tmpfs tmpfs /overlay_upper # 内存文件系统,可写mkdir -p /overlay_workmount -t overlay overlay \ -o lowerdir=/rom,upperdir=/overlay_upper,workdir=/overlay_work \ /这就是所谓的 "不死系统",怎么折腾都搞不坏。路由器、工业控制设备特别喜欢这么干。
如果你用 Buildroot 或者 Yocto 构建嵌入式 Linux,这个机制基本上都会用到。
星标公众号,第一时间看文章!
小哥搜集了一些嵌入式学习资料,公众号内回复【1024】即可找到下载链接!
推荐好文点击蓝色字体即可跳转
☞专辑|Linux应用程序编程大全 ☞ 专辑|学点网络知识 ☞ 专辑|手撕C语言 ☞ 专辑|手撕C++语言 ☞ 专辑|经验分享 ☞ 专辑|从单片机到Linux ☞ 专辑|电能控制技术 ☞ 专辑|嵌入式必备数学知识 ☞ MCU进阶专辑
☞ 经验分享