前言
一个通常意义上的文件系统驱动可以单独被编译成模块动态加载,也可以被直接编译到内核中,为了调试的方便,本文中的文件系统采用动态加载的方式实现。实现一个文件系统必须遵照内核的一些“规则”,以下我将以递进的顺序阐述文件系统的实现过程。
1.文件系统的加载与卸载
首先为了能够成功加载文件系统,文件系统需要提供文件系统的名字,超级块的加载和删除方法。这些东西反应在file_system,_type中。
1 | struct file_system_type MISER_fs_type = { |
文件系统作为一种块设备驱动,自然也需要实现module_init以及mocule_exit。代码如下:
1 | /* Called when the module is loaded. */ |
我们可以看到,设备驱动加载的时候,驱动向内核注册了文件系统,而驱动卸载的时候,文件系统的信息也被删除。文件系统加载时调用的函数为MISER_fs_mount,实际上,这个函数向内核注册了一个回调:
int MISER_fs_fill_super(struct super_block *sb, void *data, int silent)
这个函数是用来与VFS交互从而生成VFS超级块的。在MISER fs中,超级块在磁盘的第二个4096字节上,即块号为1。这个函数执行时会从磁盘中读取信息,填充到VFS提供的超级块结构体中,下列为部分关键代码。
1 | int MISER_fs_fill_super(struct super_block *sb, void *data, int silent) { |
从上述代码可以看出,我们用sb_read来读取磁盘上的内容,然后填充super_block结构体。值得注意的是,有关超级块的操作函数即superblock_operations也是在此处赋值的。由于super_block* sb在文件系统卸载之前是一直存在于内存中的,所以我们可以使用s_fs_info来存储原始的超级块信息,避免后期交互时 再次读取磁盘。
文件系统卸载的时候超级块信息需要被删除,所以MISER_fs_kill_superblock的作用时释放该超级块,通知VFS该挂载点已经卸载。
实现基本函数后,可以对文件系统进行挂载操作,挂载操作的脚本内容如下:
1 | sudo umount ./test |
上述脚本,将项目下的test文件夹作为文件系统的挂载点,并在挂载之后答应出了内核调试目录。成功执行该脚本的截图如下:
成功运行
我们可以看到test目录已经挂载成功而且内核调试信息显示文件系统挂载成功。
2.ls命令的实现
加载文件系统之后第一个要实现的功能是读取文件系统中的数据,所以选择实现文件夹读取操作,这一操作在2.x内核中是.readdir函数指针,在最新版本中是,.iterate函数指针。这个指针在保存在file_operation中,如下所示。
1 | const struct file_operations MISER_fs_dir_ops = { |
MISER_fs_iterate函数主要功能逻辑是读取inode的块数据,并且将块数据中的inode和文件名通过dir_emit函数传输到VFS层。以根目录为例,根目录的包含三个数据项,分别是父目录,当前目录和欢迎文件,所以该函数会执行以下三个语句
1 | //参数分别表示上下文,文件/目录名,文件/目录名长度,inode号,文件类型 |
完成该函数后,在填充根目录inode时将MISER_fs_dir_ops指针赋值,即可在挂在文件系统后执行ls命令。
成功运行ls
如上图所示,我们成功看到了欢迎文件。但是此时我们不能对文件进行任何操作,因为还没有实现其他的接口。
3.磁盘管理相关逻辑的实现
这个磁盘管理的内涵包括向磁盘写入和从磁盘取出读取inode,更新inode信息,维护imap,bmap,inode table等操作。为了使磁盘上的内容有序的组合起来,磁盘空间的管理十分的重要,后续的文件读写操作都与此相关。
写入和删除inode的操作存放在super_operations这个结构体中。
1 | const struct super_operations MISER_fs_super_ops = { |
可以看到,该函数的将vfs inode中的相关信息存储到MISER_inode结构体中,然后写入磁盘。这个是单独的写入磁盘操作,事实上,当我们申请inode时,imap也是需要检查刷新的,需要把相应位置标记为1。同理,evict_inode函数的作用时删除inode,删除成功后,我们需要刷新imap的值,把相应位置标记为0。
设置和写入map的操作都在map.c中,以下以imap为例。对于imap来讲,申请inode的时候需要检查第一个空闲的inode编号,当inode被释放的时候也要及时清零对应的imap。与此相关的函数如下。
1 | //从磁盘中读取数据并存在imap数组中 |
由于本文件系统并不是为了实际使用,所以上述的操作都没有考虑性能以及准确性问题。事实上,能够加上校验或者冗余备份是最好的。
4.读写文件内容
为了能够快速看到文件系统在正常工作,所以接下来需要实现文件的读写操作。文件读写操作按照一般处理,应该是实现在struct file_operations这个结构体中的。事实上,最开始我是实现在这个结构体中的read_iter函数指针中的。但是比较有趣的一点是,如果我们实现了struct address_space_operations结构体中的函数,那么struct file_operations结构体中的函数则可以交由VFS实现。代码如下:
1 | const struct file_operations MISER_fs_file_ops = { |
上述的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 | int MISER_fs_get_block(struct inode *inode, sector_t block, |
如上所示,该函数判断传入的block的大小,并将磁盘内容映射到bh中。后续的读写操作将有VFS帮我们完成。
5.inode操作
Inode操作涉及文件(夹)的创建删除,将MISER_inode映射到VFS中的inode等操作。具体实现的函数如下。
1 | const struct inode_operations MISER_fs_inode_ops = { |
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 | struct dentry *MISER_fs_lookup(struct inode *parent_inode, |
只有在这里注册了相关函数,系统调用才能正常执行。不然就会出现不支持的操作这种报错信息。
.create与.mkdir都是对应了inode的创建,只是inode的属性不能而已。.create创建普通文件而.mkdir创建文件夹。所以这两个函数的功能被函数MISER_fs_create_obj所处理。这个函数接受新建文件(夹)的请求,检查磁盘的大小,检查是否有空余的indoe,并且分配inode号,然后更新imap信息,最后更新超级块信息。由于该函数逻辑简单但是代码量比较大,故而不在此展示其具体实现。
总结
在完成上述工作之后,我们的文件系统基本已经完成了,这个系统采用线性(区别于minixi二级索引用树来管理)的方式管理磁盘空间,支持基本的增删改查文件操作,支持文件权限,支持多用户。