[文件系统]文件系统挂载与卸载详解

文件系统挂载与卸载详解

目录


第 1 章 挂载机制概述

文件系统挂载是 VFS 层的核心功能,负责将各种文件系统连接到统一的目录树中。

lua 复制代码
+------------------+
|   用户空间进程    |
+--------+---------+
         | mount() 系统调用
         v
+------------------+     +------------------+
|  struct vfsmount |---->| struct super_block|
|  (挂载实例)       |     |  (超级块)         |
+------------------+     +------------------+
         |                        |
         v                        v
+------------------+     +------------------+
| struct mnt_namespace |  | struct file_system_type |
| (挂载命名空间)    |     |  (文件系统类型)    |
+------------------+     +------------------+

挂载的本质:将一个文件系统实例(super_block)通过一个挂载实例(vfsmount)挂到全局目录树(dentry 树)的某个位置(挂载点),从而形成一个统一的命名空间。此后对挂载点路径的访问会自动穿越到新文件系统。

四个对象的分工(对应上图各方块):

  • struct super_block: "文件系统"本身------你在用一个 ext4 还是 tmpfs,它身上有块大小、魔数、根目录、inode 表等元信息。一个被挂载的物理文件系统对应一个。
  • struct vfsmount: "挂的动作" ------把 super_block 挂到全局树哪个节点的一次具体挂载记录。同一个 super_block 可以有多个 vfsmount(同一块盘挂到不同目录、或绑定挂载)。
  • struct file_system_type: "一类文件系统" ------ext4 的 get_sb/kill_sb 回调、名字都登记在这,是"模板"而非"实例"。
  • struct mnt_namespace: "一组进程看到的挂载视图" ------所有挂载实例串成的一棵树,容器是各自独立的一棵。

一句话串起来 :想要把"某个类型(file_system_type)的文件系统实例(super_block)挂进"一个名字空间(mnt_namespace)里,就需要一次挂载(vfsmount)。

举例 :mount -t ext4 /dev/sda1 /mnt 之后,"ext4 这个类型"通过 ext4_get_sb 造出"ext4 这个超级块",再用一个 vfsmount 把它挂到"当前命名空间的 /mnt"。同名命令再对 /mnt2 执行一次,则是同一个 super_block、第二个 vfsmount------两处看到盘上同一份数据。


第 2 章 挂载核心数据结构

2.1 struct vfsmount ------ 挂载实例

定义位置:include/linux/mount.h

c 复制代码
struct vfsmount {
    struct list_head      mnt_hash;        // 哈希表链接
    struct vfsmount       *mnt_parent;     // 父挂载点
    struct dentry         *mnt_mountpoint; // 挂载点dentry
    struct dentry         *mnt_root;       // 此文件系统的根dentry
    struct super_block    *mnt_sb;         // 关联的超级块
    struct list_head      mnt_mounts;      // 子挂载列表
    struct list_head      mnt_child;       // 兄弟挂载链表
    int                   mnt_flags;       // 挂载标志
    const char            *mnt_devname;    // 设备名
    struct list_head      mnt_list;        // 命名空间链表
    struct list_head      mnt_expire;      // 过期链表
    struct list_head      mnt_share;       // 共享挂载链表
    struct list_head      mnt_slave_list;  // 从属挂载列表
    struct list_head      mnt_slave;       // 从属链表项
    struct vfsmount       *mnt_master;     // 主挂载
    struct mnt_namespace  *mnt_ns;         // 所属命名空间
    int                   mnt_id;          // 挂载ID
    int                   mnt_group_id;    // 对等组ID
    atomic_t              mnt_count;       // 引用计数
    int                   mnt_expiry_mark; // 过期标记
    int                   mnt_pinned;      // 固定计数
    int                   mnt_ghosts;      // 幽灵计数
    int                   *mnt_writers;    // 写者计数(每CPU)
};

内存布局(64 位系统,总大小约 200 字节):

diff 复制代码
struct vfsmount 内存布局 (64位系统):
+==========================================+
| mnt_hash (16 bytes) - 哈希链表节点        |
+------------------------------------------+
| mnt_parent (8 bytes) - 父挂载指针        |
+------------------------------------------+
| mnt_mountpoint (8 bytes) - 挂载点dentry  |
+------------------------------------------+
| mnt_root (8 bytes) - 根dentry            |
+------------------------------------------+
| mnt_sb (8 bytes) - 超级块指针            |
+------------------------------------------+
| mnt_mounts (16 bytes) - 子挂载链表头     |
+------------------------------------------+
| mnt_child (16 bytes) - 兄弟链表节点      |
+------------------------------------------+
| mnt_flags (4 bytes) - 挂载标志           |
+------------------------------------------+
| mnt_devname (8 bytes) - 设备名指针       |
+------------------------------------------+
| mnt_list / mnt_expire / mnt_share        |
| mnt_slave_list / mnt_slave (各16 bytes)  |
+------------------------------------------+
| mnt_master (8 bytes) / mnt_ns (8 bytes)  |
+------------------------------------------+
| mnt_id / mnt_group_id (各4 bytes)        |
+------------------------------------------+
| mnt_count / mnt_expiry_mark              |
| mnt_pinned / mnt_ghosts (各4 bytes)      |
+------------------------------------------+
| mnt_writers (8 bytes) - 写者计数指针     |
+==========================================+

关键字段:

  • mnt_mountpoint:挂载点 dentry(位于父文件系统中)
  • mnt_root:本文件系统的根 dentry
  • mnt_parent:父挂载实例(根文件系统指向自己)
  • mnt_sb:关联的超级块
  • mnt_share/mnt_slave_list/mnt_master:共享子树(shared subtree)机制

文字说明:

struct vfsmount 是"一次挂载动作"的载体,它把三样东西捆绑在一起:被挂载的文件系统 (mnt_sb)、它在全局目录树中的入口 (mnt_root)、以及它挂在父文件系统上的位置 (mnt_mountpoint)。三者缺一不可:没有 mnt_root 就不知道这次挂载的文件系统根在哪里;没有 mnt_mountpoint 就不知道外界从哪里访问它;没有 mnt_sb 则数据无从读取。

整个结构按职责可分成几组:

  • 哈希定位(一棵"入口") :mnt_hash 参与挂载哈希表(见第 9 章),供路径查找用 (父挂载, 挂载点 dentry) 快速反查"这里挂的是什么"。
  • 挂载树链接(嵌套关系) :mnt_parent + mnt_child + mnt_mounts 把挂载实例组织成一棵挂载树 。mnt_parent 指向父挂载;父挂载用 mnt_mounts 这个链表头收集所有直接子挂载,每个子挂载再用自己的 mnt_child 链到父的 mnt_mounts 上。挂载点可以嵌套(如 /mnt/a 里再挂 /mnt/a/b),所以 mnt_mountpoint 位于父文件系统,而下一层挂载又挂在它的子目录上。
  • 命名空间串联(一张视图) :mnt_list 把该命名空间内所有挂载串进 mnt_namespace.list,供 /proc/mounts 遍历输出。
  • 传播语义(shared subtree) :mnt_share/mnt_slave_list/mnt_slave/mnt_master 实现共享子树传播(见第 6 章)。同一"对等组"的挂载用 mnt_share 环链起来;主从挂载用 mnt_master/mnt_slave 关联(slave 挂在 master 的 mnt_slave_list 上)。
  • 身份标识 :mnt_id 是全系统唯一的挂载标识(即 /proc/mountinfo 第一列);mnt_group_id 标识该挂载所属"对等组"(同组共享传播)。
  • 生命周期计数 :mnt_count 是引用计数。一个 vfsmount 可能被多个命名空间、多个路径同时引用(绑定挂载、容器共享),只有归零才真正释放内存;mnt_pinned/mnt_ghosts 用于卸载期间的特殊状态(钉住/幽灵)管理。
  • 写访问记账 :mnt_writers(per-CPU 下是指针)实现 mnt_want_write()/mnt_drop_write() 对"只读挂载拒绝写入"的精确记账(见 10.2)。

举例 :执行 mount -t ext4 /dev/sda1 /mnt 后,内核新建一个 vfsmount,其状态大致为:

字段 值 含义
mnt_sb 新 ext4 super_block 关联的超级块
mnt_root ext4 根目录 dentry 整个盘就是一个文件系统,根即 /
mnt_mountpoint 父文件系统里 /mnt 的 dentry 挂载点
mnt_parent 根文件系统的 vfsmount 挂载在根文件系统上
mnt_mounts 空链表 还没有子挂载
mnt_id 全局递增 ID(如 42) 挂载标识

此后用户访问 /mnt/foo,路径查找走到 /mnt 这个挂载点时,就借助 mnt_mountpoint→mnt_root 的映射"下钻"到 ext4 文件系统。

2.2 struct super_block ------ 超级块

定义位置:include/linux/fs.h

c 复制代码
struct super_block {
    struct list_head       s_list;         // 所有超级块链表
    dev_t                  s_dev;          // 设备标识符
    unsigned long          s_blocksize;    // 块大小
    unsigned char          s_blocksize_bits; // 块大小位数
    unsigned char          s_dirt;         // 脏标志
    loff_t                 s_maxbytes;     // 最大文件大小
    struct file_system_type *s_type;       // 文件系统类型
    const struct super_operations *s_op;   // 超级块操作
    const struct dquot_operations *dq_op;  // 磁盘配额操作
    const struct quotactl_ops *s_qcop;     // 配额控制操作
    unsigned long          s_flags;        // 挂载标志
    unsigned long          s_magic;        // 魔数
    struct dentry          *s_root;        // 根目录dentry
    struct rw_semaphore    s_umount;       // 卸载信号量
    struct mutex           s_lock;         // 超级块锁
    int                    s_count;        // 引用计数
    atomic_t               s_active;       // 活跃引用
    struct list_head       s_inodes;       // 所有inode链表
    struct hlist_head      s_anon;         // 匿名dentry
    struct list_head       s_files;        // 打开文件链表
    struct list_head       s_dentry_lru;   // dentry LRU链表
    struct block_device    *s_bdev;        // 块设备
    struct backing_dev_info *s_bdi;        // 后备设备信息
    struct list_head       s_instances;    // 实例链表
    struct quota_info      s_dquot;        // 配额信息
    int                    s_frozen;       // 冻结状态
    wait_queue_head_t      s_wait_unfrozen;// 解冻等待队列
    char                   s_id[32];       // 信息名称
    void                   *s_fs_info;     // 文件系统私有信息
    fmode_t                s_mode;         // 模式
    struct mutex           s_vfs_rename_mutex; // 重命名互斥锁
    u32                    s_time_gran;    // 时间粒度
    char                   *s_subtype;     // 子类型
    char                   *s_options;     // 挂载选项
};

内存布局(总大小约 400-500 字节):包含 s_list、s_dev、s_blocksize、各操作指针、s_flags、s_magic、s_root、s_umount(40 字节读写信号量)、s_lock(32 字节互斥锁)、各链表头、s_dquot(约 100 字节配额信息)、s_id32、s_fs_info、s_vfs_rename_mutex 等。

文字说明:

struct super_block 是对"一个被挂载的文件系统实例"的元信息总账 ,它集中了文件系统的"户口信息"(类型、设备、块大小、魔数)、"全局结构"(根 dentry、inode 表、dentry 缓存)、"操作回调"(s_op 等)和"并发控制"(各锁)。注意:一个物理设备(或一个文件系统)在系统里通常只有一个 super_block,但可以有多个 vfsmount 挂在不同位置------这正是 sget()(第 5 章)要"去重"的原因。

按职责分组理解字段:

  • 身份与类型 :s_dev(设备号,也是查找索引)、s_magic(魔数,如 ext4 的 0xEF53,用于校验盘是否匹配)、s_type(指向 file_system_type,说明"我是 ext4 还是 tmpfs")、s_id[32](可读名字,出现在 /proc/mounts)。
  • 几何信息 :s_blocksize/s_blocksize_bits(块大小及其对数)、s_maxbytes(单文件最大尺寸)。
  • 文件系统"根" :s_root 是该文件系统根目录的 dentry,是 vfsmount->mnt_root 的来源(simple_set_mnt 里 mnt->mnt_root = dget(sb->s_root))。
  • 全局对象表 :s_inodes(本文件系统全部 inode 链表)、s_files(挂载期间打开的文件链表)、s_dentry_lru(未使用 dentry 的 LRU 淘汰链)、s_anon(供 NFS 导出用的匿名 dentry)。这些链表让"回收、扫描、统计"得以按文件系统为单位进行。
  • 操作回调 :s_op(super_operations,见 5.2)、dq_op/s_qcop(配额操作)、s_export_op(NFS 导出)。
  • 并发控制 :s_umount(读写信号量,保护"卸载"临界区,路径查找拿读锁)、s_lock(互斥锁,保护内部状态)、s_vfs_rename_mutex(跨目录重命名防死锁的 kludge)。
  • 引用计数 :s_count(引用计数,初值带 S_BIAS 防饥饿)、s_active(活跃引用,"真正在用"的标志,归零触发 kill_sb)、s_dirt(脏标志)。
  • 设备与后端 :s_bdev(底层块设备)、s_bdi(backing_dev_info,IO 能力描述)、s_mtd(MTD 设备)。
  • 私有与挂载数据 :s_fs_info(文件系统私有指针,如 ext4 的 ext4_sb_info)、s_options(保存的挂载选项)、s_subtype(子类型,如 fuse 的 "fuseblk")。

举例 :对 mount -t ext4 /dev/sda1 /mnt:

  • s_magic = 0xEF53(EXT4_SUPER_MAGIC)
  • s_bdev = /dev/sda1 对应的 block_device
  • s_root = 该盘根目录 inode(inode 号 2)对应的 dentry
  • s_fs_info 指向 struct ext4_sb_info,内含 ext4 专属的块组描述符、日志(jbd2)信息等
  • s_id = "sda1"

2.3 struct file_system_type ------ 文件系统类型

c 复制代码
struct file_system_type {
    const char              *name;          // 文件系统名称
    int                     fs_flags;       // 标志
#define FS_REQUIRES_DEV    1               // 需要块设备
#define FS_BINARY_MOUNTDATA 2              // 二进制挂载数据
#define FS_HAS_SUBTYPE     4               // 有子类型
    struct super_block      *(*get_sb)(struct file_system_type *, int,
                                       const char *, void *, struct vfsmount *);
    void                    (*kill_sb)(struct super_block *);
    struct module           *owner;         // 所属模块
    struct file_system_type *next;          // 链表下一个
    struct list_head        fs_supers;      // 此类型的所有超级块
    struct lock_class_key   s_lock_key;     // 锁类键
    struct lock_class_key   s_umount_key;   // 卸载锁键
};

fs_flags:

  • FS_REQUIRES_DEV:需要块设备(ext4、xfs)
  • FS_BINARY_MOUNTDATA:二进制挂载数据
  • FS_HAS_SUBTYPE:有子类型(fuse)

文字说明:

struct file_system_type 是"一类文件系统"的注册登记表 。所有已注册的文件系统类型通过 next 串成全局链表 file_systems------mount -t ext4 时,do_kern_mount 就在这张链表里按名字 "ext4" 查找。它描述的是"这一类"的公共能力,而非某一次挂载:同一类型的多次挂载共享同一个 file_system_type,但各有各的 super_block。

  • name:类型名字符串,就是 mount -t <name> 里 <name> 的来源。
  • get_sb:本次挂载的关键回调 ,负责创建(或复用)super_block 并填充 vfsmount。VFS 提供通用实现 get_sb_bdev(块设备)、get_sb_nodev(无设备)、get_sb_single(单实例),文件系统只需提供一个 fill_super 回调;复杂文件系统(ext4、NFS)可自定义 get_sb。
  • kill_sb:销毁 super_block 的回调(块设备用 kill_block_super,无设备用 kill_anon_super,二者内部都调用 generic_shutdown_super)。
  • fs_flags:类型级能力位。
  • owner:所属内核模块(动态加载的文件系统用于模块引用计数,防止卸载时仍在用)。
  • fs_supers:该类型所有已创建 超级块的链表头。sget() 的去重查找就在这条链表上遍历。

fs_flags 各标志含义:

  • FS_REQUIRES_DEV:必须有底层块设备(ext4、xfs)。do_kern_mount 据此校验设备名。
  • FS_BINARY_MOUNTDATA:挂载选项是二进制(而非可打印文本),如某些网络文件系统。
  • FS_HAS_SUBTYPE:支持子类型,如 fuse 以 type.subtype 形式出现在 /proc/mounts。

举例 :内核启动执行 register_filesystem(&ext4_fs_type),把 ext4_fs_type.name="ext4"、get_sb=ext4_get_sb 挂进 file_systems。当用户 mount -t ext4 ... 时,do_kern_mount 找到这个登记项并调用其 get_sb。

2.4 struct mnt_namespace ------ 挂载命名空间

arduino 复制代码
struct mnt_namespace {
    atomic_t        count;          // 引用计数
    struct vfsmount *root;          // 根挂载
    struct list_head list;          // 所有挂载链表
    wait_queue_head_t poll;         // 等待队列
    int             event;          // 事件计数
};

每个进程通过 current->nsproxy->mnt_ns 指向自己的挂载命名空间,不同命名空间可有独立的挂载视图(用于容器隔离)。

文字说明:

struct mnt_namespace 定义了一组进程所看到的挂载树视图 。一个命名空间 = 一棵挂载树(从 root 出发)+ 该视图内全部挂载的链表(list)。多个进程可共享同一命名空间(默认情况);用 clone(CLONE_NEWNS) 或 unshare(CLONE_NEWNS) 则复制出一份独立视图。

  • count:引用计数,被共享时递增。
  • root:该命名空间的根 vfsmount。命名空间"看到的 /"就是从这里开始的。
  • list:该命名空间内全部 vfsmount 串成的链表,/proc/mounts 即遍历此链表生成。
  • poll/event:用于 mount/umount 事件的通知机制,/proc/self/mounts 变更可据此唤醒等待者。

举例(容器隔离) :容器 A、B 各自 clone(CLONE_NEWNS) 出一个新命名空间。初始内容一致(浅拷贝挂载表);随后 A 在自身命名空间内 mount 一个数据卷,只会改写 A 的 mnt_namespace,B 完全不可见。内核通过 task_struct->nsproxy->mnt_ns 切换,正是"不同容器看到不同 /proc、不同挂载点"的根本原因。


第 3 章 挂载标志体系

c 复制代码
// 超级块标志 (MS_*)
#define MS_RDONLY       1       // 只读挂载
#define MS_NOSUID       2       // 忽略suid/sgid
#define MS_NODEV        4       // 禁止设备文件
#define MS_NOEXEC       8       // 禁止执行
#define MS_SYNCHRONOUS  16      // 同步写入
#define MS_REMOUNT      32      // 重新挂载
#define MS_MANDLOCK     64      // 允许强制锁
#define MS_DIRSYNC      128     // 目录同步
#define MS_NOATIME      1024    // 不更新访问时间
#define MS_NODIRATIME   2048    // 不更新目录访问时间
#define MS_BIND         4096    // 绑定挂载
#define MS_MOVE         8192    // 移动挂载
#define MS_REC          16384   // 递归
#define MS_SHARED       (1<<20) // 共享挂载
#define MS_PRIVATE      (1<<18) // 私有挂载
#define MS_SLAVE        (1<<19) // 从属挂载
#define MS_UNBINDABLE   (1<<17) // 不可绑定

// vfsmount标志 (MNT_*)
#define MNT_NOSUID      0x01    // 忽略suid
#define MNT_NODEV       0x02    // 禁止设备
#define MNT_NOEXEC      0x04    // 禁止执行
#define MNT_NOATIME     0x08    // 不更新atime
#define MNT_NODIRATIME  0x10    // 不更新目录atime
#define MNT_RELATIME    0x20    // 相对atime
#define MNT_READONLY    0x40    // 只读
#define MNT_SHARED      0x1000  // 共享
#define MNT_UNBINDABLE  0x2000  // 不可绑定

MS_* 标志来自 mount() 系统调用,MNT_* 标志是内核内部每挂载点的标志,do_mount() 负责将 MS_* 转换为 MNT_*。

文字说明:

Linux 挂载标志分两层:MS_ * 是 mount(2) 系统调用传给内核的"请求语义";MNT_ * 是内核为每个挂载点 持久保存的"运行期属性"。do_mount() 开头做了一次"请求 → 属性"的转换(见 4.2 第 2 步),转换后的 mnt_flags 存进 vfsmount->mnt_flags。此后对读写、执行、设备访问的检查都看 mnt_flags,不再重复解释 mount() 的参数。

  • 语义相近、一一对应 :MS_NOSUID→MNT_NOSUID、MS_NODEV→MNT_NODEV、MS_NOEXEC→MNT_NOEXEC、MS_NOATIME→MNT_NOATIME、MS_RDONLY→MNT_READONLY。
  • 行为类、不落进 mnt_flags :MS_REMOUNT/MS_BIND/MS_MOVE/MS_REC/MS_SHARED 等决定 do_mount() 走哪条处理路径(重挂/绑定/移动/改类型/新挂载),是一次性控制字,不持久保存。
  • 只存在于 mnt_flags :如 MNT_RELATIME(相对 atime)、MNT_UNBINDABLE(不可绑定),没有对应的 MS_*,通常由内核按传播类型自动设置。

举例 :mount -o ro /dev/sda1 /mnt → 传入 MS_RDONLY=1 → do_mount() 转成 mnt_flags |= MNT_READONLY 存入 vfsmount->mnt_flags。之后对 /mnt 下任何文件 write() 时,mnt_want_write() 检查到 MNT_READONLY 即返回 -EROFS,与当初用户怎么敲的命令已无关。


第 4 章 mount() 系统调用完整流程

4.1 调用链总览

c 复制代码
用户空间: mount("/dev/sda1", "/mnt", "ext4", MS_NOATIME, NULL)
                      |
                      v
+--------------------------------------------------+
| SYSCALL_DEFINE5(mount, char __user *, dev_name,  |
|                 char __user *, dir_name, ...)    |
| [fs/namespace.c]                                 |
+--------------------------------------------------+
                      |
          +-----------+-----------+
          |                       |
          v                       v
+------------------+     +------------------+
| copy_mount_string|     | copy_mount_options
| 复制设备名和类型  |     | 复制挂载选项      |
+------------------+     +------------------+
          |                       |
          +-----------+-----------+
                      |
                      v
+--------------------------------------------------+
| do_mount(dev_name, dir_name, type_page,          |
|           flags, data_page)                      |
| [fs/namespace.c:1912]                            |
+--------------------------------------------------+
                      |
          +-----------+-----------+-----------+
          |           |           |           |
          v           v           v           v
     MS_REMOUNT   MS_BIND    MS_MOVE    新挂载
          |           |           |           |
          v           v           v           v
+------------+ +------------+ +------------+ +------------+
|do_remount()| |do_loopback| |do_move_mount| |do_new_mount|
+------------+ +------------+ +------------+ +------------+

4.2 do_mount() 核心逻辑

c 复制代码
long do_mount(char *dev_name, char *dir_name, char *type_page,
              unsigned long flags, void *data_page)
{
    struct path path;
    int retval = 0;
    int mnt_flags = 0;

    // 1. 解析挂载标志
    if ((flags & MS_MGC_MSK) == MS_MGC_VAL)
        flags &= ~MS_MGC_MSK;  // 去除魔数

    // 2. 分离每挂载点标志
    if (flags & MS_NOSUID)  mnt_flags |= MNT_NOSUID;
    if (flags & MS_NODEV)   mnt_flags |= MNT_NODEV;
    if (flags & MS_NOEXEC)  mnt_flags |= MNT_NOEXEC;
    if (flags & MS_NOATIME) mnt_flags |= MNT_NOATIME;
    if (flags & MS_RDONLY)  mnt_flags |= MNT_READONLY;

    // 3. 查找挂载点路径
    retval = kern_path(dir_name, LOOKUP_FOLLOW, &path);
    if (retval)
        return retval;

    // 4. 安全检查
    retval = security_sb_mount(dev_name, &path, type_page, flags, data_page);
    if (retval)
        goto dput_out;

    // 5. 根据标志分发处理
    if (flags & MS_REMOUNT)
        retval = do_remount(&path, flags & ~MS_REMOUNT, mnt_flags, data_page);
    else if (flags & MS_BIND)
        retval = do_loopback(&path, dev_name, flags & MS_REC);
    else if (flags & (MS_SHARED | MS_PRIVATE | MS_SLAVE | MS_UNBINDABLE))
        retval = do_change_type(&path, flags);
    else if (flags & MS_MOVE)
        retval = do_move_mount(&path, dev_name);
    else
        retval = do_new_mount(&path, type_page, flags, mnt_flags,
                              dev_name, data_page);

dput_out:
    path_put(&path);
    return retval;
}

do_mount() 依据标志将挂载请求分发到四类处理函数:

  • MS_REMOUNT → do_remount()(重新挂载,修改挂载选项)
  • MS_BIND → do_loopback()(绑定挂载)
  • MS_SHARED/MS_PRIVATE/MS_SLAVE/MS_UNBINDABLE → do_change_type()(修改传播类型)
  • MS_MOVE → do_move_mount()(移动挂载)
  • 其他 → do_new_mount()(新挂载)

文字说明(对应 4.1 流程图) :do_mount() 是 mount(2) 的分发中枢,执行可概括为五步:

  1. 去魔数 :老式 mount(2) 在 flags 高 16 位携带 MS_MGC_VAL(0xC0ED0000)表示"使用新语义"。当 (flags & MS_MGC_MSK) == MS_MGC_VAL 时将其清除,避免干扰后续标志判断。
  2. 转换挂载点标志 :把 MS_NOSUID 等请求位翻译成 mnt_flags 里的 MNT_* 位(见第 3 章)。产物 mnt_flags 沿调用链一路传到挂载点,最终写入 vfsmount->mnt_flags。
  3. 解析挂载点路径 :kern_path(dir_name, LOOKUP_FOLLOW, &path) 把 /mnt 解析成 (vfsmount, dentry) 对。LOOKUP_FOLLOW 表示路径末段若是符号链接则跟随解析。
  4. 安全检查 :security_sb_mount() 让 LSM(SELinux、AppArmor 等)有机会拒绝本次挂载(返回 -EACCES 等)。
  5. 按标志分发 :flags 决定调用哪个 do_*。注意判断顺序有讲究------MS_REMOUNT、MS_BIND、传播类型、MS_MOVE 都是"非新挂载"的快捷路径,最后才落到 do_new_mount()。

举例 :mount -t tmpfs none /tmp 不携带 REMOUNT/BIND/MOVE/传播位,走最后分支 do_new_mount(),经 get_sb_nodev 创建 tmpfs 超级块挂到 /tmp;而 mount -o remount,ro / 带 MS_REMOUNT,走 do_remount() 原地把根挂载改为只读。

4.3 新挂载流程 (do_new_mount)

c 复制代码
+--------------------------------------------------+
| do_new_mount(path, type, flags, mnt_flags,       |
|               dev_name, data)                    |
| [fs/namespace.c:1645]                            |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| do_kern_mount(type, flags, name, data)           |
| [fs/super.c:999]                                 |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| vfs_kern_mount(type, flags, name, data)          |
| [fs/super.c:914]                                 |
+--------------------------------------------------+
                      |
          +-----------+-----------+
          |                       |
          v                       v
+------------------+     +------------------+
| alloc_vfsmnt()   |     | security_sb_copy_data
| 分配vfsmount结构  |     | 安全模块处理     |
+------------------+     +------------------+
          |
          v
+--------------------------------------------------+
| type->get_sb(type, flags, name, data, mnt)       |
| 调用文件系统的get_sb函数                          |
| 例如: ext4_get_sb, get_sb_bdev, get_sb_nodev     |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| sget(fs_type, test, set, data)                   |
| [fs/super.c:338]                                 |
| 查找或创建超级块                                  |
+--------------------------------------------------+
                      |
          +-----------+-----------+
          |                       |
          v                       v
+------------------+     +------------------+
| 在fs_supers中查找|     | alloc_super()    |
| 已存在的超级块    |     | 分配新超级块      |
+------------------+     +------------------+
          |                       |
          +-----------+-----------+
                      |
                      v
+--------------------------------------------------+
| fill_super(sb, data, silent)                     |
| 文件系统特定的超级块初始化                         |
| 例如: ext4_fill_super                            |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| simple_set_mnt(mnt, sb)                          |
| mnt->mnt_sb = sb                                 |
| mnt->mnt_root = dget(sb->s_root)                 |
+--------------------------------------------------+

文字说明(对应上节流程图) :do_new_mount() 负责"从零挂一个新文件系统",分两大步------do_kern_mount() 先把 super_block 和 vfsmount 造出来,do_add_mount()(下一节)再把它挂进挂载树。

  • do_kern_mount(type,...):按 type(如 "ext4")在全局 file_systems 链表定位 file_system_type,转发给 vfs_kern_mount。这也是"内核模块文件系统要先 modprobe 注册"的原因。
  • vfs_kern_mount():核心工厂。先 alloc_vfsmnt() 申请一个空 vfsmount,做安全数据拷贝,再调用 type->get_sb()。get_sb 的职责是得到 super_block 并填好 mnt->mnt_sb、mnt->mnt_root。
  • get_sb → sget():先尝试复用 已存在的超级块(同设备同类型只一个),没有才 alloc_super() 新建,再 fill_super 做文件系统特有的初始化。fill_super 是最重的步骤:ext4 要读磁盘超级块、校验魔数、读块组描述符、装载日志、建立根 inode(sb->s_root)。
  • simple_set_mnt():收尾------mnt->mnt_sb = sb; mnt->mnt_root = dget(sb->s_root);,把 vfsmount 与 super_block 绑死。

举例 :mount -t ext4 /dev/sda1 /mnt 的 get_sb=ext4_get_sb 内部调用 get_sb_bdev:打开 /dev/sda1 → sget 去重 → ext4_fill_super 读盘初始化 → simple_set_mnt 绑定。若此前 /dev/sda1 已挂过,sget 直接复用旧 super_block,跳过重读。

4.4 挂载入树 (do_add_mount)

lua 复制代码
+--------------------------------------------------+
| do_add_mount(mnt, path, mnt_flags, NULL)         |
| [fs/namespace.c:1670]                            |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| graft_tree(mnt, path)                            |
| 将新挂载连接到挂载树                              |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| attach_recursive_mnt(mnt, path, &parent_path)    |
| [fs/namespace.c:1540]                            |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| mnt_set_mountpoint(path->mnt, path->dentry, mnt) |
| 设置挂载关系                                      |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| commit_tree(mnt)                                 |
| 将挂载添加到命名空间                              |
+--------------------------------------------------+

mnt_set_mountpoint 建立挂载关系:设置 mnt->mnt_parent = path->mnt、mnt->mnt_mountpoint = path->dentry、path->dentry->d_mounted++。

文字说明(对应上节流程图) :do_add_mount() 把已造好的 vfsmount 接进挂载树 ,核心是 graft_tree()(嫁接),其内部:

  1. attach_recursive_mnt() 递归处理传播关系(若新挂载带 shared/slave 语义,需同步复制到对等组),并得到 parent_path。
  2. mnt_set_mountpoint() 建立父子关系------四件事:mnt->mnt_parent = path->mnt(记下父挂载)、mnt->mnt_mountpoint = path->dentry(记下挂在父的哪个目录上)、把 mnt 链进父的 mnt_mounts、path->dentry->d_mounted++(标记该 dentry 现在是挂载点)。d_mounted 是路径查找判断"要不要下钻"的依据。
  3. commit_tree() 把新挂载链进命名空间 list、挂载哈希表,并登记 mnt_id。

为什么要 d_mounted++ :路径查找走到某个 dentry 时,若 d_mounted > 0 说明它是挂载点,须经 follow_down() 下钻到被挂载子树的根(见第 11 章)。多个挂载可叠在同一目录(d_mounted 递增多次),卸载时逐一 d_mounted--。

举例 :把 ext4 挂到 /mnt 后,根文件系统中 mnt 目录 dentry 的 d_mounted 变为 1。用户 cd /mnt 时 pathwalk 在 mnt 上发现 d_mounted=1,随即 follow_down() 切换到 ext4 的 vfsmount。


第 5 章 超级块管理与生命周期

5.1 sget() 流程 ------ 查找或创建超级块

lua 复制代码
sget() 流程:
+--------------------------------------------------+
| sget(struct file_system_type *type,              |
|      int (*test)(struct super_block *, void *),  |
|      int (*set)(struct super_block *, void *),   |
|      void *data)                                 |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| spin_lock(&sb_lock)                              |
| 加锁保护超级块链表                                |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| 遍历 type->fs_supers 链表                        |
| 查找匹配的超级块                                  |
+--------------------------------------------------+
                      |
          +-----------+-----------+
          | 找到?                  | 未找到
          v                       v
+------------------+     +------------------+
| grab_super(old)  |     | alloc_super(type)|
| 增加引用并返回    |     | 分配新超级块      |
+------------------+     +------------------+
                                  |
                                  v
+--------------------------------------------------+
| set(s, data)                                     |
| 调用set回调初始化超级块                           |
| 例如: set_bdev_super, set_anon_super             |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| 初始化超级块:                                     |
| s->s_type = type                                 |
| strlcpy(s->s_id, type->name, sizeof(s->s_id))    |
| list_add_tail(&s->s_list, &super_blocks)         |
| list_add(&s->s_instances, &type->fs_supers)      |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| get_filesystem(type)                             |
| 增加文件系统类型引用                              |
+--------------------------------------------------+

sget() 的核心思想:同一个物理设备(或同构的文件系统)只需一个超级块。若已存在则 grab_super() 增加引用复用;否则 alloc_super() 新建。

文字说明(对应上节流程图) :sget() 是超级块的"单例工厂",保证同一类型、同一标识(通常按设备)只存在一个 super_block。

  • 加锁后遍历 type->fs_supers 链表,用 test() 回调判断是否命中(如 test_bdev_super 比较 s_bdev 是否同一设备)。
  • 命中 :grab_super() 增加 s_count/s_active 引用后复用,避免重复读盘、重复初始化。这是"同一块盘被 mount 两次"时分叉点:只读方式允许复用,读写方式通常返回 -EBUSY。
  • 未命中 :alloc_super() 分配一块新 super_block,随后调用 set() 回调(set_bdev_super 填 s_bdev/s_dev,set_anon_super 分配匿名设备号)把"身份"写进去。
  • 无论新建还是复用,最终都要把 s 链入全局 super_blocks(s_list)和 type->fs_supers(s_instances),并 get_filesystem() 保持类型模块的引用。

举例 :先后执行 mount -t ext4 /dev/sda1 /mnt1 与 mount -t ext4 /dev/sda1 /mnt2:第二次 test_bdev_super 发现已有同设备的 super_block,于是 grab_super 复用(而非重读磁盘),得到同一个 super_block、两个不同 vfsmount,两个挂载点共享全部数据与缓存。

5.2 super_operations

scss 复制代码
struct super_operations {
    struct inode *(*alloc_inode)(struct super_block *sb);
    void (*destroy_inode)(struct inode *);
    void (*dirty_inode)(struct inode *);
    int (*write_inode)(struct inode *, struct writeback_control *wbc);
    int (*drop_inode)(struct inode *);
    void (*put_super)(struct super_block *);
    void (*write_super)(struct super_block *);
    int (*sync_fs)(struct super_block *sb, int wait);
    int (*freeze_fs)(struct super_block *);
    int (*unfreeze_fs)(struct super_block *);
    int (*statfs)(struct dentry *, struct kstatfs *);
    int (*remount_fs)(struct super_block *, int *, char *);
    void (*clear_inode)(struct inode *);
    void (*umount_begin)(struct super_block *);
    int (*show_options)(struct seq_file *, struct vfsmount *);
    ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
    ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
    int (*bdev_try_to_free_page)(struct super_block*, struct page*, gfp_t);
};

文字说明 :super_operations 是超级块层面的一族回调,由文件系统实现、VFS 在恰当时机调用。按职责可分几组:

  • inode 生命周期 :alloc_inode/destroy_inode 分配/释放该文件系统的 inode(ext4 会分配 ext4_inode_info 这类私有扩展结构);clear_inode/drop_inode/write_inode/dirty_inode 管理 inode 的脏化与回写。
  • 超级块生命周期 :put_super(卸载时释放私有数据)、write_super/sync_fs(把超级块/元数据刷盘)、freeze_fs/unfreeze_fs(冻结/解冻,用于快照或系统休眠)。
  • 系统调用支撑 :statfs(实现 statfs(2),输出 kstatfs)、remount_fs(实现 mount -o remount)。
  • 卸载钩子 :umount_begin(umount 开始时调用,NFS 用它向服务器发起切断通知)。
  • 其他 :show_options(向 /proc/mounts 输出挂载选项)、quota_read/quota_write(配额读写)、bdev_try_to_free_page(内存紧张时回收块设备页缓存)。

举例 :ext4 的 ext4_sops 把 alloc_inode 设为 ext4_alloc_inode------每次创建一个 inode 时,VFS 会额外分配一块 ext4_inode_info 挂在 inode 之后,用于保存 ext4 专属字段(扩展属性、块映射缓存等)。这正是"VFS 通用对象 + 文件系统私有扩展"的标准组合模式。

5.3 超级块生命周期

lua 复制代码
+------------------+                    +------------------+
| alloc_super()    |                    | sget() 查找已存在 |
| 新分配           |                    | 的超级块          |
+--------+---------+                    +--------+---------+
         |                                       |
         v                                       v
+--------+---------+                    +--------+---------+
| s_active = 1     |                    | grab_super()     |
| s_count = S_BIAS |                    | 增加活跃引用      |
| MS_ACTIVE 未设置 |                    +--------+---------+
+--------+---------+                             |
         |                                       |
         v                                       |
+--------+---------+                             |
| fill_super()     |                             |
| 文件系统初始化    |                             |
| 设置 s_root      |                             |
+--------+---------+                             |
         |                                       |
         v                                       |
+--------+---------+                             |
| s_flags |= MS_ACTIVE                            |
| 激活超级块        |                             |
+--------+---------+                             |
         |                                       |
         +-----------------+---------------------+
                           |
                           v
+------------------+
| 超级块活跃使用中  |
| s_active > 0     |
+--------+---------+
         |
         | deactivate_super()
         v
+------------------+
| s_active--       |
| 如果 s_active == 0|
+--------+---------+
         |
         v
+------------------+
| fs->kill_sb(s)   |
| 例如: kill_block_super
| generic_shutdown_super
+--------+---------+
         |
         v
+------------------+
| 从链表移除        |
| list_del(&s->s_list)
| list_del(&s->s_instances)
+--------+---------+
         |
         v
+------------------+
| destroy_super(s) |
| 释放超级块内存    |
+------------------+

关键计数:

  • s_count:引用计数(初值 S_BIAS,防饥饿)
  • s_active:活跃引用,记录"真正使用中"的引用数。引用计数 s_count 耗尽时调用 kill_sb;活跃计数 s_active 用于区分"已引用但可能处于卸载中"的状态。

文字说明(对应上节生命周期图) :超级块有两条独立计数,理解它们的差异是看懂卸载的钥匙:

  • s_active(原子,活跃引用):表示"这个超级块正被真正使用"。每个 vfsmount 持有它一份(mntget 时 atomic_inc(&sb->s_active))。只要还有 vfsmount 在用,s_active > 0,超级块就不能被回收。deactivate_super() 每次 s_active--,归零即触发 kill_sb。
  • s_count(普通引用):保护超级块本身"不被意外释放"(如正被 sget 遍历查找、正被某操作临时持有)。初值取 S_BIAS 是防饥饿技巧 ------保证已注销(s_count 归 0 被移除)的文件系统不会被新的 sget 误当活跃实例取走。

完整一生(对应上图从分配、激活、使用到销毁的每个方框):

  1. alloc_super():置 s_active=0、s_count=S_BIAS,尚未激活。
  2. fill_super() 成功、挂入挂载树后:s_flags |= MS_ACTIVE,标记"可被路径查找使用"。
  3. 活跃使用期:每次打开/路径穿越持有 s_active 引用。
  4. deactivate_locked_super():s_active 归零(不再有 vfsmount 引用)→ 调用 fs->kill_sb(s)。
  5. kill_block_super/kill_anon_super → generic_shutdown_super():刷脏数据(sync_filesystem)、丢弃 dcache(shrink_dcache_for_umount)、回收 inode(invalidate_inodes)、回调 sop->put_super、从 super_blocks 与 fs_supers 链表摘除。
  6. destroy_super():释放内存,生命周期结束。

举例 :umount /mnt 时,最后持有该 ext4 超级块的 vfsmount 被释放,deactivate_super 使 s_active 归零,于是走 kill_block_super → generic_shutdown_super:先把脏的元数据/页缓存刷回 /dev/sda1,再回收 inode 与 dentry 缓存,最后从链表摘除并释放。若卸载时仍有进程正在用(s_active 或 mnt_count 未归零),则卸载被推迟或返回 -EBUSY。


第 6 章 挂载类型详解

6.1 块设备挂载 (get_sb_bdev)

lua 复制代码
mount -t ext4 /dev/sda1 /mnt

+--------------------------------------------------+
| get_sb_bdev(fs_type, flags, dev_name, data,      |
|              fill_super, mnt)                    |
| [fs/super.c:770]                                 |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| open_bdev_exclusive(dev_name, mode, fs_type)     |
| 独占打开块设备                                    |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| sget(fs_type, test_bdev_super, set_bdev_super, bdev)|
| 查找或创建超级块                                  |
+--------------------------------------------------+
                      |
          +-----------+-----------+
          | 已存在?                 | 新建
          v                       v
+------------------+     +------------------+
| 检查只读标志一致性|     | 设置超级块:       |
| 返回已存在的sb   |     | s->s_bdev = bdev |
+------------------+     | s->s_dev = bdev->bd_dev
                         | sb_set_blocksize()|
                         +--------+---------+
                                  |
                                  v
+--------------------------------------------------+
| fill_super(sb, data, silent)                     |
| 文件系统特定初始化 (ext4_fill_super等)            |
| 读取超级块信息, 初始化inode, dentry等            |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| s->s_flags |= MS_ACTIVE                          |
| bdev->bd_super = s                               |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| simple_set_mnt(mnt, s)                           |
| 设置 vfsmount 关联超级块                          |
+--------------------------------------------------+

特点 :需要独占打开底层块设备;通过 sget() 去重(同一设备只读/读写模式一致性检查)。

文字说明(对应上节流程图) :get_sb_bdev 是最常用的挂载路径(ext4、xfs、btrfs 等磁盘文件系统都走它)。

  • open_bdev_exclusive():以独占 方式打开块设备,防止同一设备被多个文件系统同时操作导致元数据冲突。打开模式由 flags & MS_RDONLY 决定(O_RDONLY 或 O_RDWR)。
  • sget(fs_type, test_bdev_super, set_bdev_super, bdev):去重。test_bdev_super 检查是否已有指向同一 block_device 的超级块;set_bdev_super 为新块填 s->s_bdev = bdev、s->s_dev = bdev->bd_dev,并用 sb_set_blocksize 设块大小。
  • 若 sget 命中已存在超级块,还要做只读一致性 检查:已以只读挂载的设备不允许再以读写方式打开(open_bdev_exclusive 会失败返回 -EBUSY)。
  • fill_super():文件系统专属初始化(ext4 的 ext4_fill_super),读盘、建根、装日志等,然后 s_flags |= MS_ACTIVE、bdev->bd_super = s。
  • simple_set_mnt():绑定 vfsmount。

举例 :mount -t ext4 /dev/sda1 /mnt:

  • 独占打开 /dev/sda1
  • sget 首次未命中 → 新建,s_bdev=/dev/sda1、s_dev=0x801(主设备号 8 + 次设备号 1)
  • ext4_fill_super 读盘超级块,校验魔数 0xEF53,建立根 inode(inode 2),装载 jbd2 日志
  • 挂载完成,此后 /mnt 下的读写由 ext4 实现

6.2 无设备挂载 (get_sb_nodev)

sql 复制代码
mount -t tmpfs none /tmp

+--------------------------------------------------+
| get_sb_nodev(fs_type, flags, data, fill_super, mnt)|
| [fs/super.c:855]                                 |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| sget(fs_type, NULL, set_anon_super, NULL)        |
| 创建匿名超级块 (无块设备)                         |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| set_anon_super(s, NULL)                          |
| 分配匿名设备号:                                   |
| s->s_dev = MKDEV(0, minor)                       |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| fill_super(sb, data, silent)                     |
| 例如: shmem_fill_super (tmpfs)                   |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| s->s_flags |= MS_ACTIVE                          |
| simple_set_mnt(mnt, s)                           |
+--------------------------------------------------+

特点 :无底层块设备(tmpfs、proc、sysfs 等),set_anon_super() 分配匿名主设备号 0 的次设备号。

文字说明(对应上节流程图) :get_sb_nodev 服务无底层块设备的文件系统(tmpfs、proc、sysfs、devtmpfs 等),内容在内存中生成。

  • sget(fs_type, NULL, set_anon_super, NULL):test 传 NULL 表示"不按设备去重"(本就无设备);set_anon_super 为它分配一个匿名设备号 s->s_dev = MKDEV(0, minor)(主设备号 0,次设备号取自全局递增计数器),让该超级块也拥有唯一 s_dev 可作为查找索引。
  • fill_super:填充内容。tmpfs 用 shmem_fill_super 建好内存根目录;proc 用 proc_fill_super。
  • 激活并 simple_set_mnt。

举例 :mount -t tmpfs none /tmp:get_sb_nodev → 匿名超级块(s_dev 形如 0:40)→ shmem_fill_super 建出根目录 → 挂到 /tmp。因为内容只在内存(或部分换页到 swap),umount 后全部消失。

6.3 绑定挂载 (bind mount)

lua 复制代码
mount --bind /source /dest

+--------------------------------------------------+
| do_loopback(&path, dev_name, flags)              |
| [fs/namespace.c:1530]                            |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| kern_path(dev_name, LOOKUP_FOLLOW, &old_path)    |
| 查找源路径                                        |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| clone_mnt(old_path.mnt, old_path.dentry, flags)  |
| 克隆源挂载                                        |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| alloc_vfsmnt(old->mnt_devname)                   |
| 分配新的 vfsmount                                 |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| 设置新挂载:                                       |
| mnt->mnt_sb = old->mnt_sb                        |
| mnt->mnt_root = dget(root)                       |
| atomic_inc(&sb->s_active)                        |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| 处理共享/从属关系                                 |
| IS_MNT_SHARED, IS_MNT_SLAVE                      |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| graft_tree(mnt, path)                            |
| 将克隆的挂载连接到目标路径                        |
+--------------------------------------------------+

要点 :绑定挂载复用源挂载的超级块(mnt_sb 相同、mnt_root 相同),因此 /dest 和 /source 共享相同的 inode 和数据。

文字说明(对应上节流程图) :绑定挂载(mount --bind /source /dest)不是"挂一个新的文件系统",而是把已有的挂载点再"克隆"一份,让它同时出现在第二个位置。

  • do_loopback() 先 kern_path 解析源路径得到 old_path。
  • clone_mnt() 深拷贝出一个新 vfsmount:新对象持有与源相同的 mnt_sb、相同的 mnt_root ,仅 mnt_id、mnt_parent/mnt_mountpoint(挂到新位置)不同,并 atomic_inc(&sb->s_active) 让超级块引用 +1。
  • 若源是 shared/slave 传播挂载,clone_mnt 会把新挂载也链入相应传播关系(mnt_share/mnt_slave_list),保证传播语义延续。
  • 最后 graft_tree 挂到目标路径。

关键结论 :因为 mnt_sb、mnt_root 共享,/source 与 /dest 里的同名文件是同一个 inode ------对 /dest/foo 的修改立即反映到 /source/foo。它们不是"复制",只是"同一个文件系统出现在两个路径"。

举例 :mount --bind /home /tmp/homeview 后,/home/user/a.txt 与 /tmp/homeview/user/a.txt 指向同一 inode;在任一位置新建/删除文件,另一位置立即可见。常用于 chroot 环境把宿主目录"映射"进来。

6.4 挂载传播 (propagation)

lua 复制代码
共享挂载传播类型:

+------------------+------------------+------------------+
| MS_SHARED        | MS_SLAVE         | MS_PRIVATE       |
| 共享挂载         | 从属挂载         | 私有挂载         |
+------------------+------------------+------------------+
| 传播到对等组     | 接收主挂载事件   | 不传播           |
| 接收对等组事件   | 不传播到主挂载   | 不接收           |
+------------------+------------------+------------------+

传播关系图:

                    Master (共享)
                        |
           +------------+------------+
           |            |            |
         Slave1       Slave2      Shared2
        (从属)        (从属)      (共享)

共享挂载 (MS_SHARED) :在一个挂载点的操作会传播到对等组。例如在 /mnt 挂载新文件系统,会自动出现在 /chroot/mnt。

从属挂载 (MS_SLAVE) :只接收来自主挂载的事件,不传播事件到主挂载。

私有挂载 (MS_PRIVATE) :完全隔离,不传播也不接收。

不可绑定 (MS_UNBINDABLE) :不能作为绑定挂载的源。

文字说明(对应上节传播关系图) :挂载传播解决"一棵挂载树上的挂载/卸载要不要同步到其他位置"。核心是对等组(peer group) :同组的挂载通过 mnt_share 环链相连,组内任一成员的挂载事件都会被复制到其余成员。

  • MS_SHARED(共享) :事件双向传播------本挂载点上的新挂载/卸载会复制到对等组,对等组的变化也会传回。多个位置看到"同一份挂载结果"。
  • MS_SLAVE(从属) :单向 ------接收来自主挂载(mnt_master)的事件,但不把自己的事件传回主挂载。常用于"宿主挂载要下放到容器、容器内挂载不能影响宿主"。
  • MS_PRIVATE(私有) :完全隔离,既不传播也不接收,行为与传统挂载一致。
  • MS_UNBINDABLE(不可绑定) :不能作为绑定挂载的源,避免复制出无谓的传播环。

举例(共享传播) :

bash 复制代码
mount --make-shared /mnt            # 把 /mnt 标成共享
mount --rbind /mnt /chroot/mnt      # 绑定到容器根目录(成为同组成员)
mount -t tmpfs tmpfs /mnt/data      # 在 /mnt 下再挂一个 tmpfs
# 因 /mnt 与 /chroot/mnt 同属一个对等组,这个 tmpfs 自动出现在 /chroot/mnt/data

这就是"在宿主机给容器共享数据卷"的内核实现------一个共享点上的挂载/卸载,同组所有成员同步反映。


第 7 章 卸载流程详解

7.1 umount() 系统调用流程

c 复制代码
用户空间: umount("/mnt") 或 umount2("/mnt", MNT_FORCE)
                    |
                    v
+--------------------------------------------------+
| SYSCALL_DEFINE2(umount, char __user *, name,     |
|                  int, flags)                     |
| [fs/namespace.c]                                 |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| user_path_mountpoint_at(AT_FDCWD, name, &lookup) |
| 查找挂载点路径                                    |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| do_umount(mnt, flags)                            |
| [fs/namespace.c:1290]                            |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| 检查权限和条件:                                   |
| - check_mnt(mnt) 命名空间检查                    |
| - propagate_mount_busy() 忙碌检查                |
| - may_umount() 权限检查                          |
+--------------------------------------------------+
                      |
          +-----------+-----------+
          | 强制卸载?               | 正常卸载
          v                       v
+------------------+     +------------------+
| MNT_FORCE        |     | 检查引用计数      |
| 标记只读         |     | 检查打开文件      |
| sb->s_op->umount_begin| +------------------+
+------------------+              |
         |                        |
         +-----------+------------+
                     |
                     v
+--------------------------------------------------+
| umount_tree(mnt, propagate, &umount_list)        |
| 卸载整个子树                                      |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| detach_mnt(mnt, &parent_path)                    |
| 从挂载树分离:                                     |
| - list_del_init(&mnt->mnt_child)                 |
| - list_del_init(&mnt->mnt_hash)                  |
| - dentry->d_mounted--                            |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| release_mounts(&umount_list)                     |
| 释放挂载引用                                      |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| __mntput(mnt)                                    |
| 释放 vfsmount:                                   |
| - dput(mnt->mnt_root)                            |
| - free_vfsmnt(mnt)                               |
| - deactivate_super(sb)                           |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| deactivate_super(sb)                             |
| 减少超级块活跃引用                                |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| fs->kill_sb(sb)                                  |
| 调用文件系统的kill_sb函数                         |
| 例如: kill_block_super, kill_anon_super          |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| generic_shutdown_super(sb)                       |
| [fs/super.c:296]                                 |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| 清理超级块:                                       |
| - shrink_dcache_for_umount(sb)                   |
| - sync_filesystem(sb)                            |
| - invalidate_inodes(sb)                          |
| - sop->put_super(sb)                             |
| - list_del(&sb->s_list)                          |
| - list_del(&sb->s_instances)                     |
+--------------------------------------------------+

文字说明(对应上节流程图) :umount() 是 mount() 的逆操作,核心是"安全地断开挂载树中的一环并回收资源":

  1. user_path_mountpoint_at() 定位要卸载的 vfsmount(AT_FDCWD 表示相对当前工作目录)。
  2. do_umount() 先做三道检查:check_mnt()(必须属于当前命名空间,否则拒绝)、propagate_mount_busy()(该挂载或其子挂载正被使用则 -EBUSY,见 7.2)、may_umount()(权限校验)。
  3. MNT_FORCE(强制卸载,umount -f)分支 :先把超级块标为只读、调用 sb->s_op->umount_begin()(NFS 借此向服务器发起切断),试图让挂载从"忙碌"变为可卸。注意强制卸载对本地文件系统不一定可靠,通常用于 NFS 等网络文件系统。
  4. umount_tree() 递归收集整棵子树(连同子挂载)到 umount_list。
  5. detach_mnt():把每个 vfsmount 从挂载树摘除------list_del 摘出 mnt_child/mnt_hash、dentry->d_mounted--。
  6. release_mounts() → __mntput():递减 mnt_count 与 s_active;当超级块活跃引用归零,进入 deactivate_super → kill_sb → generic_shutdown_super 的销毁链(见 5.3)。

卸载为何可能失败(返回 -EBUSY) :有进程 cwd 在挂载点里、有文件被打开、有子挂载未卸、或仍有其他 vfsmount 引用该超级块。内核宁可返回错误也不破坏运行中的访问。

举例 :umount /mnt 时若无进程占用,detach_mnt 摘链、d_mounted-- 回 0;随后 s_active 归零触发 ext4 的 kill_block_super,把 /dev/sda1 的脏数据刷回盘并释放超级块,/mnt 恢复为父文件系统的一个普通空目录。

7.2 挂载点忙碌检测

c 复制代码
+--------------------------------------------------+
| propagate_mount_busy(mnt, refcnt)                |
| 检查挂载点是否忙碌                                |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| 遍历从属挂载:                                     |
| for_each_slave(mnt, child)                       |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| 检查子挂载是否有引用:                             |
| if (mnt_get_count(child) > 2)                    |
|     return -EBUSY                                |
+--------------------------------------------------+
                      |
                      v
+--------------------------------------------------+
| 检查当前挂载:                                     |
| if (mnt_get_count(mnt) > refcnt)                 |
|     return -EBUSY                                |
+--------------------------------------------------+

忙碌条件:
1. 有进程在挂载点目录下
2. 有文件被打开
3. 有子挂载存在
4. 有其他引用

文字说明(对应上节流程图) :propagate_mount_busy() 判断"这个挂载还能不能卸"。vfsmount 有内置的"基值引用"(每次 mntget 都 +1、mntput -1),refcnt 是调用者已知的正常持有数,因此 mnt_get_count(child) > 2 这类比较是在问"除了固有引用,是否还有额外引用(即正被使用)"。

检查会递归覆盖整棵子树 :先遍历所有从属挂载(for_each_slave),再检查自身。只要子树里任何一个挂载的引用数超过预期,就判定忙碌返回 -EBUSY 拒绝卸载------避免"上层卸了、下层还在用"的危险状态。

举例 :若有进程 cwd 在 /mnt/sub(sub 是挂在 /mnt 下的另一文件系统),此时 umount /mnt 会先检查子挂载 sub,发现其 mnt_count > 2(被该进程 cwd 额外引用),返回 -EBUSY。必须先让该进程离开(或先 umount /mnt/sub),才能卸载 /mnt。


第 8 章 挂载树与命名空间

8.1 挂载树结构

c 复制代码
                    挂载树结构示例:

                    / (root vfsmount)
                    |
    +---------------+---------------+
    |               |               |
   usr/            etc/            mnt/
    |               |               |
   local/         passwd         sda1/
    |                               |
   bin/                          data/
                                    |
                                  file.txt

vfsmount 关系图:

+==================+     +==================+
| root vfsmount    |     | usr vfsmount     |
| mnt_parent = root|     | mnt_parent = root|
| mnt_root = /     |     | mnt_root = /usr  |
| mnt_mountpoint = /|    | mnt_mountpoint = /usr
| mnt_mounts ------+---->| mnt_mounts       |
+==================+     +==================+
         ^                        ^
         |                        |
         +-----------+------------+
                     |
          mnt_child 链表

文字说明(对应上节两幅图) :上半幅展示的是目录树视角 ------挂载点把各个文件系统"缝"进一棵全局路径树,用户感知到的 /、/usr、/mnt/sda1 连续路径其实分属多个文件系统。下半幅是挂载树视角 ------每个被挂载文件系统对应一个 vfsmount,父子关系由 mnt_parent(指向上层挂载)和 mnt_mounts/mnt_child(收集/挂接子挂载)表达。这两棵"树"同构但不同对象:前者是 dentry 的嵌套,后者是 vfsmount 的嵌套。

  • 对挂载在根文件系统上的 usr vfsmount:mnt_parent = root vfsmount、mnt_mountpoint = /usr 的 dentry、mnt_root = /usr 所在文件系统的根。
  • 若 /usr/local 上又挂了一个文件系统,local vfsmount 的 mnt_parent 就指向 usr vfsmount,形成 root → usr → local 的三层链。路径查找正是在这套 mnt_parent + mnt_mountpoint 关系上决定"走到挂载点要不要下钻"(见第 11 章与《03_namei详解》)。

8.2 命名空间与挂载树

c 复制代码
struct mnt_namespace
+----------------------+
| count: 1             |
| root: root_mnt ------+--+
| list ----------------+--+--+
+----------------------+  |  |
                          |  |
                          v  v
struct vfsmount (root)    |  struct vfsmount (usr)
+----------------------+  |  +----------------------+
| mnt_ns --------------+--+  | mnt_ns --------------+--+
| mnt_parent = self   |     | mnt_parent = root    |  |
| mnt_root = /        |     | mnt_root = /usr      |  |
| mnt_mounts ---------+--+  | mnt_mountpoint = /usr|  |
| mnt_list ----------+--+|  | mnt_list ------------+--+
+----------------------+  ||  +----------------------+
                          ||
                          v|  遍历所有挂载
                          |v
                          +-----------> list_head (mnt_namespace.list)

挂载父子关系:
root_mnt
    |
    +--- usr_mnt (mnt_child 链接)
    |       |
    |       +--- local_mnt
    |
    +--- var_mnt
            |
            +--- log_mnt

命名空间 (mnt_namespace) 的含义 :一个 mnt_namespace 是一棵独立的挂载树视图,对应"一组进程看到的 / 起点"。其核心成员 root(该命名空间的根 vfsmount)和 list(该命名空间内全部挂载的链表)共同决定这棵视图的范围:

scss 复制代码
每个进程 task_struct.nsproxy.mnt_ns 指向一个 mnt_namespace
   ├─ 新进程默认继承父进程的命名空间(共享同一棵挂载树)
   ├─ clone(CLONE_NEWNS) → 复制出一个独立命名空间(浅拷贝挂载表)
   └─ 之后在该命名空间内的 mount/umount 不影响其它命名空间

共享 vs 独立 :clone(CLONE_NEWNS) 创建新命名空间时,会复制当前所有挂载的 vfsmount 引用(copy_mnt_ns → clone_mnt),使新老命名空间开始时内容一致;此后双方各自挂载/卸载即分道扬镳。这是"容器/chroot 隔离挂载视图 "的内核基础------每个容器可以有自己独立的 /proc、/sys、数据卷挂载点,互不可见。vfsmount 通过引用计数(mnt_count)支持被多个命名空间共享,卸载时只有引用归零才真正释放。

挂载树的父子链接 :vfsmount 除了参与命名空间的 list 链表,还用 mnt_child/mnt_mounts/mnt_parent 表达"挂载点嵌套"关系。上图 root_mnt → usr_mnt → local_mnt 表示 usr 挂在 root 的 /usr 上、local 又挂在 usr 的某子目录上。路径查找正是沿这套 mnt_parent + mnt_mountpoint 关系决定"走到挂载点要不要下钻到被挂载的子树根"(详见《03_namei详解》的 follow_mount)。


第 9 章 挂载哈希表

c 复制代码
mount_hashtable[hash(mnt, dentry)]

static inline unsigned long hash(struct vfsmount *mnt, struct dentry *dentry)
{
    unsigned long tmp = ((unsigned long)mnt / L1_CACHE_BYTES);
    tmp += ((unsigned long)dentry / L1_CACHE_BYTES);
    tmp = tmp + (tmp >> HASH_SHIFT);
    return tmp & (HASH_SIZE - 1);
}

哈希表结构:
mount_hashtable
+------------------+
| bucket[0]        | -> NULL
+------------------+
| bucket[1]        | -> vfsmount1 -> vfsmount2 -> NULL
+------------------+
| bucket[2]        | -> NULL
+------------------+
| ...              |
+------------------+

查找挂载:
struct vfsmount *__lookup_mnt(struct vfsmount *mnt, struct dentry *dentry, int dir)
{
    struct list_head *head = mount_hashtable + hash(mnt, dentry);
    struct vfsmount *p;

    list_for_each_entry(p, head, mnt_hash) {
        if (p->mnt_parent == mnt && p->mnt_mountpoint == dentry)
            return p;
    }
    return NULL;
}

挂载哈希表用于快速查找"挂载在某挂载点上的 vfsmount",查找键为 (父挂载, 挂载点 dentry)。__lookup_mnt() 是路径查找时 follow_mount()/follow_down() 的核心函数。

文字说明(对应上节内容) :

  • 为什么需要哈希表 :路径查找在遍历目录时,每经过一个目录都要回答"这个目录上是否挂了子文件系统"。若靠遍历 mnt_mounts 链表逐一比对,遇到深层目录链会退化成 O(挂载数)。挂载哈希表用 (父 vfsmount, 挂载点 dentry) 作键,一次 hash + 桶内遍历即可命中。
  • hash 函数技巧 :(unsigned long)mnt / L1_CACHE_BYTES 与 (unsigned long)dentry / L1_CACHE_BYTES 先把两个指针按缓存行对齐再相加,让"同缓存行内的地址"映射到相近桶,提升缓存局部性;tmp + (tmp >> HASH_SHIFT) 再做一次雪崩混合,最后 & (HASH_SIZE-1) 取桶下标(要求 HASH_SIZE 是 2 的幂)。
  • 桶内比较 :__lookup_mnt 遍历该桶的 mnt_hash 链表,同时满足 p->mnt_parent == mnt 且 p->mnt_mountpoint == dentry 才命中------即"挂在 mnt 的这个 dentry 上的挂载"。
  • dir 参数 :dir 区分查找方向(1 表示往下找子挂载,用于 follow_down)。

举例 :查找 /mnt/data/file,走到 data 目录时 follow_mount/follow_down 调用 __lookup_mnt(root_mnt, data_dentry, 1),按 hash(root_mnt, data_dentry) 定位桶,比对 mnt_parent==root_mnt && mnt_mountpoint==data_dentry,命中挂在 data 上的 ext4 vfsmount,随即下钻到它的 mnt_root。


第 10 章 同步与引用计数机制

10.1 挂载相关锁

markdown 复制代码
1. namespace_sem (读写信号量)
   - 保护整个挂载命名空间
   - 挂载/卸载操作需要写锁
   - 查找操作需要读锁

2. vfsmount_lock (自旋锁)
   - 保护挂载哈希表
   - 保护 mnt_count, mnt_flags
   - 短时间持有

3. sb_lock (自旋锁)
   - 保护超级块链表
   - 保护 s_count

4. s_umount (读写信号量)
   - 保护超级块卸载
   - 挂载时获取写锁

5. s_lock (互斥锁)
   - 保护超级块内部状态
   - 文件系统特定操作

锁获取顺序 (避免死锁):
1. namespace_sem
2. vfsmount_lock
3. sb_lock
4. s_umount
5. s_lock

文字说明(对应上节内容) :挂载子系统用一组锁保护不同粒度的共享数据,掌握它们的层次关系就能理解大部分挂载相关代码的加锁方式:

  • namespace_sem(读写信号量):最大粒度 ,保护整个挂载命名空间的结构。mount()/umount() 改挂载树需写锁;路径查找、/proc/mounts 遍历只需读锁(可并发)。
  • vfsmount_lock(自旋锁):保护挂载哈希表与 mnt_count/mnt_flags 等短小字段。因其是自旋锁,必须短持、禁抢占内使用。
  • sb_lock(自旋锁):保护 super_blocks 链表与 s_count。
  • s_umount(读写信号量):保护单个超级块的卸载;路径查找对每个遇到的超级块拿读锁,卸载要写锁。
  • s_lock(互斥锁):保护超级块内部状态与文件系统私有操作,粒度最细。

死锁预防 :加锁必须自外向内、自上而下 ,即严格按上述顺序(namespace_sem → vfsmount_lock → sb_lock → s_umount → s_lock)获取,绝不反向,否则两个 CPU 各持一把锁互等另一把即形成死锁。

举例 :mount -t ext4 /dev/sda1 /mnt 全程持有 namespace_sem 写锁(保证挂载树不被并发改动),期间访问哈希表时叠加 vfsmount_lock,创建超级块时获取 sb_lock,初始化 s_root 时拿 s_umount 写锁。层层嵌套但方向一致,不会与并发的路径查找互相卡死。

10.2 写访问管理

sql 复制代码
mnt_want_write() / mnt_drop_write()

写访问获取流程:
+------------------+
| mnt_want_write() |
+--------+---------+
         |
         v
+------------------+
| preempt_disable()|
| 禁用抢占         |
+--------+---------+
         |
         v
+------------------+
| inc_mnt_writers()|
| 增加写者计数     |
+--------+---------+
         |
         v
+------------------+
| smp_mb()         |
| 内存屏障         |
+--------+---------+
         |
         v
+------------------+
| 等待 MNT_WRITE_HOLD
| 清除             |
+--------+---------+
         |
         v
+------------------+
| 检查只读状态     |
| __mnt_is_readonly|
+--------+---------+
         |
    +----+----+
    |         |
    v         v
 只读       可写
    |         |
    v         v
 返回错误   返回成功

写者计数 (每CPU):
#ifdef CONFIG_SMP
    int *mnt_writers;  // per-CPU变量
#else
    int mnt_writers;   // 单变量
#endif

__mnt_is_readonly() 检查两处只读标志:

  • mnt->mnt_flags & MNT_READONLY(挂载点只读)
  • mnt->mnt_sb->s_flags & MS_RDONLY(超级块只读)

10.3 关键调用栈

mount() 调用栈(自底向上逐帧含义) :

c 复制代码
#0  alloc_vfsmnt () at fs/namespace.c:128       分配一个 vfsmount 结构(内存申请+初始化)
#1  vfs_kern_mount () at fs/super.c:924         (核心)调用 get_sb 创建 super_block, 再包成 vfsmount
#2  do_kern_mount () at fs/super.c:999          (核心)按 fstype 名定位 file_system_type, 转发到 vfs_kern_mount
#3  do_new_mount () at fs/namespace.c:1658      (核心)把新 vfsmount 挂到挂载点, 处理 CLONE/绑定
#4  do_mount () at fs/namespace.c:1975          解析 flags/dev_name/data, 分发到 do_new_mount
#5  sys_mount () at fs/namespace.c:2040         系统调用入口, 拷贝用户态参数
#6  system_call_fastpath                        从用户态陷入内核的中断入口
  • 用户 mount -t ext4 /dev/sdb1 /data → sys_mount
  • do_kern_mount 按 "ext4" 在全局 file_systems 链表定位类型 → ext4_get_sb → ext4_fill_super 读取磁盘超级块、初始化 inode 操作等(见《11》)
  • do_new_mount 在 __get_new_mnt 后调用 graft_tree/attach_mnt 把 vfsmount 挂入挂载树与命名空间链表

umount() 调用栈(自底向上逐帧含义) :

c 复制代码
#0  generic_shutdown_super () at fs/super.c:296  通用超级块关闭(丢缓存/同步/回调 put_super)
#1  kill_block_super () at fs/super.c:841        块设备文件系统的 super 销毁入口
#2  deactivate_locked_super () at fs/super.c:215 超级块引用归零后触发的销毁
#3  __mntput () at fs/namespace.c:614            释放 vfsmount 的底层实现(降引用)
#4  mntput_no_expire () at fs/namespace.c:624    检查能否真正释放 mnt(无其它引用)
#5  release_mounts () at fs/namespace.c:1260     递归卸载挂载点下的子挂载
#6  do_umount () at fs/namespace.c:1350          卸载主流程: 校验权限/忙碌, 走释放路径
#7  sys_umount () at fs/namespace.c:1380         系统调用入口
  • release_mounts 会先卸载目标挂载点之下的子挂载,再卸自身,保证树上自底向上清理
  • deactivate_locked_super 之后才真正把 super_block 释放;若仍有进程占用(mnt_count 未归零)则延迟释放

10.4 数据结构关系图

lua 复制代码
+==========================================================+
|                    挂载实例与超级块关系                    |
+==========================================================+

                    struct file_system_type
                    +----------------------+
                    | name: "ext4"         |
                    | get_sb               |
                    | kill_sb              |
                    | fs_supers ----------+--+
                    +----------------------+  |
                                              v
                    struct super_block        |
                    +----------------------+  |
                    | s_type --------------+--+
                    | s_list (全局链表)    |
                    | s_instances ---------+--+
                    | s_dev: 0x801         |
                    | s_root (dentry)      |
                    +----------------------+
                    struct vfsmount
                    +----------------------+
                    | mnt_sb --------------+--+
                    | mnt_root             |<----+
                    | mnt_parent           |     |
                    | mnt_mountpoint       |     |
                    +----------------------+     | dget(s_root)
                                                 |
                    struct dentry                |
                    +----------------------+     |
                    | d_inode              |<----+
                    | d_sb ----------------+--+
                    +----------------------+  |
                    struct inode              |
                    +----------------------+  |
                    | i_sb ----------------+--+
                    | i_ino                |
                    +----------------------+

第 11 章 挂载后的文件读写流程

文件系统挂载完成后,用户进程对文件的读写会经过 VFS 层和具体文件系统层的协同处理。

11.1 读写流程总览

css 复制代码
用户空间: read(fd, buf, count) / write(fd, buf, count)
    │
    ▼
系统调用层: SYSCALL_DEFINE3(read/write, ...) [fs/read_write.c]
    │
    ▼
VFS 层: vfs_read() / vfs_write() [fs/read_write.c]
    │
    ▼
文件系统层: file->f_op->read() / file->f_op->write()
    │       或 file->f_op->aio_read() / aio_write()
    ▼
页缓存层: address_space->a_ops->readpage() / writepage() [mm/filemap.c]
    │
    ▼
块设备层: submit_bio() → 块 I/O 调度 → 驱动程序
    │
    ▼
硬件层: 磁盘控制器 → 磁盘读写

11.2 挂载点穿越 (Mount Traversal)

路径查找遇到挂载点时,需要正确穿越到子挂载:

scss 复制代码
int follow_down(struct path *path)
{
    struct vfsmount *mounted;

    spin_lock(&vfsmount_lock);
    // 在挂载哈希表中查找子挂载
    mounted = __lookup_mnt(path->mnt, path->dentry, 1);
    if (mounted) {
        path->mnt = mounted;      // 切换到子挂载
        path->dentry = dget(mounted->mnt_root);  // 切换到子挂载根
        mntput(mounted->mnt_parent);
    }
    spin_unlock(&vfsmount_lock);
    return mounted ? 1 : 0;
}

示例 :查找 /mnt/data/file.txt

ini 复制代码
初始: root_vfsmount (mnt_root=/)
  → dentry "mnt" (d_mounted=1)   [挂载点]
  → follow_down() 切换到 mnt_vfsmount
  → dentry "data" (d_mounted=1)  [挂载点]
  → follow_down() 切换到 ext4_vfsmount
  → dentry "file.txt" (d_mounted=0)  [普通文件]

最终: path.mnt = ext4_vfsmount, path.dentry = file_dentry

文字说明(对应上节流程图与示例) :follow_down() 是"路径查找跨越挂载点"的原子操作,其本质是一次挂载哈希表查询(见第 9 章):

  1. 以当前 (path->mnt, path->dentry) 为键,__lookup_mnt(..., 1) 查该目录上的子挂载。
  2. 命中则整体切换到子挂载 :path->mnt = mounted(换到子文件系统的 vfsmount)、path->dentry = mounted->mnt_root(换到子文件系统根目录),并 mntput(mnt->mnt_parent) 释放对父挂载的临时引用。
  3. 未命中则原地不动返回 0,路径查找继续在当前文件系统内下钻。

关键点 :穿越只发生在目录 上(d_mounted > 0 的挂载点),对普通文件无效;且穿越是"换 mnt + 换到子根",而不是"继续在父里走"。所以 follow_down 常与 follow_mount(解析到挂载点后判断是否下钻)配合,循环直到不再有挂载可下钻。

示例分解 (/mnt/data/file.txt):

ini 复制代码
路径查找在 root 文件系统内解析:
  "mnt"   目录 → d_mounted=1(是挂载点) → follow_down 切换到 mnt_vfsmount, dentry=其根
  "data"  目录 → d_mounted=1(又叠了挂载) → follow_down 切换到 ext4_vfsmount, dentry=其根
  "file.txt" → d_mounted=0(普通文件) → 停止穿越, 在 ext4 文件系统内解析该 dentry

最终 path.mnt = ext4_vfsmount、path.dentry = file.txt 的 dentry,一次路径查找就正确穿透了两层挂载点到达目标文件。

11.3 挂载标志对读写的影响

yaml 复制代码
MNT_READONLY / MS_RDONLY:
  - mnt_want_write() 返回 -EROFS
  - 所有写操作被拒绝

MNT_NOSUID:
  - 执行文件时忽略 setuid/setgid 位

MNT_NOEXEC:
  - 禁止执行程序, execve() 返回 -EACCES

MNT_NODEV:
  - 禁止访问设备文件

MNT_NOATIME / MNT_RELATIME:
  - 影响访问时间更新策略, 减少元数据写入

文字说明:挂载标志并不改变文件系统本身,而是作为"该挂载点上的一次性开关",在每次读写/执行时被检查:

  • MNT_READONLY/MS_RDONLY:一切写路径的第一道闸门 。任何 write()/truncate()/create() 都会先调 mnt_want_write(),它检查只读后返回 -EROFS(见 10.2)。
  • MNT_NOEXEC:execve() 在该挂载点上被拒绝,返回 -EACCES,是"把数据盘挂成不可执行"的安全手段,/tmp 或某些容器常用。
  • MNT_NODEV:禁止在该挂载点上打开设备文件(mknod 的节点无法使用),防止"通过挂载点制造/访问设备文件"提权。
  • MNT_NOSUID:执行时忽略文件的 setuid/setgid 位,防止从不可信挂载点运行提权二进制。这三者可组合成 noexec,nodev,nosuid 的"标准安全三连"。
  • MNT_NOATIME/MNT_RELATIME:平衡"atime 准确性"与"元数据写放大"。默认 atime 每次读都更新(大量写盘);NOATIME 完全不更新;RELATIME 仅在"mtime/ctime 比 atime 新"或"atime 已超过 24h"时才更新,是多数发行版的默认,显著减少读操作触发的元数据回写。

举例 :mount -o noexec,nodev,nosuid /dev/sda1 /data 后,在 /data 下运行任意脚本会得到 -EACCES(noexec);即使 /data 里有 mknod 出的设备节点也无法打开(nodev);有 suid 位的程序执行时权限也被降级(nosuid)。这样即使挂载内容不可信,也封堵了主要提权路径。

11.4 文件读写中的权限检查链

scss 复制代码
1. mnt_want_write(file->f_path.mnt)  -- 检查挂载点写权限 (-EROFS)
2. security_file_permission(file, MAY_WRITE)  -- LSM 安全检查
3. inode_permission(inode, acc_mode)  -- 检查 inode 权限

文字说明 :写操作(含写文件内容、改权限、删改目录项等)会依次经过三道独立层次的检查,任何一层拒绝都直接返回错误:

  1. 挂载点级 mnt_want_write():只看"能不能对这个挂载点写"。只读挂载、还有 pending 的冻结/卸载都在这层拦截,返回 -EROFS。与 inode 权限无关------根用户也被挡在只读挂载外。
  2. LSM 级 security_file_permission():SELinux/AppArmor 等基于安全标签的扩展检查,比如"容器进程能否写宿主数据卷"。
  3. inode 级 inode_permission():经典的 Unix 权限(属主 ugo + mode 位、ACL、capability 如 CAP_DAC_OVERRIDE)。根用户(CAP_DAC_OVERRIDE)可越过此层,但仍不能越过第 1 层的只读。

举例 :普通用户对 mount -o ro /dev/sda1 /mnt 里的文件执行 echo hi > /mnt/f:mnt_want_write 最先发现 MNT_READONLY 返回 -EROFS,根本到不了 inode 权限检查------这也说明只读挂载对任何用户(包括 root)都是硬性只读。


总结

Linux 文件系统挂载的核心要点:

  1. 分层架构:VFS 层提供统一接口,文件系统类型注册具体实现,挂载实例管理挂载树。
  2. 核心数据结构 :struct vfsmount(挂载实例)、struct super_block(超级块)、struct file_system_type(文件系统类型)、struct mnt_namespace(挂载命名空间)。
  3. 挂载类型:块设备挂载(get_sb_bdev)、无设备挂载(get_sb_nodev)、绑定挂载(bind mount)、挂载传播(shared/slave/private)。
  4. 同步机制:namespace_sem 保护命名空间、vfsmount_lock 保护挂载结构、sb_lock 保护超级块链表。
  5. 引用计数:mnt_count(挂载引用)、s_count(超级块引用)、s_active(超级块活跃引用)。
相关推荐
GeW20 分钟前
新质生产力路线图解读:数据库性能优化与Linux调优专场
linux·数据库
陈希1232 小时前
Linux 服务器性能排查手册(2026):CPU、内存、磁盘IO、网络四大瓶颈定位
linux·服务器·网络
测试界的小码农5 小时前
【Linux 命令实战(03):文件创建、复制、移动与删除,掌握 touch、mkdir、cp、mv、rm】
linux·服务器·php
byte轻骑兵6 小时前
实战 1:修改 BlueZ 源码,添加自定义开机日志
linux·aiot·bluez·电脑蓝牙·嵌入式蓝牙
叼烟扛炮6 小时前
Linux 学习笔记(五十):网络层——IP 协议、子网划分与 NAT 魔法
linux·笔记·学习
叼烟扛炮6 小时前
Linux 学习笔记(六十):单线程的逆袭——epoll 高并发服务器实战
linux·服务器·网络·笔记·学习·算法
AR-26710-6 小时前
Linux Day32——实操五类资源告急排查
linux·运维
StarsGreen7 小时前
libva中的图像格式说明
linux
H.莓飛7 小时前
【C++】【STL】vector_OJ题
linux·服务器·开发语言·数据结构·c++·visual studio