Linux 挂载传播 (Mount Propagation) 是 Linux 内核中的一个高级特性,主要用于控制挂载点(Mount Point)在不同的挂载命名空间(Mount Namespace)之间,或者在同一个命名空间内的不同绑定挂载(Bind Mount)之间,如何共享挂载事件。
介绍
Linux 挂载传播解决的核心问题是:如果我在目录 A 下挂载了一个设备,那么作为目录 A 的克隆体目录 B,是否也能自动看到这个挂载?
这在容器技术(如 Docker、Kubernetes)中极为重要。
核心概念:挂载事件与对等组
在理解传播类型之前,需要理解两个概念:
- 挂载事件 (Mount Event):指执行
mount 或 umount 操作。注意,这里指的不是创建文件,而是挂载新的文件系统。
- 对等组 (Peer Group):当你创建一个 Bind Mount(绑定挂载)时,源目录和目标目录可以被视为属于同一个
对等组。传播属性决定了事件如何在组内或组间传递。
四种传播类型 (Propagation Types)
Linux 定义了四种主要的挂载传播模式。假设我们有两个目录:Source(源) 和 Target(目标,即 Bind Mount 的副本)。
Shared (共享挂载, MS_SHARED)
- 特性:双向传播。
- 行为:
- 如果在 Source 下挂载了新设备,Target 下也会自动出现该挂载。
- 如果在 Target 下挂载了新设备,Source 下也会自动出现该挂载。
- 应用场景:当你希望两个路径完全同步挂载状态时。
Slave (从属挂载, MS_SLAVE)
- 特性:单向传播(从 Master 到 Slave)。
- 行为:
- Source (Master) 的挂载事件会传播给 Target (Slave)。
- Target (Slave) 的挂载事件不会传播给 Source。
- 应用场景:这是容器中最常用的模式。宿主机(Host)挂载了 USB 驱动器,容器内能看到;但容器内挂载临时文件系统,不应该影响宿主机。
Private (私有挂载, MS_PRIVATE)
- 特性:无传播(完全隔离)。这是大多数挂载的默认状态。
- 行为:
- Source 和 Target 彻底断开联系。任何一方的挂载操作都不会影响另一方。
- 应用场景:需要完全隔离的环境,确保容器内的挂载操作完全不泄露,也不受外部影响。
Unbindable (不可绑定, MS_UNBINDABLE)
- 特性:私有且禁止再次绑定。
- 行为:
- 类似 Private,不传播事件。
- 特殊限制:这个挂载点不能作为 Bind Mount 的源。如果你试图执行
mount --bind /mnt/unbindable /tmp/target,会报错。
- 应用场景:防止递归爆炸(例如挂载根目录到子目录时防止无限循环),或者严格禁止某目录被克隆。
图解传播方向
假设 A 是源,B 是 A 的 Bind Mount:
| 模式 |
A 发生挂载 $\to$ B 能看到? |
B 发生挂载 $\to$ A 能看到? |
关系描述 |
| Shared |
✅ 是 |
✅ 是 |
镜面对称 |
| Slave |
✅ 是 |
❌ 否 |
主从关系 |
| Private |
❌ 否 |
❌ 否 |
各自独立 |
| Unbindable |
❌ 否 |
❌ 否 |
独立且不可复制 |
绑定挂载 (mount --bind)
mount --bind 允许将文件系统层级结构中的一部分(通常是一个目录)挂载到另一个位置。
这就好比给一个文件夹开了两扇门,你可以从任何一扇门进去,看到的、操作的都是同一个房间里的东西。
基本语法
mount --bind <源目录> <挂载点目录>
或者简写为:
- 源目录:已经存在的、你想要共享的原始目录。
- 挂载点目录:你想要挂载到的目标位置(该目录必须存在,通常为空)。
它是如何工作的?
当你执行 mount --bind /A /B 后:
- 访问
/B 就等同于访问 /A。
- 如果你在
/B 中创建、修改或删除文件,/A 中的内容会实时发生完全相同的变化(因为它们本质上操作的是磁盘上的同一个 Inode)。
持久化配置 (/etc/fstab)
如果希望重启后 bind 挂载依然生效,需要将其写入 /etc/fstab 文件。
格式:
示例:
/home/data /var/www/html/data none bind 0 0
如果是只读挂载,因为 fstab 不支持直接分两步写,通常可以直接写选项(部分现代发行版支持):
/source /dest none bind,ro 0 0
如何卸载
与卸载普通设备一样,使用 umount:
注意:卸载挂载点不会删除源目录的数据,只会断开连接。
与软链接 (Symbolic Link) 的区别
虽然 mount --bind 看起来很像软链接(ln -s),但它们有本质区别:
| 特性 |
软链接 (ln -s) |
绑定挂载 (mount --bind) |
| 层级 |
文件系统之上的快捷方式 |
文件系统层面的映射 (VFS) |
| Chroot 环境 |
失效。如果在 chroot 环境外创建链接指向外部,chroot 内部无法访问。 |
有效。可以穿透 chroot 限制,将宿主机目录映射进容器/沙盒。 |
| 应用感知 |
应用程序可以检测到这是个链接。 |
对应用程序透明,看起来就像普通的物理目录。 |
| 跨文件系统 |
可以跨越不同的分区/文件系统。 |
也可以跨越,但仅限于挂载点层面。 |
常见使用场景
假设你的 Web 服务器(如 Nginx/Apache)默认根目录是 /var/www/html,但你的开发代码在 /home/user/project。
你可以不用复制文件,而是直接挂载过去:
mount --bind /home/user/project /var/www/html/project
这样,Web 服务器就能直接读取并服务你的用户目录下的文件,且无需担心权限导致的路径遍历问题(相比软链接更安全)。
这是 mount --bind 最核心的用途。当你创建一个隔离环境(如 chroot)时,环境内部需要访问系统的 /proc、/dev 或 /sys 目录才能正常运行软件。
# 假设 /srv/my-chroot 是你的隔离环境根目录
mount --bind /proc /srv/my-chroot/proc
mount --bind /dev /srv/my-chroot/dev
这样,chroot 内部的程序就能读取系统进程信息和设备文件。
- 场景三:只读挂载 (Read-Only Bind)
你可以将一个目录以只读的方式挂载到另一个位置,用于保护数据不被修改。
这需要两步操作(Linux 内核限制):
- 先进行普通的 bind 挂载:
mount --bind /source /dest
- 再重新挂载为只读:
mount -o remount,ro,bind /dest
现在,用户可以通过 /source 写入数据,但通过 /dest 只能读取数据。
如果你挂载一个空目录到一个非空目录上,原目录的内容会被遮盖住(不会丢失,只是暂时看不见),直到你卸载它。这在调试某些系统行为时很有用。
递归绑定 (mount --rbind)
标准的 --bind 只会挂载该目录本身的文件系统。如果源目录下还有其他挂载点(Sub-mounts),它们不会被自动带过去。
例子:
/mnt/disk1 是一个挂载的硬盘。
- 你执行
mount --bind /mnt /tmp/test。
- 你会发现
/tmp/test/disk1 是空的。
解决方法: 使用 --rbind (Recursive Bind)。
mount --rbind /mnt /tmp/test
这将递归地把源目录下的所有子挂载点也一同挂载过去。
实战演示
你需要 root 权限来测试。我们建立两个目录来模拟。
# 1. 准备目录
mkdir -p /test/source
mkdir -p /test/target
# 2. 将 source 挂载为一个挂载点(bind 自身是为了让它成为一个挂载点)
mount --bind /test/source /test/source
# 3. 将 source 设置为 Shared (共享模式)
mount --make-shared /test/source
# 4. 创建 target 作为 source 的 bind mount
mount --bind /test/source /test/target
# 此时,source 和 target 都在同一个 Shared Peer Group 中。
测试 Shared:
# 在 source 下挂载一个临时文件系统
mkdir /test/source/sub
mount -t tmpfs tmpfs /test/source/sub
# 查看 target,你会发现 target 下也自动挂载了!
ls /test/target/sub # 正常访问
测试 Slave:
# 先清理
umount /test/source/sub
umount /test/target
# 将 target 重新挂载,并设置为 source 的 Slave
mount --bind /test/source /test/target
mount --make-slave /test/target # 关键步骤
# 测试:Source 变动 -> Target
mount -t tmpfs tmpfs /test/source/sub
ls /test/target/sub # ✅ Target 能看到 Source 的挂载
# 测试:Target 变动 -> Source
mkdir /test/target/sub2
mount -t tmpfs tmpfs /test/target/sub2
ls /test/source/sub2 # ❌ Source 看不到 Target 的挂载 (如果是空的目录则说明没挂载过来)
在 Docker 中的应用
在 Docker 中使用 -v 或 --mount 挂载卷时,挂载传播非常关键,特别是当你在容器内运行 Docker (Docker-in-Docker) 或者需要容器感知宿主机磁盘变化时。
Docker 默认通常是 rprivate(递归私有),参考。
语法示例:
# 使用 --mount 语法指定传播模式 (bind-propagation)
docker run -d \
--name my-container \
--mount type=bind,source=/mnt/data,target=/data,bind-propagation=rshared \
my-image
或者使用 -v 的简写:
docker run -v /mnt/data:/data:rshared ...
rshared (Recursive Shared): 容器内挂载目录,宿主机可见;宿主机挂载目录,容器可见。
rslave (Recursive Slave): 宿主机挂载 USB,容器可见;容器内挂载 tmpfs,宿主机不可见。(最常用且安全的方式)。
docker -v 的底层实现公式:
$$ \text{Docker Volume} = \text{Mount Namespace} + \text{Mount (–bind)} $$
- Mount Namespace:让容器拥有独立的挂载视图,不影响宿主机和其他容器。
- Bind Mount:将宿主机的文件系统树的一根枝条(目录),直接
嫁接到容器文件系统树的某个节点上。
- 遮盖效应:挂载点会覆盖掉镜像(OverlayFS)中原有的目录内容。
总结
- Shared: 大家互通,适合完全同步。
- Slave: 上级管下级,下级不乱动上级,适合容器读取宿主机动态挂载。
- Private: 井水不犯河水,默认且最安全。
- Unbindable: 孤僻,谁也别想粘着我。
注意:命令中的 r (如 rshared, rslave) 代表 Recursive (递归),意味着该属性不仅应用于当前挂载点,还应用于该挂载点下的所有子挂载点。