本篇文章需要对 Linux 文件系统模型和 VFS 的 4 大基本对象有所了解,如果还不了解上述内容的话请参考之前的这篇博客:Linux 文件系统模型与 VFS 入门。
# 简单文件系统
当向 VFS 注册一个文件系统时,至少需要构建 superblock
,并在 superblock
中初始化根目录的 inode
和 dentry
,同时提供挂载(mount)和卸载(unmount)方法。在最简化的实现中,我们仅创建了 superblock
和根 inode
,未实现文件和目录的创建逻辑,因此此时文件系统仅包含根目录,无法添加新的文件或目录,也无法执行文件操作。这样的简化还有一个好处,我们可以暂时不涉及具体的存储介质,而是优先实现 VFS 相关逻辑,确保文件系统可以正确挂载,并逐步完善其功能。这些基本功能可以拆解为 5 个主要任务:
- 创建
inode
- 填充
superblock
- 实现
mount
方法 - 实现
unmount
方法 - 注册文件系统
# 创建 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 Numbersimplefs_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 |
然后就可以依次执行如下命令:
sudo insmod simplefs.ko
sudo mount -t simplefs /dev/loop0 /mnt
sudo umount /mnt
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"); |