前言
由于最近在学习文件系统相关知识。为了巩固这部分知识,想办法对其进行实践。本篇文章主要记录一下如何自定义一个文件系统。
主要参考了
《Linux内核探秘》——提供文件系统原理及框架说明
《一个简单地文件系统》——提供较新内核的简单文件系统实现代码。全篇文章以它为例
编译文件系统
自定义的文件系统可以作为一个模块,通过 insmod 的方式注册到内核中。类似编译一个驱动模块,编译一个文件系统模块需要:
- 文件系统相关代码
- Makefile
文件系统相关代码后续再进行说明,首先关注 Makefile 。
CONFIG_MODULE_SIG=n
ifneq ($(KERNELRELEASE),)
obj-m := myfs.o
else
KDIR:=/lib/modules/$(shell uname -r)/build
all:
make -C $(KDIR) M=$(PWD) modules
clean:
rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.order
endif
install:
sudo insmod myfs.ko
uninstall:
sudo rmmod myfs
mount:
mkdir testmp && sudo mount -t my none ./testmp
umount:
sudo umount ./testmp && rm -rf ./testmp
其中 KDIR 指定了用于编译模块的内核源码路径。意味着,需要根据目标内核进行 KDIR 的设置,一般路径为 /lib/modules/xxx版本/build。对于像 Ubuntu 发行版而言,其 /lib/modules/ 下会自带对应其自身版本的内核源码。如果希望编译的模块在当前 Ubuntu 下直接运行,则可以通过 uname -r 版本的获取,如 Makefile 所示。如果希望使用指定内核源码,例如 4.9.229,可以先编译该版本源码后,执行 make module_install ,则自动在 /lib/modules/ 下生成 4.9.229 版本的 build。
具体实现
在找到上述开源文件系统代码前,笔者是通过 《Linux内核探秘》 进行学习的。该书中通过自定义一个 aufs 文件系统,来对文件系统的文件管理功能进行说明。但 aufs 文件系统基于内核版本为 2.6.18,笔者尝试编译该版本内核一直失败,又考虑到目前较新版本 vfs 的实现与 2.6 的有不少出入,故作罢。才找到了上述开源的 myfs 文件系统。
结合 aufs 与 myfs 基本能够将文件系统的代码逻辑梳理清楚。两个文件系统都很简单,不同之处在于,aufs 直接在模块初始化时为文件系统创建了文件夹与文件,相当于模拟了 vfs 一些基本实现。而 myfs 则自定义了 vfs 的接口,但无额外功能。可以拿来即用。
首先,根据 aufs 了解一下如何自定义一个文件系统。整个代码的梳理顺序由下往上。
aufs
注册文件系统
insmod 一个模块时,会调用该模块 module_init 所指定的注册函数。对于 aufs 而言为 aufs_init。
- 通过 register_filesystem 将 au_fs_type 注册到内核中
- 执行 kern_mount,为 aufs 初始化一些数据结构。例如根目录 / 的 dentry,inode 等。在这里复习一下 dentry 与 inode。首先,用户所看到的目录,其实对应内核中的 dentry,用于根据文件名检索相应的 inode。在内核根文件系统中,就有一棵 dentry 组成的树。而其他文件系统,自己也有 dentry 树,所谓挂载 mount 的作用便是将其他文件系统的 dentry 树接到内核根文件系统的 dentry 树中,这样内核就能统一管理所有目录了。因此,在这里 aufs 调用 kern_mount ,会先为 aufs 创建属于自己的根目录 dentry 和 inode 。直到执行 mount -t aufs none /xxx 时,才会将 aufs 挂载到 /xxx 上 ,这些数据结构存在 vfsmount 结构体中,即 aufs_mount。
- 建好了 aufs 的根目录,那么便可调用 aufs_create_dir 来创建 aufs 的其他目录。这里的 aufs_create_dir 其实模拟了内核 mkdir 的执行流程。
- 进一步地可以通过 aufs_create_file ,基于父目录 pslot 创建文件。
可以看到,aufs 初始化时不单单进行注册,它还会自行创建两个 aufs 目录和几个文件。
static int __init aufs_init(void)
{
int retval;
struct dentry *pslot;
retval = register_filesystem(&au_fs_type);
if(!retval){
aufs_mount = kern_mount(&au_fs_type);
if(IS_ERR(aufs_mount)){
printk(KERN_ERR "aufs: could not mount!\n");
unregister_filesystem(&au_fs_type);
return retval;
}
}
pslot = aufs_create_dir("woman star",NULL);
aufs_create_file("lbb",S_IFREG|S_IRUGO,pslot,NULL,NULL);
aufs_create_file("fbb",S_IFREG|S_IRUGO,pslot,NULL,NULL);
aufs_create_file("ljl",S_IFREG|S_IRUGO,pslot,NULL,NULL);
pslot = aufs_create_dir("man star",NULL);
aufs_create_file("ldh",S_IFREG|S_IRUGO,pslot,NULL,NULL);
aufs_create_file("lcw",S_IFREG|S_IRUGO,pslot,NULL,NULL);
aufs_create_file("jw",S_IFREG|S_IRUGO,pslot,NULL,NULL);
return retval;
}
static void __exit aufs_exit(void)
{
simple_release_fs(&aufs_mount,&aufs_mount_count);
unregister_filesystem(&au_fs_type);
}
module_init(aufs_init);
module_exit(aufs_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("This is a simple module");
MODULE_VERSION("Ver 0.1");
创建目录
进一步地,需要实现 aufs_create_dir 完成目录项的创建。由于入参只有路径名 pathname,因此需要先根据 pathname 找到目标最近的目录项,才能进行创建
struct dentry *aufs_create_dir(const char *name,struct dentry *parent)
{
return aufs_create_file(name,S_IFDIR|S_IRWXU|S_IRUGO|S_IXUGO,parent,NULL,NULL);
}
struct dentry *aufs_create_file(const char *name,mode_t mode,struct dentry *parent,void *data,struct file_operations *fops)
{
struct dentry *dentry = NULL;
int error;
printk("aufs: creating file '%s'\n",name);
error = aufs_create_by_name(name,mode,parent,&dentry);
if (error){
dentry = NULL;
goto exit;
}
exit:
return dentry;
}
实际干活的是 aufs_create_by_name。通过 lookup_one_len 找到 name 中离目标最近的目录项 dentry。找到之后,便可以根据 mode 判断是创建目录文件还是普通文件。对应分别调用 aufs_mkdir,aufs_create。到这里我们可以知道,实际内核执行 mkdir 过程中,也是进行了 aufs_create_by_name 类似的步骤找出 dentry,而我们只需要实现 aufs_mkdir 和 aufs_create 接口即可。
static int aufs_create_by_name(const char *name, mode_t mode,struct dentry *parent, struct dentry **dentry)
{
int error = 0;
if(!parent){
if (aufs_mount && aufs_mount->mnt_sb){
parent = aufs_mount->mnt_sb->s_root;
}
}
if(!parent){
printk("Ah! can not find a parent!\n");
return -EFAULT;
}
*dentry = NULL;
mutex_lock(&parent->d_inode->i_mutex);
*dentry = lookup_one_len(name, parent, strlen(name));
if(!IS_ERR(dentry)){
if((mode & S_IFMT)==S_IFDIR)
error = aufs_mkdir(parent->d_inode,*dentry,mode);
else
error = aufs_create(parent->d_inode,*dentry,mode);
}else
error = PTR_ERR(dentry);
mutex_unlock(&parent->d_inode->i_mutex);
return error;
}
具体地,无论是 aufs_mkdir 还是 aufs_create 都是创建文件,都调用 aufs_mknod。因此 aufs_mknod 也是我们需要自行定义的。在 aufs 中如下,通过 aufs_get_inode 创建一个 inode,并将其绑到目录项 dentry中。在 inode 创建过程中,可以对它进行初始化,因此可以为他设置 file_operation 了,这样在 write 或者 read 时就能调用相应的方法。
static int aufs_mknod(struct inode *dir, struct dentry *dentry,int mode,dev_t dev)
{
struct inode *inode;
int error = -EPERM;
if(dentry->d_inode)
return -EEXIST;
inode = aufs_get_inode(dir->i_sb,mode,dev);
if (inode){
d_instantiate(dentry, inode);
dget(dentry);
error = 0;
}
return error;
}
static struct inode *aufs_get_inode(struct super_block *sb, int mode, dev_t dev)
{
struct inode *inode = new_inode(sb);
if(inode){
inode->i_mode = mode;
inode->i_uid = current->fsuid;
inode->i_gid = current.fsgid;
inode->i_blksize = PAGE_CACHE_SIZE;
inode->i_blocks = 0;
inode_>i_atime = inode->i_mtime = inode->i_ctime = CURRENT_TIME;
switch (mode & S_IFMT){
default:
init_special_inode(inode, mode, dev);
break;
case S_IFREG:
printk("create a file \n");
break;
case S_IFDIR:
inode->i_op = &simple_dir_inode_operations;
inode->i_fop = &simple_dir_operations;
printk("create a dir file \n");
inode->i_nlink++;
break;
}
}
return inode;
}
小结
根据 aufs,我们可以直观地体会到内核如何组织文件的。也可以明白,哪些部分 vfs 已经帮我们做好了,哪些部分需要自定义。总的来说,我们可以知道
- kern_mount 其实就是 mount 系统调用,这部分我们不用管也可以
- 对应的,aufs_create_dir 就是 mkdir ,aufs_create_file 就是 touch。这些我们也可以不管,因为 vfs 都会处理好的
- 但是我们需要实现 aufs_mkdir 和 aufs_create 的接口,事实上对应的就是 super_operation ( inode_operation)
- aufs_get_inode 我们也需要自定义,我们需要为自定义文件系统生成的 inode 指定 file_operation
myfs
理解了 aufs 后,我们看 myfs 就很轻松了。需要注意的是 myfs 对应较新的内核版本,一些结构体及其函数指针也发生了改变,如 file_system_type 中的 .get_sb 在较新内核版本中为 .mount,其返回值类型也变了。但这不影响对文件系统本质的理解。
基于上述内容,我们直接看 myfs 自定义了哪些方法
首先,初始化模块时,仅仅只进行文件系统注册 register_filesystem
static int __init init_my_fs(void)
{
printk("init myfs\n");
// 模块初始化时将文件系统类型注册到系统中
return register_filesystem(&my_fs_type);
}
定义了 operation
struct super_operations my_super_ops = { // 自定义 super_block 操作集合
.statfs = simple_statfs, // 给出文件系统的统计信息,例如使用和未使用的数据块的数目,或者文件件名的最大长度。
.drop_inode = generic_delete_inode, // 当inode的引用计数降为0时,将inode删除。
.put_super = myfs_put_super,
};
struct address_space_operations myfs_aops = {
.readpage = simple_readpage, // 用于从后备存储器将一页数据读入页框
.write_begin = simple_write_begin, // 根据文件的读写位置pos计算出文件的页偏移值,根据索引查找或者分配一个page结构。将页中数据初始化为“0”。
.write_end = simple_write_end, // 在对page执行写入操作后,执行相关更新操作。
.set_page_dirty = __set_page_dirty_no_writeback, // 将某个页标记为“脏”
};
struct file_operations myfs_file_operations = {
.read_iter = generic_file_read_iter,
.write_iter = generic_file_write_iter,
.splice_read = generic_file_splice_read,
.splice_write = iter_file_splice_write,
.fsync = noop_fsync, // 写回设备,内存文件系统不进行操作
.llseek = generic_file_llseek,
};
struct inode_operations myfs_dir_inode_ops = {
.create = myfs_create,
.lookup = simple_lookup, // 可以在此处重新设置 dentry 的操作函数
.link = simple_link, // 硬链接
.unlink = simple_unlink, // 删除文件
.symlink = myfs_symlink, // 软链接
.mkdir = myfs_mkdir,
.rmdir = simple_rmdir,
.mknod = myfs_mknod,
.rename = simple_rename,
};
struct inode_operations myfs_file_inode_ops = {
.getattr = simple_getattr,
.setattr = simple_setattr,
};
实现了供 vfs 调用的接口。这些实现,是通过与上述 operation 绑定后,operation 再与 inode 进行绑定,从而 vfs 可以通过 inode 完成调用。
static int
myfs_mknod(struct inode *dir, struct dentry *dchild, umode_t mode, dev_t dev)
{
struct inode * inode = myfs_get_inode(dir->i_sb, dir, mode, dev);
int error = -ENOSPC;
printk(KERN_INFO "myfs: mknod\n");
if (inode) {
d_instantiate(dchild, inode); // 将新建的 inode 和 dentry 相关联
dget(dchild); // 增加 dentry 的引用计数
dir->i_mtime = dir->i_ctime = current_time(dir); // 更新父目录的时间
error = 0;
/* real filesystems would normally use i_size_write function */
dir->i_size += 0x20; /* bogus small size for each dir entry */
}
return error;
}
// 创建目录
static int myfs_mkdir(struct inode * dir, struct dentry * dentry, umode_t mode)
{
int retval = myfs_mknod(dir, dentry, mode | S_IFDIR, 0);
if (!retval)
inc_nlink(dir); // 父目录的硬链接数+1,因为子目录的“..”
return retval;
}
// 创建普通文件
static int myfs_create(struct inode *dir, struct dentry *dentry, umode_t mode, bool want_excl)
{
return myfs_mknod(dir, dentry, mode | S_IFREG, 0);
}
写到这才发现关于 mount 的调用接口没有分析。不过大同小异,对应于 aufs 的为 aufs_get_sb,对应 myfs 为 myfs_get_sb
至此,如何实现一个文件系统也基本了解了,终于要进入正题 erofs