挂载传播性机制
1. 概述
挂载传播性(Mount Propagation)是 Linux 内核在 2.6.15 版本引入的一项重要特性,DragonOS 对此进行了完整实现。该机制控制在一个挂载点上发生的挂载/卸载事件是否以及如何传播到其他相关的挂载点。
1.1 为什么需要挂载传播性?
在容器化和命名空间隔离的场景下,不同进程可能拥有不同的挂载命名空间(Mount Namespace)。传统的挂载行为无法满足以下需求:
共享存储:多个容器需要看到相同的存储变化
隔离性:某些容器的挂载变化不应影响其他容器
灵活配置:不同目录树需要不同的传播策略
1.2 核心概念
挂载传播性引入了以下核心概念:
概念 |
说明 |
|---|---|
Peer Group |
一组共享挂载事件的挂载点集合 |
传播类型 |
定义挂载点如何参与事件传播 |
Bind Mount |
将一个目录树绑定到另一个位置 |
命名空间 |
挂载点的隔离边界 |
2. 传播类型
DragonOS 支持四种传播类型,每种类型定义了不同的事件传播行为:
2.2 Private(私有)
┌─────────────┐ ┌─────────────┐
│ Mount A │ │ Mount B │
│ private │ ✗ │ private │
│ │◄───────►│ │
└─────────────┘ └─────────────┘
不传播任何事件
特性:
挂载事件既不发送也不接收
每个挂载点完全独立
这是新建挂载的默认类型
典型用例:
需要完全隔离的容器
临时挂载点
2.3 Slave(从属)
┌─────────────┐ ┌─────────────┐
│ Master │ ───────►│ Slave │
│ shared │ │ slave │
│ │◄─ ✗ ────│ │
└─────────────┘ └─────────────┘
单向传播:Master → Slave
特性:
只接收来自 Master 的事件,不向外传播
可以有自己的本地挂载变化,但不影响 Master
Master 必须是 Shared 类型
典型用例:
只读共享视图
容器需要看到主机的挂载变化,但不能影响主机
2.4 Unbindable(不可绑定)
┌─────────────┐ ┌─────────────┐
│ Mount A │ ✗ │ Mount B │
│ unbindable │◄───────►│ any │
│ │ 禁止bind │ │
└─────────────┘ └─────────────┘
特性:
不能被 bind mount
不参与任何传播
最强的隔离级别
典型用例:
敏感数据目录
防止意外暴露的系统目录
3. Peer Group 机制
3.1 什么是 Peer Group?
Peer Group 是共享挂载传播关系的挂载点集合。同一 Peer Group 内的所有 Shared 挂载点会双向传播挂载事件。
┌──────────────────────────────────┐
│ Peer Group (ID=42) │
│ │
Namespace A │ ┌─────────┐ │
┌─────────────────┼───│ /mnt/a │ │
│ │ │ shared │ │
│ │ └────┬────┘ │
│ │ │ │
└─────────────────┼────────┼─────────────────────────┤
│ │ │
Namespace B │ │ │
┌─────────────────┼────────┼─────────────────────────┤
│ │ │ │
│ │ ┌────▼────┐ │
│ │ │ /mnt/b │ │
│ │ │ shared │ │
│ │ └─────────┘ │
└─────────────────┼──────────────────────────────────┤
└──────────────────────────────────┘
3.2 Peer Group 的形成
Peer Group 在以下情况形成或扩展:
设置 Shared 类型:当挂载点首次被设为 Shared,分配新的 Group ID
Bind Mount:对 Shared 挂载执行 bind mount,新挂载加入同一 Peer Group
命名空间复制:
unshare(CLONE_NEWNS)时,Shared 挂载被复制并加入同一 Peer Group
3.3 Group ID 分配
每个 Peer Group 由唯一的 Group ID 标识:
Group ID 分配器
┌─────────────────────────────────────┐
│ ID Pool: [1, 2, 3, 4, 5, ...] │
│ │
│ 已分配: {1 → Group A, 3 → Group B} │
│ 可用: {2, 4, 5, ...} │
└─────────────────────────────────────┘
Group ID 从 1 开始分配
0 表示无效/未加入任何组
当 Peer Group 为空时,ID 可回收
4. 事件传播流程
4.1 Mount 事件传播
当在 Shared 挂载点上创建新挂载时:
步骤 1: 在源挂载点创建子挂载
┌──────────────┐
│ /mnt/a │ ← mount("", "/mnt/a/sub", "ramfs", ...)
│ shared │
│ │ │
│ ┌──▼───┐ │
│ │ sub │ │
│ └──────┘ │
└──────────────┘
步骤 2: 获取 Peer Group 成员
┌──────────────────────────────────────┐
│ Peer Group 42: │
│ - /mnt/a (源) │
│ - /mnt/b (Peer) │
│ - /mnt/c (Peer) │
└──────────────────────────────────────┘
步骤 3: 向每个 Peer 传播
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ /mnt/a │ │ /mnt/b │ │ /mnt/c │
│ shared │ │ shared │ │ shared │
│ │ │ │ │ │ │ │ │
│ ┌──▼───┐ │ │ ┌──▼───┐ │ │ ┌──▼───┐ │
│ │ sub │ │ │ │ sub' │ │ │ │ sub''│ │
│ └──────┘ │ │ └──────┘ │ │ └──────┘ │
└──────────────┘ └──────────────┘ └──────────────┘
源 复制 复制
4.2 Umount 事件传播
当在 Shared 挂载点上卸载子挂载时:
步骤 1: umount("/mnt/a/sub")
┌──────────────┐
│ /mnt/a │
│ shared │
│ │ │
│ ┌──▼───┐ │ ← umount
│ │ sub │ │
│ └──────┘ │
└──────────────┘
步骤 2: 传播到所有 Peer
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ /mnt/a │ │ /mnt/b │ │ /mnt/c │
│ shared │ │ shared │ │ shared │
│ │ │ │ │ │
│ (empty) │ │ (empty) │ │ (empty) │
│ │ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
已卸载 传播卸载 传播卸载
4.3 传播到 Slave
Slave 挂载点单向接收事件:
┌──────────────┐ ┌──────────────┐
│ Master │ │ Slave │
│ shared │ │ slave │
│ │ │ ───► │ │ │
│ ┌──▼───┐ │ │ ┌──▼───┐ │
│ │ sub │ │ │ │ sub' │ │
│ └──────┘ │ │ └──────┘ │
└──────────────┘ └──────────────┘
│
▼
Slave 上的本地挂载
不会传播回 Master
5. 命名空间交互
5.1 命名空间复制
当调用 unshare(CLONE_NEWNS) 创建新的挂载命名空间时:
复制前(父进程的命名空间):
┌─────────────────────────────────────┐
│ Mount Namespace (Parent) │
│ │
│ / ┌──────────┐ │
│ └── mnt/ │ shared │ │
│ └── data │ Group 1 │ │
│ └──────────┘ │
└─────────────────────────────────────┘
unshare(CLONE_NEWNS) 后:
┌─────────────────────────────────────┐
│ Mount Namespace (Parent) │
│ │
│ / ┌──────────┐ │
│ └── mnt/ │ shared │◄───────┼─┐
│ └── data │ Group 1 │ │ │
│ └──────────┘ │ │ Peer
└─────────────────────────────────────┘ │ 关系
│
┌─────────────────────────────────────┐ │
│ Mount Namespace (Child) │ │
│ │ │
│ / ┌──────────┐ │ │
│ └── mnt/ │ shared │◄───────┼─┘
│ └── data │ Group 1 │ │
│ └──────────┘ │
└─────────────────────────────────────┘
关键行为:
Private 挂载:简单复制,无 Peer 关系
Shared 挂载:复制后加入同一 Peer Group,建立跨命名空间传播
Slave 挂载:保持 Slave 关系
Unbindable 挂载:不可复制到新命名空间
5.2 跨命名空间传播示例
时间线:
───────────────────────────────────────────────────►
T1: 父进程创建 shared 挂载
Parent NS: /mnt/shared (Group 1)
T2: 子进程 unshare(CLONE_NEWNS)
Parent NS: /mnt/shared (Group 1) ◄──┐
│ Peer
Child NS: /mnt/shared (Group 1) ◄──┘
T3: 父进程在 /mnt/shared/sub 挂载
Parent NS: /mnt/shared/sub ←── 新挂载
│
▼ 传播
Child NS: /mnt/shared/sub ←── 自动出现
T4: 子进程也能看到 /mnt/shared/sub
6. 传播类型转换
6.1 状态转换图
┌───────────────┐
MS_SHARED │ │ MS_PRIVATE
┌───────────►│ SHARED │◄───────────┐
│ │ │ │
│ └───────┬───────┘ │
│ │ │
│ │ MS_SLAVE │
│ ▼ │
┌──────┴──────┐ ┌─────────────┐ ┌─────┴───────┐
│ │ │ │ │ │
│ PRIVATE │◄─────│ SLAVE │─────►│ UNBINDABLE │
│ │ │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
MS_PRIVATE MS_UNBINDABLE
6.2 转换规则
源类型 |
目标类型 |
操作 |
副作用 |
|---|---|---|---|
Private |
Shared |
|
分配新 Group ID |
Shared |
Private |
|
从 Peer Group 移除 |
Shared |
Slave |
|
变为 Peer Group 的接收者 |
Slave |
Shared |
|
断开与 Master 的连接 |
* |
Unbindable |
|
清除所有关系 |
7. 设计原则
7.1 最小惊讶原则
新挂载默认为 Private,不产生意外的副作用
只有显式设置 Shared 才参与传播
传播行为明确且可预测
7.2 性能考虑
Peer Group 使用全局注册表管理,O(1) 查找
传播操作使用延迟执行,避免阻塞挂载操作
弱引用(Weak)避免循环引用和内存泄漏
7.3 一致性保证
使用原子操作和锁保护传播状态
传播失败不影响原始操作
支持部分传播后的状态恢复
8. 与 Linux 的兼容性
DragonOS 的挂载传播性实现遵循 Linux 语义:
特性 |
Linux |
DragonOS |
|---|---|---|
Shared 传播 |
✓ |
✓ |
Private 隔离 |
✓ |
✓ |
Slave 单向传播 |
✓ |
✓ |
Unbindable |
✓ |
✓ |
跨命名空间传播 |
✓ |
✓ |
递归传播 (MS_REC) |
✓ |
✓ |
/proc/self/mountinfo |
✓ |
部分 |