一、挂载文件系统基础
1.挂载描述符
Linux系统的一个文件系统,只有挂载到内存中目录树的一个目录下,进程才能够访问这个文件系统。每次挂载文件系统,虚拟文件系统就会创建一个挂载描述符(mount结构体)。挂载描述符用来描述文件系统的一个挂载实例,同一个存储设备上的文件系统可以多次挂载,每次挂载到不同的目录下。
四个重要字段:
1.struct hlist_node mnt_hash:内核维护一张全局的挂载点哈希表。当新的挂载点创建时,会根据关键标识(如设备号、挂载点路径)计算哈希值,然后将mnt_hash作为节点插入对应哈希槽的链表中。当系统需要查找某个挂载点时(例如判断某个路径是否已挂载),会先对目标挂载点信息计算哈希值,定位到哈希表中的对应链表,再遍历链表匹配具体的 mount
结构体。
2.struct mount *mnt_parent:指向当前挂载点的父挂载点。比如你把 U 盘挂载到/mnt/usb
,/mnt
就是父挂载点。
3.struct dentry *mnt_mountpoint:挂载的位置(目录)。比如 U 盘挂载到/mnt/usb
,它就指向/mnt
中代表usb
这个目录项,标明 “我在父挂载点的哪个位置被挂载”。
4.struct vfsmount *mnt: 像 “挂载信息包裹”。vfsmount
封装了挂载点的核心信息,比如属于哪个命名空间、挂载操作函数等。它把挂载相关的细节(如如何访问、权限管理等)打包起来,让上层代码能方便调用挂载点的各种属性和操作。
2.文件系统类型
因每种文件系统的超级块的格式不同,所以每种文件系统需要向虚拟文件系统VFS注册文件系统类型file_system_type,并且实现mount方法用来读取和解析超级块。
管理员权限可以执行命令:cat /proc/filesystems来查看已经注册的文件系统类型:
查询第一列说明文件系统是否需要挂载一个块设备上,nodev表示侯建的文件系统不需要挂接到块设备上(可以理解为虚拟文件系统如proc、sys等)。第二列是内核支持的文件系统。
3.挂载文件系统
虚拟文件系统在内存中把目录组织成一棵树,一个文件系统只有挂载到内存中目录树的一个目录,进程才能访问这个文件系统。
管理员权限可以执行命令: mount -t ext3 /dev/sda1 /a ,把sata硬盘的第一个分区上的ext3文件系统挂载到目录/a下。管理员可以执行命令: umount dir 来卸载目录dir下挂载的设备。
glibc库封装挂载文件系统的函数mount,两个卸载文件系统的函数umount/umount2.
可以理解为把filesystemtype类型文件系统管理的设备source挂载到target这个目录,使用案例如下:
#include <sys/mount.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main() {
const char *source = "/dev/sda1"; // 设备名称
const char *target = "/mnt/mydisk"; // 目录名称
const char *filesystemtype = "ext4"; // 文件系统类型
unsigned long mountflags = MS_RDONLY; // 只读挂载
const void *data = ""; // 挂载选项
// 创建挂载点目录(如果不存在)
if (mkdir(target, 0755) == -1 && errno != EEXIST) {
perror("mkdir");
exit(EXIT_FAILURE);
}
// 挂载文件系统
if (mount(source, target, filesystemtype, mountflags, data) == -1) {
perror("mount");
exit(EXIT_FAILURE);
}
printf("Filesystem mounted successfully.\n");
return 0;
}
当我们将文件系统2挂载到目录/a下,目录a属于文件系统1,挂载描述符的数据结构如下:
为了能够快速找到目录a下挂载的文件系统,把文件系统2的挂载描述符加入到全局散列表mount_hashtable,关键字是{父挂载描述符,挂载点},根据文件系统1的挂载描述符和目录和目录a可以在散列表中找到文件系统2的挂载描述符。
二、系统调用mount
系统调用mount和上面glibc提供的接口mount函数虽然名称一样,但是前者工作在内核态而后者在用户态。
具体讲解如下:
一、系统调用
mount
入口处理(用户态数据拷贝)
用户态数据获取
系统调用SYSCALL_DEFINE5(mount...
是入口,接收用户态传入的参数:
dev_name
(块设备名)、dir_name
(挂载点路径)、type
(文件系统类型)、flags
(挂载标志)、data
(选项信息)。- 通过
copy_mount_string
和copy_mount_options
函数,将用户态字符串(如文件系统类型、设备名)和选项信息拷贝到内核态,生成内核态变量kernel_type
、kernel_dev
、options
。- 检查拷贝是否成功(通过
PTR_ERR
判断返回值是否为错误指针),若失败则跳转错误处理(如goto out_type
等)。进入核心挂载逻辑
调用do_mount(kernel_dev, dir_name, kernel_type, flags, options)
,将处理后的内核态数据传入,正式执行挂载操作。二、
do_mount
核心挂载流程1. 定位挂载目标(
user_path
)
- 调用
user_path
,根据用户传入的挂载点路径(dir_name
),查找对应的目录项(dentry
)和挂载描述符(vfsmount
)。这一步相当于在内核文件系统中定位 “在哪里挂载”,建立挂载点的路径映射。2. 获取文件系统类型(
get_fs_type
)
- 通过
get_fs_type
,根据内核态的文件系统类型名称(kernel_type
),查找对应的file_system_type
实例。这是确认 “用什么文件系统规则挂载”,比如 ext4、NTFS 等不同文件系统的挂载逻辑由各自的file_system_type
定义。3. 分配挂载描述符(
alloc_vfsmnt
)
- 调用
alloc_vfsmnt
分配新的vfsmount
结构体,用于封装挂载点的核心信息(如命名空间、操作函数等),为后续挂载操作准备数据结构。4. 执行文件系统挂载方法
- 调用文件系统类型对应的挂载方法(如通过
mount_fs
等逻辑),读取并解析设备的超级块(Super Block)。超级块包含文件系统的元数据(如块大小、inode 数量等),这一步是挂载的核心,验证文件系统的有效性并加载基础信息。5. 维护挂载关系链表
- 加入超级块挂载链表:将新分配的挂载描述符(
vfsmount
)添加到对应超级块的挂载实例链表中,便于内核跟踪同一文件系统的所有挂载点。- 加入哈希表:通过
mnt_hash
成员(如前文所述的哈希节点),将挂载点加入全局挂载点哈希表,实现高效查找。- 处理父子关系:将挂载描述符加入父挂载点的子节点链表(通过
mnt_parent
等成员维护层级关系),完善挂载点的树形结构,确保文件系统层级清晰。
三、总体流程总结升华 (重要)
挂载点是文件系统接入系统目录的“接入点”,即用户通过目录路径就可以访问文件系统。一个文件系统可以被多次挂载。即使单文件系统挂载点少,但是整个文件系统存在海量文件,因此挂载点总数庞大,必须通过哈希优化全局管理效率。
散列表是全局的,用于管理整个系统所有文件系统的挂载点。无论文件系统类型、次数,所有mount结构体的mnt_hash都会加入到这张全局散列表。
当我们通过一个路径访问一个文件系统时,从这个路径的文件,比如/home/a是被哪个文件系统挂载到最终通过这个文件系统绑定的具体硬件设备(如磁盘/dev/sda1)去读取具体的文件信息的过程中。如果没有这个哈希表,虽然每个文件系统也许只有一个挂载点,但是想从无数挂载点中找到具体需要的这一个还是非常困难得。