Linux 挂载传播介绍

发布时间: 更新时间: 总字数:3040 阅读时间:7m 作者: IP上海 分享 网址

Linux 挂载传播 (Mount Propagation) 是 Linux 内核中的一个高级特性,主要用于控制挂载点(Mount Point)在不同的挂载命名空间(Mount Namespace)之间,或者在同一个命名空间内的不同绑定挂载(Bind Mount)之间,如何共享挂载事件

介绍

Linux 挂载传播解决的核心问题是:如果我在目录 A 下挂载了一个设备,那么作为目录 A 的克隆体目录 B,是否也能自动看到这个挂载?

这在容器技术(如 Docker、Kubernetes)中极为重要。

核心概念:挂载事件与对等组

在理解传播类型之前,需要理解两个概念:

  1. 挂载事件 (Mount Event):指执行 mountumount 操作。注意,这里指的不是创建文件,而是挂载新的文件系统。
  2. 对等组 (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 -B <源目录> <挂载点目录>
  • 源目录:已经存在的、你想要共享的原始目录。
  • 挂载点目录:你想要挂载到的目标位置(该目录必须存在,通常为空)。

它是如何工作的?

当你执行 mount --bind /A /B 后:

  • 访问 /B 就等同于访问 /A
  • 如果你在 /B 中创建、修改或删除文件,/A 中的内容会实时发生完全相同的变化(因为它们本质上操作的是磁盘上的同一个 Inode)。

持久化配置 (/etc/fstab)

如果希望重启后 bind 挂载依然生效,需要将其写入 /etc/fstab 文件。

格式:

/源目录    /挂载点    none    bind    0    0

示例:

/home/data    /var/www/html/data    none    bind    0    0

如果是只读挂载,因为 fstab 不支持直接分两步写,通常可以直接写选项(部分现代发行版支持):

/source    /dest    none    bind,ro    0    0

如何卸载

与卸载普通设备一样,使用 umount

umount /挂载点目录

注意:卸载挂载点不会删除源目录的数据,只会断开连接。

虽然 mount --bind 看起来很像软链接(ln -s),但它们有本质区别:

特性 软链接 (ln -s) 绑定挂载 (mount --bind)
层级 文件系统之上的快捷方式 文件系统层面的映射 (VFS)
Chroot 环境 失效。如果在 chroot 环境外创建链接指向外部,chroot 内部无法访问。 有效。可以穿透 chroot 限制,将宿主机目录映射进容器/沙盒。
应用感知 应用程序可以检测到这是个链接。 对应用程序透明,看起来就像普通的物理目录。
跨文件系统 可以跨越不同的分区/文件系统。 也可以跨越,但仅限于挂载点层面。

常见使用场景

  • 场景一:Web 服务器目录映射

假设你的 Web 服务器(如 Nginx/Apache)默认根目录是 /var/www/html,但你的开发代码在 /home/user/project。 你可以不用复制文件,而是直接挂载过去:

mount --bind /home/user/project /var/www/html/project

这样,Web 服务器就能直接读取并服务你的用户目录下的文件,且无需担心权限导致的路径遍历问题(相比软链接更安全)。

  • 场景二:构建 Chroot 或 容器环境

这是 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 内核限制):

  1. 先进行普通的 bind 挂载:
mount --bind /source /dest
  1. 再重新挂载为只读:
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)} $$

  1. Mount Namespace:让容器拥有独立的挂载视图,不影响宿主机和其他容器。
  2. Bind Mount:将宿主机的文件系统树的一根枝条(目录),直接嫁接到容器文件系统树的某个节点上。
  3. 遮盖效应:挂载点会覆盖掉镜像(OverlayFS)中原有的目录内容。

总结

  • Shared: 大家互通,适合完全同步。
  • Slave: 上级管下级,下级不乱动上级,适合容器读取宿主机动态挂载。
  • Private: 井水不犯河水,默认且最安全。
  • Unbindable: 孤僻,谁也别想粘着我。

注意:命令中的 r (如 rshared, rslave) 代表 Recursive (递归),意味着该属性不仅应用于当前挂载点,还应用于该挂载点下的所有子挂载点。

参考

  1. https://lwn.net/Articles/690679/
  2. https://docs.docker.com/engine/containers/run/#bind-mounts
  3. https://kubernetes-csi.github.io/docs/deploying.html#enabling-mount-propagation
本文总阅读量 次 本站总访问量 次 本站总访客数
Home Archives Categories Tags Statistics