文件系统挂载与卸载详解
目录
- 第 1 章 挂载机制概述
- 第 2 章 挂载核心数据结构
- 第 3 章 挂载标志体系
- 第 4 章 mount() 系统调用完整流程
- 第 5 章 超级块管理与生命周期
- 第 6 章 挂载类型详解
- 第 7 章 卸载流程详解
- 第 8 章 挂载树与命名空间
- 第 9 章 挂载哈希表
- 第 10 章 同步与引用计数机制
- 第 11 章 挂载后的文件读写流程
第 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:本文件系统的根 dentrymnt_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_devices_root= 该盘根目录 inode(inode 号 2)对应的 dentrys_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) 的分发中枢,执行可概括为五步:
- 去魔数 :老式
mount(2)在flags高 16 位携带MS_MGC_VAL(0xC0ED0000)表示"使用新语义"。当(flags & MS_MGC_MSK) == MS_MGC_VAL时将其清除,避免干扰后续标志判断。 - 转换挂载点标志 :把
MS_NOSUID等请求位翻译成mnt_flags里的MNT_*位(见第 3 章)。产物mnt_flags沿调用链一路传到挂载点,最终写入vfsmount->mnt_flags。 - 解析挂载点路径 :
kern_path(dir_name, LOOKUP_FOLLOW, &path)把/mnt解析成(vfsmount, dentry)对。LOOKUP_FOLLOW表示路径末段若是符号链接则跟随解析。 - 安全检查 :
security_sb_mount()让 LSM(SELinux、AppArmor 等)有机会拒绝本次挂载(返回-EACCES等)。 - 按标志分发 :
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()(嫁接),其内部:
attach_recursive_mnt()递归处理传播关系(若新挂载带 shared/slave 语义,需同步复制到对等组),并得到parent_path。mnt_set_mountpoint()建立父子关系------四件事:mnt->mnt_parent = path->mnt(记下父挂载)、mnt->mnt_mountpoint = path->dentry(记下挂在父的哪个目录上)、把mnt链进父的mnt_mounts、path->dentry->d_mounted++(标记该 dentry 现在是挂载点)。d_mounted是路径查找判断"要不要下钻"的依据。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误当活跃实例取走。
完整一生(对应上图从分配、激活、使用到销毁的每个方框):
alloc_super():置s_active=0、s_count=S_BIAS,尚未激活。fill_super()成功、挂入挂载树后:s_flags |= MS_ACTIVE,标记"可被路径查找使用"。- 活跃使用期:每次打开/路径穿越持有
s_active引用。 deactivate_locked_super():s_active归零(不再有 vfsmount 引用)→ 调用fs->kill_sb(s)。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链表摘除。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() 的逆操作,核心是"安全地断开挂载树中的一环并回收资源":
user_path_mountpoint_at()定位要卸载的 vfsmount(AT_FDCWD表示相对当前工作目录)。do_umount()先做三道检查:check_mnt()(必须属于当前命名空间,否则拒绝)、propagate_mount_busy()(该挂载或其子挂载正被使用则-EBUSY,见 7.2)、may_umount()(权限校验)。MNT_FORCE(强制卸载,umount -f)分支 :先把超级块标为只读、调用sb->s_op->umount_begin()(NFS 借此向服务器发起切断),试图让挂载从"忙碌"变为可卸。注意强制卸载对本地文件系统不一定可靠,通常用于 NFS 等网络文件系统。umount_tree()递归收集整棵子树(连同子挂载)到umount_list。detach_mnt():把每个 vfsmount 从挂载树摘除------list_del摘出mnt_child/mnt_hash、dentry->d_mounted--。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 章):
- 以当前
(path->mnt, path->dentry)为键,__lookup_mnt(..., 1)查该目录上的子挂载。 - 命中则整体切换到子挂载 :
path->mnt = mounted(换到子文件系统的 vfsmount)、path->dentry = mounted->mnt_root(换到子文件系统根目录),并mntput(mnt->mnt_parent)释放对父挂载的临时引用。 - 未命中则原地不动返回 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 权限
文字说明 :写操作(含写文件内容、改权限、删改目录项等)会依次经过三道独立层次的检查,任何一层拒绝都直接返回错误:
- 挂载点级
mnt_want_write():只看"能不能对这个挂载点写"。只读挂载、还有 pending 的冻结/卸载都在这层拦截,返回-EROFS。与 inode 权限无关------根用户也被挡在只读挂载外。 - LSM 级
security_file_permission():SELinux/AppArmor 等基于安全标签的扩展检查,比如"容器进程能否写宿主数据卷"。 - 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 文件系统挂载的核心要点:
- 分层架构:VFS 层提供统一接口,文件系统类型注册具体实现,挂载实例管理挂载树。
- 核心数据结构 :
struct vfsmount(挂载实例)、struct super_block(超级块)、struct file_system_type(文件系统类型)、struct mnt_namespace(挂载命名空间)。 - 挂载类型:块设备挂载(get_sb_bdev)、无设备挂载(get_sb_nodev)、绑定挂载(bind mount)、挂载传播(shared/slave/private)。
- 同步机制:namespace_sem 保护命名空间、vfsmount_lock 保护挂载结构、sb_lock 保护超级块链表。
- 引用计数:mnt_count(挂载引用)、s_count(超级块引用)、s_active(超级块活跃引用)。