本篇文章是从零实现文件系统的第二篇文章,主要内容为利用 VFS 提供的 API 实现一个最简单的文件系统并可以正确挂载 / 卸载,其中会开始涉及一些 linux 文件系统的相关知识和实现原理。

⚙️ 以下是本篇文章所使用的环境:

  • Ubuntu Server 24.04
  • Linux kernel 6.8.0-51-generic
  • VSCode + GCC

⚓️对应代码版本: V0.1

本篇文章需要对 Linux 文件系统模型和 VFS 的 4 大基本对象有所了解,如果还不了解上述内容的话请参考之前的这篇博客:Linux 文件系统模型与 VFS 入门

# 简单文件系统

当向 VFS 注册一个文件系统时,至少需要构建 superblock ,并在 superblock 中初始化根目录的 inodedentry ,同时提供挂载(mount)和卸载(unmount)方法。在最简化的实现中,我们仅创建了 superblock 和根 inode ,未实现文件和目录的创建逻辑,因此此时文件系统仅包含根目录,无法添加新的文件或目录,也无法执行文件操作。这样的简化还有一个好处,我们可以暂时不涉及具体的存储介质,而是优先实现 VFS 相关逻辑,确保文件系统可以正确挂载,并逐步完善其功能。这些基本功能可以拆解为 5 个主要任务:

  1. 创建 inode
  2. 填充 superblock
  3. 实现 mount 方法
  4. 实现 unmount 方法
  5. 注册文件系统

# 创建 inode

在创建 superblock 时需要同时创建根目录(填充 s_root根dentry ),而目录上是由 inode 结构来表示的,所以我们需要先实现一个 inode 的创建函数:

/**
 * @sb: 创建的 inode 所属的 superblock,所有 inodes 都属于某个 superblock。
 * @dir: 如果新建的是目录或文件,则 dir 是其父目录的 inode,用于继承权限等属性。
 * @mode: 用于指示 inode 的类型和权限,例如是否是目录 (S_IFDIR) 还是普通文件 (S_IFREG)。
 * @dev: 用于设备文件(如字符设备或块设备),现在的实现中并没有用到它。
 * @return: 返回初始化好的 inode
 * 
 * 这个函数创建并初始化 inode,但目前他只支持创建目录类型的 inode。
 */
struct inode *simplefs_get_inode(struct super_block *sb,
				 const struct inode *dir, umode_t mode,
				 dev_t dev)
{
	struct inode *inode = new_inode(sb);
	if (inode) {
		inode->i_ino = get_next_ino();
		inode_init_owner(&nop_mnt_idmap, inode, dir, mode);
		inode->__i_atime = inode->__i_mtime = inode->__i_ctime = inode_set_ctime_current(inode);
		switch (mode & S_IFMT) {
		case S_IFDIR:  /* 目录 */
			/* i_nlink will be initialized to 1 in the inode_init_always function
			 * (that gets called inside the new_inode function),
			 * We change it to 2 for directories, for covering the "." entry */
			inc_nlink(inode);
			break;
		case S_IFREG: /* 普通文件 */
		case S_IFLNK: /* 符号链接 */
		default:
			printk(KERN_ERR
			       "simplefs can create meaningful inode for only root directory at the moment\n");
			return NULL;
			break;
		}
	}
	return inode;
}
  • new_inode(sb) 是 VFS 层的 API,负责在 super_block 中申请一个新的 inode 结构,并初始化它的基础字段(如 i_nlink = 1 )。
  • get_next_ino() 是一个 VFS 提供的辅助函数,它会返回一个唯一的 inode 号。
  • inode_init_owner() 主要用于设置 inode 的所有者和权限:
    • &nop_mnt_idmap 是一个 挂载 ID 映射,用于处理用户和组 ID 的转换, nop_mnt_idmap 代表不进行任何映射的 ID 映射,即该 inode 直接继承父目录的 uid/gid。
    • inode 是新创建的 inode。
    • dir 是父目录的 inode。
    • mode 指定文件的类型(如目录、普通文件)。
  • mode & S_IFMT 用于提取 inode 的文件类型,它可以是:
    • S_IFDIR (目录)
    • S_IFREG (普通文件)
    • S_IFLNK (符号链接)
  • inc_nlink(inode) :当使用 new_inode() 创建 inode 时, i_nlink 默认是 1 ,这里手动调用 inc_nlink() 使其变为 2 。这样做的原因是目录的 . (自身引用)会增加 i_nlink 计数,由于 . 指向自身,因此目录的链接数最少为 2
  • 对于普通文件和符号链接直接返回 NULL ,并在内核日志中打印一条错误信息,表示 simplefs 目前不支持普通文件或符号链接。

# 填充 superblock

在 Linux VFS 框架中,每个挂载的文件系统都需要一个 super_block 结构来存储全局的文件系统信息。所以我们需要实现一个「初始化 super_block ,并创建文件系统的根目录」的函数。在写完创建 inode 的方法后,就可以在构建 superblock 时调用该方法来完成根目录的创建。该函数需要在文件系统被 mount 时被调用,我们只需要实现一个函数原型为 int (*fill_super)(struct super_block *, void *, int) 的方法,就可以方便的使用内核写好的函数来调用该函数(下一个部分中会用到)。具体的实现代码如下:

/**
 * @sb:  指向 super_block,VFS 在挂载时会传递进来,文件系统需要初始化它。
 * @data: 挂载选项,一些文件系统可以通过这个参数接收挂载参数。
 * @silent: 指示是否 静默挂载,如果为 1,则在错误时不打印日志。
 * 
 * 这个函数的作用是初始化 super_block,并创建文件系统的根目录。
 */
int simplefs_fill_super(struct super_block *sb, void *data, int silent)
{
	struct inode *inode;
	/* A magic number that uniquely identifies our filesystem type */
	sb->s_magic = 0x20250130;
	inode = simplefs_get_inode(sb, NULL, S_IFDIR, 0);
	sb->s_root = d_make_root(inode);
	if (!sb->s_root) /* 若返回 NULL,说明分配失败,则返回 -ENOMEM: */
		return -ENOMEM;
	return 0;
}
  • s_magic 是一个 文件系统唯一标识符(Magic Number),用于识别文件系统类型。 0x20250130 只是 simplefs 自定义的一个 Magic Number,通常不同文件系统都有自己的 Magic Number
  • simplefs_get_inode 创建一个新的 inode,表示根目录,其参数含义如下:
    • sb 是超级块,表示这个 inode 属于当前文件系统。
    • NULL 表示没有父目录(根目录没有上级目录)。
    • S_IFDIR 表示目录类型
    • 0 代表设备号(对于普通文件系统可以忽略)。
  • d_make_root(inode) 创建根目录的 dentry,并将 inode 关联到 dentry ,最终赋值给 sb->s_root 。该函数会调用 d_alloc_anon() 创建一个匿名 dentry,并与 inode 绑定。这意味着根目录 dentry 不需要通过路径查找,而是直接创建并绑定

# 实现 mount 方法

文件系统的 mount 方法会在文件系统 mount 时自动被 VFS 调用,所以需要向 VFS 注册,VFS 定义的接口原型为 struct dentry *(*mount) (struct file_system_type *, int, const char *, void *); ,我们只需要调用内核提供的一个通用函数 mount_bdev 并将我们实现的 simplefs_fill_super 函数提供给他即可,代码如下:

/**
 * @fs_type: 指向 struct file_system_type,代表 simplefs 这个文件系统。
 * @flags: 挂载标志,例如 MS_RDONLY(只读挂载)。
 * @dev_name: 要挂载的 设备名称(如 /dev/sdb1)。
 * @data: 额外的挂载参数(此处未使用)。
 * @return: 若成功,返回指向根目录的 dentry。
 * 
 * 这个函数在文件系统挂载时被 VFS 调用,返回指向文件系统的根 dentry 的指针。
 */
static struct dentry *simplefs_mount(struct file_system_type *fs_type, int flags, const char *dev_name, void *data)
{
	struct dentry *ret;
	ret = mount_bdev(fs_type, flags, dev_name, data, simplefs_fill_super);
	if (unlikely(IS_ERR(ret))) /* IS_ERR () 检查 ret 是否是一个错误指针 */
		printk(KERN_ERR "Error mounting simplefs");
	else
		printk(KERN_INFO "simplefs is successfully mounted on [%s]\n",
		       dev_name);
	return ret;
}

mount_bdev() 的函数签名如下:

struct dentry *mount_bdev(struct file_system_type *fs_type, int flags,
			  const char *dev_name, void *data,
			  int (*fill_super)(struct super_block *, void *, int));

该函数的前 4 个参数的含义与 simplefs_mount 对应参数含义一致,第五个参数 fill_super 是一个回调函数,用于填充 super_block 结构,我们使用了自己定义的 simplefs_fill_super 方法。

# 实现 umount 方法

由于文件系统现在还非常简单,甚至简单到无法正常使用,所以我们卸载文件系统系统时也非常的简单,只是输出了一条信息。

static void simplefs_kill_superblock(struct super_block *s)
{
	printk(KERN_INFO
	       "simplefs superblock is destroyed. Unmount successful.\n");
	/* 目前这只是一个示例函数。随着我们的文件系统逐渐成熟,我们将在这里进行更有意义的操作 */
	return;
}

# 注册文件系统

注册文件系统其实就是创建并填充一个 struct file_system_type 结构并在 init 时调用 register_filesystem ,这样 mount -t simplefs 命令才能能够识别它。同理,注销文件系统就是调用 unregister_filesystem 即可,比较简单,这里直接给出代码:

struct file_system_type simplefs_fs_type = {
	.owner = THIS_MODULE,
	.name = "simplefs",
	.mount = simplefs_mount,
	.kill_sb = simplefs_kill_superblock,
};
static int simplefs_init(void)
{
	int ret;
	ret = register_filesystem(&simplefs_fs_type);
	if (likely(ret == 0))
		printk(KERN_INFO "Successfully registered simplefs\n");
	else
		printk(KERN_ERR "Failed to register simplefs. Error:[%d]", ret);
	return ret;
}
static void simplefs_exit(void)
{
	int ret;
	ret = unregister_filesystem(&simplefs_fs_type);
	if (likely(ret == 0))
		printk(KERN_INFO "Successfully unregistered simplefs\n");
	else
		printk(KERN_ERR "Failed to unregister simplefs. Error:[%d]",
		       ret);
}
module_init(simplefs_init);
module_exit(simplefs_exit);
  • simplefs_fs_type 是一个 文件系统类型 ( file_system_type ) 结构体,用于向 Linux 内核注册 simplefs 文件系统。
    • .owner = THIS_MODULE 用于指定拥有该文件系统的内核模块。
    • .name = "simplefs" 定义文件系统的名称,用于 mount 命令。
    • .mount.kill_sb 用于指定挂载( mount )函数和卸载( umount )函数

# simplefs 的生命周期

simplefs 的代码执行示意图如下:

User: mount -t simplefs /dev/sdb1 /mnt/simplefs
simplefs_mount()
mount_bdev() ───► 打开 /dev/sdb1
simplefs_fill_super()
       ├──► 设置 Magic Number
       ├──► simplefs_get_inode() ───► 创建 inode(根目录)
       ├──► d_make_root() ───► 创建 dentry(根目录)
返回 dentry(根目录)
挂载成功(VFS 记录 sb->s_root)

进一步的来说,其生命周期如下:

1 `insmod simplefs.ko``simplefs_init()`
     ├── `register_filesystem(&simplefs_fs_type)`
2 用户挂载 `mount -t simplefs /dev/sdb1 /mnt/simplefs`
     ├── `simplefs_mount()`
     ├── `mount_bdev()``simplefs_fill_super()`
     ├── `simplefs_get_inode()``d_make_root()`
     ├── `dentry` 被创建,挂载成功
3 用户访问 `/mnt/simplefs`
     ├── 读/写操作由 VFS 调度
4 `umount /mnt/simplefs`
     ├── `simplefs_kill_superblock()`
     ├── `kill_block_super()`
5 `rmmod simplefs`
     ├── `unregister_filesystem(&simplefs_fs_type)`
     ├── `simplefs_exit()`
     └── 模块成功卸载

# 测试

由于我们现在还不涉及到实际硬盘中的数据存储,所以我们可以随便创建一个虚拟的块设备,然后强行指定使用 simplefs 格式即可。

# 创建一个 512MB 的文件作为虚拟磁盘
dd if=/dev/zero of=simplefs.img bs=1M count=512
# 将 simplefs.img 关联到一个 loop 设备,即虚拟出一个块设备 loop0
sudo losetup /dev/loop0 simplefs.img

然后就可以依次执行如下命令:

  1. sudo insmod simplefs.ko
  2. sudo mount -t simplefs /dev/loop0 /mnt
  3. sudo umount /mnt
  4. sudo rmmod simplefs

然后执行 sudo dmesg 可以看到内核日志中正确输出了相关信息:

[2080877.150746] loop0: detected capacity change from 0 to 1048576
[2080892.914067] Successfully registered simplefs
[2080898.528370] simplefs is successfully mounted on [/dev/loop0]
[2080903.367777] simplefs superblock is destroyed. Unmount successful.
[2080907.374764] Successfully unregistered simplefs

在本章中,我们实现了一个最简单的文件系统,虽然还不能创建文件和目录,但是已经可以被内核正确的挂载和卸载了,是一个很好的开端,后面我们将在当前文件系统的基础上继续扩展功能,以此来不断地深入理解 Linux 文件系统。

# 代码

整体代码如下

/*
 * A simple Filesystem for Linux Kernel 6.8.0.
 *
 * Initial Author:  Gality <gality369@gmail.com>
 * License: GNU General Public License v3 - https://www.gnu.org/licenses/gpl-3.0.html
 * Date: 2025-01-30
 */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
struct inode *simplefs_get_inode(struct super_block *sb,
				 const struct inode *dir, umode_t mode,
				 dev_t dev);
int simplefs_fill_super(struct super_block *sb, void *data, int silent);
/**
 * @sb: The super block of the filesystem (inodes, blocks, etc).
 * @dir: The parent directory's inode.
 * @mode: The mode of the inode to be created (S_IFDIR, S_IFREG, etc).
 * @dev: The device id of the inode to be created.
 * @return: The inode created.
 * This function creates an inode for the filesystem, initializes it 
 * and returns it. For now, it only creates inodes for root directories.
 */
struct inode *simplefs_get_inode(struct super_block *sb,
				 const struct inode *dir, umode_t mode,
				 dev_t dev)
{
	struct inode *inode = new_inode(sb);
	if (inode) {
		inode->i_ino = get_next_ino();
		inode_init_owner(&nop_mnt_idmap, inode, dir, mode);
		inode->__i_atime = inode->__i_mtime = inode->__i_ctime = inode_set_ctime_current(inode);
		switch (mode & S_IFMT) {
		case S_IFDIR:
			/* i_nlink will be initialized to 1 in the inode_init_always function
			 * (that gets called inside the new_inode function),
			 * We change it to 2 for directories, for covering the "." entry */
			inc_nlink(inode);
			break;
		case S_IFREG:
		case S_IFLNK:
		default:
			printk(KERN_ERR
			       "simplefs can create meaningful inode for only root directory at the moment\n");
			return NULL;
			break;
		}
	}
	return inode;
}
/**
 * @sb: The superblock which is passed from the VFS to the filesystem.
 * @data: The mount arguments data that might be passed to the filesystem while mounting.
 * @silent: A flag to indicate whether the filesystem should print logs or not.
 * @return: 0 on success, and error code on failure.
 * This function fills the super block of the filesystem with necessary information.
 */
int simplefs_fill_super(struct super_block *sb, void *data, int silent)
{
	struct inode *inode;
	/* A magic number that uniquely identifies our filesystem type */
	sb->s_magic = 0x20250130;
	inode = simplefs_get_inode(sb, NULL, S_IFDIR, 0);
	sb->s_root = d_make_root(inode);
	if (!sb->s_root)
		return -ENOMEM;
	return 0;
}
/**
 * @fs_type: The filesystem type structure that is registered with the kernel.
 * @flags: Mount flags (e.g. MS_RDONLY for read-only mounts).
 * @dev_name: The name of the device to be mounted (/dev/sda1, /dev/sdb1, etc).
 * @data: Extra data that might be passed to the filesystem while mounting.
 * @return: The root dentry of the filesystem that is mounted.
 * This function is called when the VFS is asked to mount this filesystem 
 * and returns the root dentry of the filesystem.
 */
static struct dentry *simplefs_mount(struct file_system_type *fs_type,
				     int flags, const char *dev_name,
				     void *data)
{
	struct dentry *ret;
	ret = mount_bdev(fs_type, flags, dev_name, data, simplefs_fill_super);
	if (unlikely(IS_ERR(ret)))
		printk(KERN_ERR "Error mounting simplefs");
	else
		printk(KERN_INFO "simplefs is successfully mounted on [%s]\n",
		       dev_name);
	return ret;
}
static void simplefs_kill_superblock(struct super_block *s)
{
	printk(KERN_INFO
	       "simplefs superblock is destroyed. Unmount successful.\n");
	/* This is just a dummy function as of now. As our filesystem gets matured,
	 * we will do more meaningful operations here */
	return;
}
struct file_system_type simplefs_fs_type = {
	.owner = THIS_MODULE,
	.name = "simplefs",
	.mount = simplefs_mount,
	.kill_sb = simplefs_kill_superblock,
};
static int simplefs_init(void)
{
	int ret;
	ret = register_filesystem(&simplefs_fs_type);
	if (likely(ret == 0))
		printk(KERN_INFO "Successfully registered simplefs\n");
	else
		printk(KERN_ERR "Failed to register simplefs. Error:[%d]", ret);
	return ret;
}
static void simplefs_exit(void)
{
	int ret;
	ret = unregister_filesystem(&simplefs_fs_type);
	if (likely(ret == 0))
		printk(KERN_INFO "Successfully unregistered simplefs\n");
	else
		printk(KERN_ERR "Failed to unregister simplefs. Error:[%d]",
		       ret);
}
module_init(simplefs_init);
module_exit(simplefs_exit);
MODULE_LICENSE("GPL"); /* 改变了许可证以符合 Linux 内核的要求 */
MODULE_AUTHOR("Gality");

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Gality 微信支付

微信支付

Gality 支付宝

支付宝