Linux内核学习轨迹第六部:文件系统的注册、挂载与卸载全流程(第三节)
·
3. 文件系统的注册、挂载与卸载全流程
挂载(mount)是Linux文件系统的核心操作,它把一个文件系统实例接入到VFS的目录树中,让用户可以访问该文件系统的内容。我们日常使用的mount命令、容器的挂载命名空间、绑定挂载、overlay联合挂载,底层都依赖VFS的挂载机制。
本章节基于Linux 6.6内核源码,完整拆解文件系统的注册、挂载、卸载的全链路内核实现,以及不同挂载类型的底层原理。
3.1 文件系统的注册:struct file_system_type
Linux内核中,每个文件系统类型(比如ext4、xfs、proc、tmpfs)都对应一个struct file_system_type实例,描述了该文件系统的类型名称、挂载入口、属性等信息。文件系统模块加载时,会把自己的file_system_type实例注册到内核的全局文件系统链表中,内核才能识别并挂载该类型的文件系统。
3.1.1 核心结构体拆解
struct file_system_type定义在include/linux/fs.h中,核心字段:
struct file_system_type {
// 文件系统类型的名称,比如"ext4"、"xfs"、"proc"
const char *name;
// 文件系统的标志位,比如FS_REQUIRES_DEV(需要块设备)、FS_NO_DCACHE(不缓存dentry)
int fs_flags;
// 挂载的核心入口函数,mount系统调用最终会调用这个函数
struct dentry *(*mount)(struct file_system_type *fs_type, int flags, const char *dev_name, void *data);
// 杀死超级块,umount时调用
void (*kill_super)(struct super_block *sb);
// 该文件系统类型的模块指针,用于模块引用计数
struct module *owner;
// 全局文件系统链表的节点,所有注册的文件系统都链接到file_systems链表
struct hlist_node fs_supers;
// 该文件系统类型的所有超级块实例链表
struct hlist_head fs_supers_list;
};
核心字段解析:
- name:文件系统类型的名称,mount -t ext4中的ext4就是这个名称,内核通过这个名称找到对应的file_system_type实例;
- mount:挂载的核心入口函数,mount系统调用时,内核会调用这个函数,完成文件系统的超级块读取、初始化、根目录dentry创建,返回文件系统的根dentry;
- kill_super:umount时调用的函数,用于释放超级块实例,清理文件系统的资源;
- fs_flags:文件系统的标志位,核心标志:
- FS_REQUIRES_DEV:该文件系统需要块设备,比如ext4/xfs等磁盘文件系统;
- FS_NOMOUNT:该文件系统不允许用户态挂载,比如rootfs;
- FS_USERNS_MOUNT:允许在用户命名空间中挂载,比如proc/sysfs/tmpfs;
- FS_DISKLESS:无磁盘的内存文件系统,比如proc/tmpfs/sysfs。
3.1.2 文件系统的注册与注销
1.注册流程:
文件系统模块加载时,调用register_filesystem()函数,把自己的file_system_type实例注册到内核的全局file_systems哈希表中,加入全局文件系统链表。
以ext4为例,注册代码简化如下:
static struct file_system_type ext4_fs_type = {
.name = "ext4",
.mount = ext4_mount,
.kill_super = kill_block_super,
.fs_flags = FS_REQUIRES_DEV,
.owner = THIS_MODULE,
};
static int __init init_ext4_fs(void)
{
// 注册ext4文件系统类型
return register_filesystem(&ext4_fs_type);
}
static void __exit exit_ext4_fs(void)
{
// 注销ext4文件系统类型
unregister_filesystem(&ext4_fs_type);
}
module_init(init_ext4_fs)
module_exit(exit_ext4_fs)
2.注销流程:
文件系统模块卸载时,调用unregister_filesystem()函数,把file_system_type实例从全局哈希表和链表中移除,内核不再支持该类型的文件系统挂载。
3.查看已注册的文件系统:
用户态可以通过cat /proc/filesystems查看内核中已注册的文件系统类型,第一列是标志位(nodev表示不需要块设备),第二列是文件系统名称。
3.2 挂载的核心数据结构
Linux 6.6内核的挂载机制,基于两个核心结构体:struct vfsmount和struct mount,描述了一个挂载实例的所有信息。
3.2.1 挂载实例:struct mount
struct mount是挂载的核心结构体,描述了一个挂载实例的所有属性,包括挂载源、挂载点、挂载参数、子挂载、挂载命名空间等,定义在fs/mount.h中,核心字段:
struct mount {
// 该挂载实例的vfsmount,供VFS层使用
struct vfsmount mnt;
// 挂载实例的哈希表节点,用于快速查找
struct hlist_node mnt_hash;
// 父挂载实例的链表节点,链接到父挂载的mnt_mounts链表
struct list_head mnt_child;
// 该挂载实例的所有子挂载的链表头
struct list_head mnt_mounts;
// 挂载点的dentry,也就是挂载到哪个目录上
struct dentry *mnt_mountpoint;
// 该挂载实例的根dentry,也就是被挂载文件系统的根目录
struct dentry *mnt_root;
// 父挂载实例
struct mount *mnt_parent;
// 挂载的标志位,比如MS_RDONLY/MS_NOSUID/MS_NODEV/MS_NOEXEC
int mnt_flags;
// 挂载的命名空间,每个挂载命名空间有独立的挂载树
struct mnt_namespace *mnt_ns;
// 挂载实例的引用计数
refcount_t mnt_count;
// 挂载源的设备名称,比如/dev/sda1
char *mnt_devname;
// 挂载的文件系统类型
struct file_system_type *mnt_fs_type;
// 挂载的超级块
struct super_block *mnt_sb;
} __randomize_layout;
3.2.2 VFS挂载描述符:struct vfsmount
struct vfsmount是VFS层暴露的挂载描述符,嵌入在struct mount中,供VFS层的路径解析、权限检查使用,定义在include/linux/mount.h中,核心字段:
struct vfsmount {
// 该挂载实例的根dentry
struct dentry *mnt_root;
// 该挂载实例的超级块
struct super_block *mnt_sb;
// 挂载的标志位
int mnt_flags;
} __randomize_layout;
3.2.3 挂载命名空间:struct mnt_namespace
挂载命名空间是Linux Namespace的一种,实现了进程间的挂载树隔离,每个挂载命名空间有独立的挂载树,进程只能看到自己所在命名空间的挂载实例,是容器技术的核心底层基础。
struct mnt_namespace定义在fs/mount.h中,核心字段:
struct mnt_namespace {
// 命名空间的引用计数
atomic_t count;
// 命名空间的根挂载实例
struct mount *root;
// 该命名空间的所有挂载实例的链表
struct list_head list;
// 命名空间的用户命名空间
struct user_namespace *user_ns;
// 命名空间的序列号
u64 seq;
};
- 核心特性:fork()创建子进程时,可以通过CLONE_NEWNS标志创建新的挂载命名空间,子进程有独立的挂载树,后续的mount/umount操作不会影响父进程和其他命名空间,这就是容器中挂载的目录不会影响宿主机的底层原理。
3.3 挂载的全链路内核实现
用户态调用mount()系统调用,最终会落到内核的sys_mount()函数,完整执行流程如下(基于Linux 6.6内核,源码在fs/namespace.c):
用户态 mount() → sys_mount() → ksys_mount()
↓
1. 参数合法性检查
├→ 检查挂载源、挂载点、文件系统类型、挂载标志、挂载参数的合法性
├→ 把用户态的挂载参数复制到内核态
└→ 检查进程是否有挂载的权限
↓
2. 路径解析:获取挂载点的path实例
├→ 解析挂载点的路径名,找到对应的dentry和vfsmount
├→ 检查挂载点是否是目录,是否有访问权限
└→ 检查挂载点是否已经被挂载,处理重复挂载
↓
3. 查找文件系统类型
├→ 根据用户传入的文件系统类型名称,在全局file_systems哈希表中找到对应的file_system_type实例
├→ 如果没有找到,尝试加载对应的文件系统内核模块
└→ 如果模块加载失败,返回错误
↓
4. 调用文件系统的mount函数,创建超级块和根dentry
├→ 调用file_system_type->mount函数,这是具体文件系统的挂载入口
├→ 对于磁盘文件系统(ext4):打开块设备,读取磁盘超级块,创建super_block实例,初始化超级块,读取根目录inode,创建根dentry
├→ 对于内存文件系统(proc):创建super_block实例,初始化内存文件系统的根目录,创建根dentry
└→ 返回被挂载文件系统的根dentry
↓
5. 创建挂载实例struct mount
├→ 分配mount实例,初始化mnt_mountpoint(挂载点dentry)、mnt_root(文件系统根dentry)、mnt_parent(父挂载实例)
├→ 初始化挂载标志位、文件系统类型、超级块、设备名称
├→ 把挂载实例加入到父挂载的mnt_mounts子挂载链表
├→ 把挂载实例加入到当前进程的挂载命名空间的挂载链表
└→ 把挂载实例加入到全局挂载哈希表
↓
6. 路径解析缓存更新
├→ 标记挂载点的dentry为DCACHE_MOUNTED,路径解析时会自动跳转到挂载实例的根dentry
└→ 更新dcache的哈希表
↓
7. 挂载完成,返回0
3.3.1 核心挂载类型的底层原理
1.普通挂载:最基础的挂载,把一个块设备的文件系统挂载到一个目录上,比如mount /dev/sda1 /data,底层流程就是上面的标准流程,需要块设备,读取磁盘超级块,创建super_block实例。
2.绑定挂载(Bind Mount):把一个文件/目录挂载到另一个文件/目录上,实现目录/文件的镜像访问,比如mount --bind /a /b,访问/b等价于访问/a。
- 底层原理:绑定挂载不会创建新的super_block实例,而是复用原文件/目录的super_block和inode,创建一个新的mount实例,挂载点的dentry指向原文件/目录的dentry,路径解析时直接跳转到原文件/目录,不需要新的文件系统,也不需要块设备。
- 核心特性:绑定挂载可以实现目录的跨文件系统访问,也可以实现单个文件的挂载,是容器中目录挂载的核心方式。
3.只读挂载:mount -o ro /dev/sda1 /data,挂载时设置MS_RDONLY标志位,挂载后文件系统只读,无法写入。
- 底层原理:挂载时把MS_RDONLY标志位设置到super_block的s_flags和mount实例的mnt_flags中,VFS层在处理写入操作时,会检查这个标志位,拒绝写入请求,同时把文件系统的所有inode标记为只读。
4.私有挂载/共享挂载/从挂载/不可绑定挂载:Linux的挂载传播类型,控制挂载事件在不同挂载实例、不同命名空间之间的传播,是容器挂载隔离的核心。
- 核心类型:
- MS_SHARED:共享挂载,挂载事件会在共享的peer组之间传播;
- MS_PRIVATE:私有挂载,挂载事件不会传播,默认类型;
- MS_SLAVE:从挂载,只能接收主挂载的传播事件,自己的事件不会传播出去;
- MS_UNBINDABLE:不可绑定挂载,无法被绑定挂载。
- 底层原理:内核通过peer组管理共享挂载,挂载事件发生时,会遍历peer组中的所有挂载实例,同步挂载事件,实现传播。
4.overlay联合挂载:overlayfs是一种联合文件系统,把多个目录(upper层、lower层)联合挂载到一个目录上,实现分层存储,是Docker/Containerd容器镜像的核心底层技术。
- 底层原理:overlayfs实现了自己的file_system_type和对应的mount函数,创建super_block实例,把upper层和lower层的目录联合起来,读操作优先从upper层读取,upper层没有的从lower层读取;写操作会触发COW(写时复制),把文件从lower层复制到upper层,然后修改upper层的文件,lower层的文件永远不会被修改,实现了镜像的分层复用。
3.4 卸载的全链路内核实现
用户态调用umount()系统调用,最终落到内核的sys_umount()函数,完整执行流程如下:
用户态 umount() → sys_umount() → ksys_umount()
↓
1. 参数合法性检查
├→ 检查卸载路径的合法性,检查卸载标志位
├→ 检查进程是否有卸载的权限
└→ 检查是否是busy状态(有进程正在使用该挂载实例)
↓
2. 路径解析:找到对应的挂载实例
├→ 解析卸载路径,找到对应的dentry和vfsmount
├→ 从全局挂载哈希表中找到对应的mount实例
└→ 检查该挂载实例是否可以被卸载
↓
3. 检查挂载实例的busy状态
├→ 检查挂载实例的引用计数,如果大于0,说明有进程正在使用,返回EBUSY错误
├→ 检查是否有子挂载实例,如果有,需要先卸载子挂载,否则返回EBUSY错误
└→ 如果设置了MNT_FORCE标志,强制卸载,忽略busy状态
↓
4. 清理挂载实例
├→ 把挂载实例从父挂载的子挂载链表中移除
├→ 把挂载实例从当前命名空间的挂载链表中移除
├→ 把挂载实例从全局挂载哈希表中移除
├→ 清除挂载点dentry的DCACHE_MOUNTED标志
└→ 路径解析缓存更新
↓
5. 释放超级块
├→ 调用super_block的s_op->put_super函数,清理超级块的资源
├→ 调用file_system_type->kill_super函数,销毁超级块实例
├→ 等待该文件系统的所有inode释放,回收缓存的inode和dentry
└→ 关闭块设备(如果是磁盘文件系统)
↓
6. 释放mount实例,卸载完成,返回0
核心工程细节:
- umount返回EBUSY的常见原因:有进程的当前工作目录在该挂载点下,或者有进程打开了该挂载点下的文件,或者有子挂载实例没有卸载;
- umount -l(懒卸载):设置MNT_DETACH标志,把挂载实例从挂载树中立即移除,但是挂载实例的资源不会立即释放,等到所有进程都不再使用该挂载实例时,再释放资源,不会返回EBUSY错误;
- umount -f(强制卸载):设置MNT_FORCE标志,强制卸载,忽略busy状态,仅对NFS等网络文件系统有效,本地磁盘文件系统不支持强制卸载。
3.5 工程实践与避坑指南
1.挂载的权限安全最佳实践
生产环境中,挂载时需要设置安全的挂载标志,防范安全风险,核心最佳实践:
- 非系统分区,设置nosuid标志,禁止suid/sgid权限位生效,防范提权攻击;
- 非执行文件分区,设置noexec标志,禁止分区内的文件执行,防范恶意代码执行;
- 设备文件分区,设置nodev标志,禁止识别设备文件,防范通过设备文件访问硬件的攻击;
- 不需要写入的分区,设置ro只读标志,最小化写入权限,防范数据篡改;
- 示例:mount -o nosuid,noexec,nodev,ro /dev/sdb1 /data。
2.容器挂载的核心注意事项
容器的挂载是基于绑定挂载和挂载命名空间实现的,核心避坑指南:
- 绝对不要把宿主机的/、/dev、/proc、/sys等系统目录挂载到容器中,会导致容器可以控制宿主机,有严重的安全风险;
- 容器内的挂载默认是私有挂载,不会传播到宿主机,除非设置了共享挂载;
- 绑定挂载宿主机的目录到容器时,要严格控制权限,设置ro只读标志,除非必须写入;
- 容器退出时,要清理容器内的挂载实例,避免挂载泄漏,导致宿主机的目录无法卸载。
3.挂载失败的排查流程
当mount命令执行失败时,按照以下流程排查:
- 查看dmesg内核日志,找到mount失败的详细错误信息,比如超级块损坏、文件系统不支持、块设备不存在;
- 检查文件系统类型是否正确,内核是否加载了对应的文件系统模块,cat /proc/filesystems查看已注册的文件系统;
- 检查块设备是否存在,是否有读写权限,lsblk查看块设备列表,blkid查看块设备的文件系统类型;
- 检查挂载点是否存在,是否是目录,是否有访问权限;
- 检查文件系统是否损坏,用fsck检查修复文件系统;
- 检查是否有重复挂载,mount | grep <挂载点>查看是否已经挂载;
- 检查挂载命名空间,是否在正确的命名空间中执行mount命令。
4.umount失败(EBUSY)的排查与解决
umount返回EBUSY是最常见的卸载失败问题,排查流程:
- 用lsof <挂载点>查看哪些进程打开了该挂载点下的文件,关闭对应的进程,再执行umount;
- 用fuser -mv <挂载点>查看哪些进程的当前工作目录在该挂载点下,把进程的工作目录切换到其他目录,再执行umount;
- 用mount | grep <挂载点>查看是否有子挂载实例,先卸载子挂载,再卸载父挂载;
- 如果是NFS挂载,检查NFS服务器是否正常,是否有锁占用;
- 如果无法关闭进程,用umount -l <挂载点>执行懒卸载,先从挂载树中移除,等进程退出后自动释放资源。
5.proc/sysfs等伪文件系统的挂载注意事项
- proc文件系统必须挂载到/proc目录,mount -t proc proc /proc,很多系统命令(ps、top)依赖proc文件系统;
- sysfs文件系统必须挂载到/sys目录,mount -t sysfs sysfs /sys,设备管理、udev、cgroup都依赖sysfs;
- 伪文件系统是内存文件系统,不需要块设备,卸载后数据会丢失,重新挂载后内核会重新生成;
- 不要修改proc/sysfs下的文件,除非你明确知道修改的后果,可能会导致系统崩溃、内核panic。
更多推荐

所有评论(0)