编写文件系统之装载与卸载

前言

一个通常意义上的文件系统驱动可以单独被编译成模块动态加载,也可以被直接编译到内核中,为了调试的方便,本文中的文件系统采用动态加载的方式实现。实现一个文件系统必须遵照内核的一些“规则”,以下我将以递进的顺序阐述文件系统的实现过程。

1.文件系统的加载与卸载

首先为了能够成功加载文件系统,文件系统需要提供文件系统的名字,超级块的加载和删除方法。这些东西反应在file_system,_type中。

1
2
3
4
5
6
struct file_system_type MISER_fs_type = {  
.owner = THIS_MODULE,
.name = "MISER_fs",
.mount = MISER_fs_mount,
.kill_sb = MISER_fs_kill_superblock, /* unmount */
};

文件系统作为一种块设备驱动,自然也需要实现module_init以及mocule_exit。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* Called when the module is loaded. */  
int MISER_fs_init(void)
{
int ret;
ret = register_filesystem(&MISER_fs_type);
if (ret == 0)
printk(KERN_INFO "Sucessfully registered MISER_fs\n");
else
printk(KERN_ERR "Failed to register MISER_fs. Error: [%d]\n", ret);
return ret;
}

/* Called when the module is unloaded. */
void MISER_fs_exit(void)

int ret;
ret = unregister_filesystem(&MISER_fs_type);
if (ret == 0)
printk(KERN_INFO "Sucessfully unregistered MISER_fs\n");
else
printk(KERN_ERR "Failed to unregister MISER_fs. Error: [%d]\n", ret);
}
module_init(MISER_fs_init);
module_exit(MISER_fs_exit);

MODULE_LICENSE("MIT");
MODULE_AUTHOR("cv");

我们可以看到,设备驱动加载的时候,驱动向内核注册了文件系统,而驱动卸载的时候,文件系统的信息也被删除。文件系统加载时调用的函数为MISER_fs_mount,实际上,这个函数向内核注册了一个回调:

int MISER_fs_fill_super(struct super_block *sb, void *data, int silent)
这个函数是用来与VFS交互从而生成VFS超级块的。在MISER fs中,超级块在磁盘的第二个4096字节上,即块号为1。这个函数执行时会从磁盘中读取信息,填充到VFS提供的超级块结构体中,下列为部分关键代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int MISER_fs_fill_super(struct super_block *sb, void *data, int silent)  {  
struct buffer_head *bh;
bh = sb_bread(sb, 1);
struct MISER_fs_super_block *sb_disk;
sb_disk = (struct MISER_fs_super_block *)bh->b_data;
struct inode *root_inode;
if (sb_disk->block_size != 4096) {
printk(KERN_ERR "MISER_fs expects a blocksize of %d\n", 4096);
ret = -EFAULT;
goto release;
}
//fill vfs super block
sb->s_magic = sb_disk->magic;
sb->s_fs_info = sb_disk;
sb->s_maxbytes = MISER_BLOCKSIZE * MISER_N_BLOCKS; /* Max file size */
sb->s_op = &MISER_fs_super_ops;
}

从上述代码可以看出,我们用sb_read来读取磁盘上的内容,然后填充super_block结构体。值得注意的是,有关超级块的操作函数即superblock_operations也是在此处赋值的。由于super_block* sb在文件系统卸载之前是一直存在于内存中的,所以我们可以使用s_fs_info来存储原始的超级块信息,避免后期交互时 再次读取磁盘。
文件系统卸载的时候超级块信息需要被删除,所以MISER_fs_kill_superblock的作用时释放该超级块,通知VFS该挂载点已经卸载。
实现基本函数后,可以对文件系统进行挂载操作,挂载操作的脚本内容如下:

1
2
3
4
5
6
7
sudo umount ./test  
sudo rmmod MISER_fs
dd bs=4096 count=100 if=/dev/zero of=image
./mkfs image
insmod MISER_fs.ko
mount -o loop -t MISER_fs image ./test
dmesg

上述脚本,将项目下的test文件夹作为文件系统的挂载点,并在挂载之后答应出了内核调试目录。成功执行该脚本的截图如下:
成功运行
我们可以看到test目录已经挂载成功而且内核调试信息显示文件系统挂载成功。

2.ls命令的实现

加载文件系统之后第一个要实现的功能是读取文件系统中的数据,所以选择实现文件夹读取操作,这一操作在2.x内核中是.readdir函数指针,在最新版本中是,.iterate函数指针。这个指针在保存在file_operation中,如下所示。

1
2
3
4
const struct file_operations MISER_fs_dir_ops = {  
.owner = THIS_MODULE,
.iterate = MISER_fs_iterate,
};

MISER_fs_iterate函数主要功能逻辑是读取inode的块数据,并且将块数据中的inode和文件名通过dir_emit函数传输到VFS层。以根目录为例,根目录的包含三个数据项,分别是父目录,当前目录和欢迎文件,所以该函数会执行以下三个语句

1
2
3
4
//参数分别表示上下文,文件/目录名,文件/目录名长度,inode号,文件类型  
dir_emit(ctx, ".", 1,0, DT_DIR);
dir_emit(ctx, "..", 2,0, DT_DIR);
dir_emit(ctx, "file", 4,1, DT_REG);

完成该函数后,在填充根目录inode时将MISER_fs_dir_ops指针赋值,即可在挂在文件系统后执行ls命令。
成功运行ls
如上图所示,我们成功看到了欢迎文件。但是此时我们不能对文件进行任何操作,因为还没有实现其他的接口。

3.磁盘管理相关逻辑的实现

这个磁盘管理的内涵包括向磁盘写入和从磁盘取出读取inode,更新inode信息,维护imap,bmap,inode table等操作。为了使磁盘上的内容有序的组合起来,磁盘空间的管理十分的重要,后续的文件读写操作都与此相关。
写入和删除inode的操作存放在super_operations这个结构体中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const struct super_operations MISER_fs_super_ops = {  
.evict_inode = MISER_evict_inode,
.write_inode = MISER_write_inode,
};
MISER_fs_super_ops需要在填充超级块时赋值到super_block的s_ops字段中。MISER_write_inode函数的功能是将内存中的inode保存在磁盘上。关键代码如下。

int MISER_write_inode(struct inode *inode, struct writeback_control *wbc)
{
struct buffer_head * bh;
struct MISER_inode * raw_inode = NULL;
MISER_fs_get_inode(inode->i_sb, inode->i_ino, raw_inode);
if (!raw_inode)
return -EFAULT;
raw_inode->mode = inode->i_mode;
raw_inode->i_uid = fs_high2lowuid(i_uid_read(inode));
raw_inode->i_gid = fs_high2lowgid(i_gid_read(inode));
raw_inode->i_nlink = inode->i_nlink;
raw_inode->file_size = inode->i_size;
raw_inode->i_atime = (inode->i_atime.tv_sec);
raw_inode->i_mtime = (inode->i_mtime.tv_sec);
raw_inode->i_ctime = (inode->i_ctime.tv_sec);
mark_buffer_dirty(bh);
brelse(bh);
return 0;
}

可以看到,该函数的将vfs inode中的相关信息存储到MISER_inode结构体中,然后写入磁盘。这个是单独的写入磁盘操作,事实上,当我们申请inode时,imap也是需要检查刷新的,需要把相应位置标记为1。同理,evict_inode函数的作用时删除inode,删除成功后,我们需要刷新imap的值,把相应位置标记为0。
设置和写入map的操作都在map.c中,以下以imap为例。对于imap来讲,申请inode的时候需要检查第一个空闲的inode编号,当inode被释放的时候也要及时清零对应的imap。与此相关的函数如下。

1
2
3
4
5
6
7
8
9
//从磁盘中读取数据并存在imap数组中  
int get_imap(struct super_block* sb, uint8_t* imap, ssize_t imap_size);
//在vaddr数组中找到第一个为0的bit,这个函数用于定位空inode或者block
int MISER_find_first_zero_bit(const void *vaddr, unsigned size);
//将imap的某一位置0或者1,并保存在磁盘上
int set_and_save_imap(struct super_block* sb, uint64_t inode_num, uint8_t value);
//定义的位操作宏如下
#define setbit(number,x) number |= 1UL << x
#define clearbit(number, x) number &= ~(1UL << x)

由于本文件系统并不是为了实际使用,所以上述的操作都没有考虑性能以及准确性问题。事实上,能够加上校验或者冗余备份是最好的。

4.读写文件内容

为了能够快速看到文件系统在正常工作,所以接下来需要实现文件的读写操作。文件读写操作按照一般处理,应该是实现在struct file_operations这个结构体中的。事实上,最开始我是实现在这个结构体中的read_iter函数指针中的。但是比较有趣的一点是,如果我们实现了struct address_space_operations结构体中的函数,那么struct file_operations结构体中的函数则可以交由VFS实现。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const struct file_operations MISER_fs_file_ops = {  
.owner = THIS_MODULE,
.llseek = generic_file_llseek,
.mmap = generic_file_mmap,
.fsync = generic_file_fsync,
.read_iter = generic_file_read_iter,
.write_iter = generic_file_write_iter,
};
const struct address_space_operations MISER_fs_aops = {
.readpage = MISER_fs_readpage,
.writepage = MISER_fs_writepage,
.write_begin = MISER_fs_write_begin,
.write_end = generic_write_end,
};

上述的generic开头的函数是不需要我们手动实现的。上述的address_space_operations操作其实是实现了页高速缓存的一些操作。页高速缓存是linux内核实现的一种主要磁盘缓存,它主要用来减少对磁盘的IO操作,具体地讲,是通过把磁盘中的数据缓存到物理内存中,把对磁盘的访问变为对物理内存的访问。这些接口一旦实现,那么对文件的操作就可以转移到内存中,这就是为什么可以使用generic开头的这些函数来代替手写。
MISER_fs_readpage, MISER_fs_writepage以及MISER_fs_write_begin都被注册回调到同一个函数MISER_fs_get_block。MISER_fs_get_block主要返回内核请求长度的数据。至于读写操作,内核调用__bwrite函数最终调用块设备驱动执行。因为在我没有采用二级或者多级索引,故而MISER_fs_get_block函数逻辑比较简单,部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int MISER_fs_get_block(struct inode *inode, sector_t block,  
struct buffer_head *bh, int create)
{
struct super_block *sb = inode->i_sb;
if (block > MISER_N_BLOCKS)
return -ENOSPC;
struct MISER_inode H_inode;
if (-1 == MISER_fs_get_inode(sb, inode->i_ino, &H_inode))
return -EFAULT;
if (H_inode.blocks == 0)
if(alloc_block_for_inode(sb, &H_inode, 1))
return -EFAULT;
map_bh(bh, sb, H_inode.block[block]);
return 0;
}

如上所示,该函数判断传入的block的大小,并将磁盘内容映射到bh中。后续的读写操作将有VFS帮我们完成。

5.inode操作

Inode操作涉及文件(夹)的创建删除,将MISER_inode映射到VFS中的inode等操作。具体实现的函数如下。

1
2
3
4
5
6
const struct inode_operations MISER_fs_inode_ops = {  
.lookup = MISER_fs_lookup,
.mkdir = MISER_fs_mkdir,
.create = MISER_fs_create,
.unlink = MISER_fs_unlink,
};

MISER_fs_lookup是其中比较复杂的一个函数,它负责将一个目录下的inode信息交由VFS管理。首先,MISER_fs_lookup读取文件夹的内容,然后遍历文件夹下面的MISER_inode,找到我们想要的MISER_inode,根据不同的文件属性,申请vfs_inode;并对不同的vfs_inode设置不同的操作。假设vfs_inode对应的是一个文件,那么就设置vfs_inode->mapping->a_ops,如果vfs_inode对应的是文件夹,那么就设置vfs_inode->f_ops = &MISER_fs_dir_ops;最后将vfs_inode注册到VFS中。这部分的关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct dentry *MISER_fs_lookup(struct inode *parent_inode,  
struct dentry *child_dentry, unsigned int flags)
{
struct super_block *sb = parent_inode->i_sb;
struct MISER_inode H_inode;
//省略代码
for (i = 0; i < H_inode.dir_children_count; i++) {
if (strncmp
(child_dentry->d_name.name, dtptr[i].filename,
MISER_FILENAME_MAX_LEN) == 0){
inode = iget_locked(sb, dtptr[i].inode_no);
if (inode->i_state & I_NEW) {
inode_init_owner(inode, parent_inode, 0);
struct MISER_inode H_child_inode;
if (-1 == MISER_fs_get_inode(sb, dtptr[i].inode_no, &H_child_inode))
return ERR_PTR(-EFAULT);
MISER_fs_convert_inode(&H_child_inode, inode);
inode->i_op = &MISER_fs_inode_ops;
if (S_ISDIR(H_child_inode.mode)) {
inode->i_fop = &MISER_fs_dir_ops;
} else if (S_ISREG(H_child_inode.mode)) {
inode->i_fop = &MISER_fs_file_ops;;
inode->i_mapping->a_ops = &MISER_fs_aops;
}
inode->i_mode = H_child_inode.mode;
inode->i_size = H_child_inode.file_size;
insert_inode_hash(inode);
unlock_new_inode(inode);
}
}
}
//省略代码
}

只有在这里注册了相关函数,系统调用才能正常执行。不然就会出现不支持的操作这种报错信息。
.create与.mkdir都是对应了inode的创建,只是inode的属性不能而已。.create创建普通文件而.mkdir创建文件夹。所以这两个函数的功能被函数MISER_fs_create_obj所处理。这个函数接受新建文件(夹)的请求,检查磁盘的大小,检查是否有空余的indoe,并且分配inode号,然后更新imap信息,最后更新超级块信息。由于该函数逻辑简单但是代码量比较大,故而不在此展示其具体实现。

总结

在完成上述工作之后,我们的文件系统基本已经完成了,这个系统采用线性(区别于minixi二级索引用树来管理)的方式管理磁盘空间,支持基本的增删改查文件操作,支持文件权限,支持多用户。

PS:前往公众号“Linux工坊”,可以查看最新内容!关注即可免费领取面试&Linux技术书籍!

------ 本文结束------
0%