深入理解Linux文件系统的挂载过程
1. 背景介绍
在Linux的世界里,一切皆文件。无论是硬盘、鼠标还是网络连接,都可以被视为文件来处理。而文件系统则是管理这些文件的大脑,它负责组织和管理所有的文件,确保我们能够高效地找到它们。然而,仅仅有一个文件系统还不够,我们需要一种方法来打开这扇大门,这就是挂载的概念。挂载,简单来说,就是告诉操作系统:“嘿,这里有本书(或是一堆书),请把它放在这个位置,让我能够阅读。”接下来,我们就一起来探索Linux文件系统的挂载过程吧!
2. 挂载流程总览与流程图
2.1 流程图展示
2.2 简要说明
用户在终端执行 mount 命令,请求挂载特定文件系统。该请求通过系统调用 sys_mount 进入内核。内核中处理挂载逻辑,依次查找和验证合适的文件系统类型,初始化 super_block 和根 inode,完成挂载过程。
下面我们就一步步来看看吧
3. mount 命令与用户空间交互
在用户空间,mount 命令用于将设备或文件系统挂载到一个挂载点。它是与内核进行交互的主要途径。挂载是文件系统操作中至关重要的部分,允许用户访问存储设备中的数据。
例如,执行以下命令:
mount /dev/sda1 /mnt
这条命令将设备 /dev/sda1(通常是一个磁盘分区)挂载到 /mnt 目录。挂载后,用户就可以通过访问 /mnt 来访问该设备上的文件系统内容。
mount 工具根据这些参数构建一个挂载请求,包含设备、挂载点、文件系统类型、挂载选项等信息。如果用户没有指定文件系统类型,mount 工具会尝试自动检测文件系统类型。构建好的挂载请求会通过sys_mount系统调用交给内核进行处理。
4. 进入内核:sys_mount 系统调用
4.1 处理挂载参数与路径解析
当用户通过 mount 命令请求挂载一个文件系统时,该请求会被转换为 sys_mount 系统调用。这一节我们将详细介绍 sys_mount 如何处理挂载参数和路径解析的过程。
sys_mount 首先需要将用户空间的参数复制到内核空间。
路径解析是 sys_mount 的另一个重要任务。内核需要将传入的路径解析为内核中的 dentry 和 vfsmount 结构。dentry 表示文件系统中的一个目录项,而 vfsmount 表示一个挂载点。
struct path path;
err = user_path_at(AT_FDCWD, kernel_dir_name, LOOKUP_FOLLOW, &path);
if (err)
goto out_free_data;
struct dentry *mnt_point = path.dentry;
struct vfsmount *mnt = path.mnt;
挂载选项通过 flags 参数传递。sys_mount 需要解析这些选项并将其应用于挂载过程中。常见的挂载选项包括:
MS_RDONLY: 只读挂载。MS_NOSUID: 不允许设置用户ID或组ID。MS_NODEV: 不允许访问设备文件。MS_NOEXEC: 不允许执行文件。
在完成参数解析和路径解析后,sys_mount 将调用挂载核心函数 do_mount 来处理实际的挂载操作。
4.2 调用内核挂载核心函数 do_mount
do_mount 函数用于处理文件系统的挂载过程。下面是它如何一步步调用到 file_system_type 中的 mount 回调函数的过程:
do_mount 的关键步骤之一是根据文件系统类型来查找对应的 file_system_type 结构。这个结构包含了文件系统的相关操作函数,包括 mount 回调函数。
fs_type = get_fs_type(fstype);
这里的 fstype 是用户传入的文件系统类型的字符串。get_fs_type 函数会查找内核中注册的所有文件系统类型,并返回匹配的 file_system_type 结构体。file_system_type 结构体是文件系统类型的描述符,它包含了对该文件系统的操作
一旦通过 fstype 获取到对应的 file_system_type,do_mount 函数会调用该结构体中的 mount 回调函数:
sb = fs_type->mount(fs_type, flags, dev_name, data);
在这里,fs_type->mount 是注册在 file_system_type 结构中的具体挂载操作函数,它会处理文件系统的具体挂载逻辑。每个文件系统类型(如 ext4, xfs 等)都会提供自己的 mount 函数。当 do_mount 调用 fs_type->mount 时,实际执行的是该文件系统的挂载实现。不同文件系统的 mount 函数会有不同的行为,但大体上都会执行以下操作:
创建超级块(superblock):这是一个描述挂载文件系统的结构。挂载根目录:根目录(root directory)会在超级块中初始化,并设置相关参数。初始化其他资源:例如设备、日志等。
挂载完成后,mount 回调函数会返回一个指向超级块(super_block)的指针,表示挂载成功。do_mount 最终会通过返回值传递给用户空间,告诉用户挂载操作是否成功。
4.3 总结
通过上述步骤,我们可以看到 do_mount 函数是如何逐步调用 file_system_type 结构中的 mount 回调函数来完成文件系统的挂载操作的。这些回调函数负责创建超级块并初始化文件系统的具体逻辑,从而确保文件系统能够正确地挂载到指定的挂载点上。下面我们来详细讨论如何使用file_system_type注册文件系统 。
5. file_system_type 与文件系统注册
5.1 file_system_type 结构体的定义与作用
在 Linux 内核中,file_system_type 结构体用于表示不同类型的文件系统。可以将它类比为 C++ 中的类,而具体实现的文件系统(例如 ext2)则是这个类的实例。这意味着每个文件系统都会有一个对应的 file_system_type 实例,该实例包含了文件系统的基本信息和操作方法。
5.1.1 结构体定义
file_system_type 结构体的定义位于 include/linux/fs.h 文件中,其主要字段包括:
struct file_system_type {
const char *name; // 文件系统名称
int (*mount) (struct file_system_type *, int,
const char *, void *); // 挂载函数指针
struct module *owner; // 拥有该文件系统的模块
...
};
name: 文件系统名称,用于标识文件系统类型。mount: 挂载函数指针,指向一个函数,该函数负责执行特定文件系统的初始化挂载逻辑。owner: 指向拥有该文件系统的模块,通常用于模块卸载时的依赖管理。
5.1.2 mount函数指针
mount 函数指针是 file_system_type 结构体中的一个重要成员。它定义了一个函数,该函数负责初始化并挂载文件系统。典型的 mount 函数实现如下:
// 填充超级块信息
static int myfs_fill_super(struct super_block *sb, void *data, int silent) {
sb->s_magic = MYFS_MAGIC; // 设置文件系统的魔数
sb->s_op = &myfs_super_ops; // 设置超级块操作
sb->s_fs_info = NULL; // 没有额外的文件系统信息
// 创建根目录inode,并将其设置为超级块的根目录
struct inode *root_inode = myfs_get_inode(sb, NULL, S_IFDIR | 0755, 0);
if (!root_inode)
return -ENOMEM; // 内存分配失败
sb->s_root = d_make_root(root_inode); // 创建根目录的dentry
if (!sb->s_root) {
iput(root_inode); // 如果创建失败,释放root_inode
return -ENOMEM;
}
return 0;
}
// 挂载文件系统
static struct dentry *myfs_mount(struct file_system_type *fs_type, int flags, const char *dev_name, void *data) {
return mount_nodev(fs_type, flags, data, myfs_fill_super); // 使用无设备挂载
}
函数的调用流程如下:file_system_type -> mount -> mount_nodev -> myfs_fill_super
5.1.2.1 fill_super 回调函数
sb: 指向超级块结构体的指针,超级块包含文件系统的重要元数据信息。data: 指向挂载数据的指针,通常用于传递特定于文件系统的选项。silent: 一个布尔值,指示是否在出错时保持静默。
设置文件系统的魔数:
sb->s_magic = MYFS_MAGIC;
这里设置了文件系统的魔数,用于在后续操作中验证文件系统的类型。
设置超级块操作:
sb->s_op = &myfs_super_ops;
这里设置了超级块的操作集,myfs_super_ops 包含了一系列针对超级块的操作函数。
创建根目录inode:
struct inode *root_inode = myfs_get_inode(sb, NULL, S_IFDIR | 0755, 0);
if (!root_inode)
return -ENOMEM; // 内存分配失败
这里创建了根目录的inode,作为这个文件系统的root节点。
创建根目录的dentry:
sb->s_root = d_make_root(root_inode);
if (!sb->s_root) {
iput(root_inode); // 如果创建失败,释放root_inode
return -ENOMEM;
}
这里创建了根目录的 dentry,并将其设置为超级块的根目录, 以后就可以在这个根目录下面创建子目录/文件了。
5.1.2.2 mount 回调函数
fs_type: 指向 file_system_type 结构体的指针,表示要挂载的文件系统类型。flags: 挂载标志,用于指定挂载选项。dev_name: 设备名,对于无设备文件系统(如内存文件系统),可以为空。data: 指向挂载数据的指针,通常用于传递特定于文件系统的选项。
调用 mount_nodev 函数:return mount_nodev(fs_type, flags, data, myfs_fill_super);
这里调用了 mount_nodev 函数,该函数用于挂载无设备文件系统。myfs_fill_super 是一个回调函数,用于填充超级块信息。
5.2 文件系统的注册流程:register_filesystem
在 Linux 内核中,文件系统需要通过 register_filesystem() 函数注册。只有注册后的文件系统类型才可以被挂载。
int register_filesystem(struct file_system_type *fs);
fs: 指向 file_system_type 结构体的指针,表示要注册的文件系统类型。
5.2.1 注册过程详解
内核维护了一个全局链表,其中包含了所有已注册的文件系统类型。这个链表的头节点通常是一个 struct list_head 类型的变量。register_filesystem 函数会将传入的 file_system_type 结构体添加到这个链表中。这样,当用户尝试挂载某个文件系统时,内核可以通过遍历这个链表来找到相应的文件系统类型。
5.2.2 示例代码
以下是一个简单的示例,展示了如何在一个模块中注册一个文件系统:
#include
#include
#include
static struct file_system_type myfs_type = {
.name = "myfs",
.mount = myfs_mount,
.owner = THIS_MODULE,
};
static int __init myfs_init(void) {
int ret;
ret = register_filesystem(&myfs_type);
if (ret) {
printk(KERN_ERR "Failed to register filesystem: %d\n", ret);
return ret;
}
printk(KERN_INFO "Filesystem 'myfs' registered successfully.\n");
return 0;
}
static void __exit myfs_exit(void) {
unregister_filesystem(&myfs_type);
printk(KERN_INFO "Filesystem 'myfs' unregistered.\n");
}
module_init(myfs_init);
module_exit(myfs_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example of a custom filesystem.");
myfs_type: 定义了一个 file_system_type 结构体,表示自定义的文件系统类型。myfs_init: 模块初始化函数,调用 register_filesystem 注册文件系统。myfs_exit: 模块退出函数,调用 unregister_filesystem 卸载文件系统。
5.3 文件系统的查找与匹配
当用户尝试挂载一个文件系统时,内核会通过查找全局链表中的 file_system_type 结构来匹配适合的文件系统类型。
5.3.1 查找过程详解
用户请求挂载:
用户通过 mount 命令或系统调用请求挂载一个文件系统。例如:
mount -t ext4 /dev/sda1 /mnt
内核解析请求:
内核接收到挂载请求后,会解析命令行参数,提取文件系统类型(如 ext4)、设备名(如 /dev/sda1)和挂载点(如 /mnt)。
查找文件系统类型:
内核会遍历全局链表中的 file_system_type 结构,查找与请求的文件系统类型匹配的项。这个查找过程通常通过字符串比较来完成,比较 file_system_type 结构中的 name 字段。
调用挂载函数:
一旦找到匹配的 file_system_type 结构,内核会调用该结构中的 mount 函数指针,执行具体的挂载操作。例如,对于 ext4 文件系统,内核会调用 ext4_mount 函数。
5.3.2 示例代码
以下是一个简化的示例,展示了内核如何查找并调用文件系统的挂载函数:
#include
#include
#include
// 全局链表头
static LIST_HEAD(file_systems);
// 查找并挂载文件系统
struct dentry *find_and_mount(const char *fstype, const char *dev_name, void *data) {
struct file_system_type *fs_type;
struct list_head *pos;
// 遍历全局链表
list_for_each(pos, &file_systems) {
fs_type = list_entry(pos, struct file_system_type, next);
if (strcmp(fs_type->name, fstype) == 0) {
// 找到匹配的文件系统类型
return fs_type->mount(fs_type, 0, dev_name, data);
}
}
// 没有找到匹配的文件系统类型
return ERR_PTR(-ENODEV);
}
file_systems: 全局链表头,用于存储所有已注册的文件系统类型。find_and_mount: 查找并挂载文件系统的函数。它遍历全局链表,查找与请求的文件系统类型匹配的项,并调用其 mount 函数。
5.3.3 总结
通过全局链表,内核能够高效地管理和查找已注册的文件系统类型。当用户请求挂载一个文件系统时,内核会遍历这个链表,找到匹配的文件系统类型,并调用其挂载函数来完成挂载操作。