接着上篇文章VFS- 内核是如何抽象文件系统的阐述了VFS以后,这篇文章主要想讲述一下在内核当中如何创建一个文件系统.其实根据上一篇博客来说,我们的文件系统主要能够满足VFS的抽象,就可以在内核中构建一个自己的文件系统.一个文件系统满足的功能其实就是针对文件的增删改查,目录的管理,还有链接等等,这是从用户的角度来看,而文件系统本身也要有自己的状态信息,维护在超级块里,可以被挂载,然后向下要提交IO请求(一般是磁盘也可以是网络,甚至是内存).这里的实现我们选择在内存当中实现一个文件系统.
代码参考了《Linux内核探秘》[1],以及内核代码ramfs的部分[2],基于内存构建一个文件系统.完整代码可以在这里查看,代码是基于2.6.32的内核的,当中涉及了一些模块编程的内容可以参考”The Linux Kernel Module Programming Guide”[3]
为了实现一个文件系统,首先我们需要定义一个文件系统.
执行make
,insmod aufs.ko
,然后cat /proc/filesystems | grep aufs
就能看到aufs名列其中,说明我们的文件系统已经注册到了内核当中.接下来我们需要挂载文件系统,但是挂载的过程中会导致panic,应为我们还没有定义文件系统super_block的获取和释放函数.
挂载文件系统的时候依赖这两个函数,不然就会导致空指针.接下来我们定义文件系统的两个接口.”kill_sb”使用的是内核函数kill_litter_super
,它会对super_block的内容进行释放.”get_sb”这个接口调用了”aufs_get_sb”函数,这个函数也是调用了内核函数get_sb_nodev
,这个函数会创建对应的super_block,这个函数针对的是不依赖/dev的文件系统,如果依赖/dev的话,需要调用别的函数,另外会根据/dev对应的设备获取super_block(比如说ext4会读对应的被格式化后的块设备的头来实例化超级块).我们需要传入一个函数指针用于填充空白的super_block,就是”aufs_fill_super”,然而”aufs_fill_super”也调用了内核函数.
看一下具体代码.
为了填充super_block,需要初始化sb以及创建根目录的inode和dentry.s_blocksize
指定了文件系统的块大小,一般是一个PAGE_SIZE
的大小,这里的PAGE_CACHE_SIZE
和PAGE_SIZE
是一样的,PAGE_CACHE_SIZE_SHIFT
是对应的位数,所以
s_blocksize_bits
是块大小的bit位位数. 接着是inode初始化,new_inode
为sb创建一个关联的inode
结构体,并对inode
初始化,包括uid
,gid
,i_mode
.对应的i_fop
和i_op
使用了内核默认的接口simple_dir(_inode)_operations
,后面会仔细讨论,这里先加上方便展示代码,如果对应的接口未定义的话,初始化的时候文件系统根目录会出现不会被认作目录的情况.
接下来安装模块,然后挂载文件系统,mount -t aufs none tmp
,因为我们的文件系统没有对应的设备类型所以参数会填none,对应的目录是tmp,这样tmp就成为了aufs的根目录,如果ls一把tmp,里面是什么都没有的,我们cd tmp && touch x
返回的结果是不被允许,因为我们还没有定义对应的接口,不能创建文件.
我们继续,我们让这个文件系统可以创建目录,那我们需要定义目录inode的接口,一组inode_operations
和一组file_operations
.以下是实现.
其实很简单,aufs_get_inode只创建目录类型的inode,并且赋值对应的函数指针.file_operations
使用的默认接口,这个后面再提,inode_operations
主要是inode的创建,aufs_create和aufs_mkdir都是对aufs_mknod针对不同mode的封装,aufs_symlink暂时不讲,因为inode还没有做mapping,软链的时候不可写会导致panic.进行上面类似的编译和挂载以后我们就能创建简单文件和目录了,但是创建的文件不能做任何操作,因为我们没有定义对应的接口.
挑个接口说一下,比如link接口就是创建了一个dentry指向了同一个inode,并且增加inode的引用计数,unlink就是把dentry删掉,inode保留.
软链有点复杂,所以放到后面讲.
当我们能够完成目录和文件的创建和删除之后,我们可以继续文件的读写了,换句话说我们要定义普通文件的inode的file_operations
接口.
为了能够添加文件我们增加如下代码
并把aufs_get_inode改成
这样以后我们就能对文件进行读写了,实际上文件的读写首先要依赖于mmap操作,把对应的页映射到虚拟内存当中来进行读写.编译并添加模块再挂载以后我们发现touch的文件可以读写了. 现在具体举一段代码路径分析一下,从read开始.
read其实还是依赖了aio_read的接口,只不过加上了wait的部分,保证同步,kiocb
是”kernel I/O control block”记录I/O的信息,这里标记了偏移和剩余量.
再看aio_read的接口
struct iovec
是一个数组每个元素是一段数据的开始和长度,这个结构和后面的io有关.
如果是不是DIRECT_IO的话,就会把iovector组装成read_descriptor_t
传入do_generic_file_read
当中.do_generic_file_read
的读的具体过程是
一般是通过mapping获取页缓存中的页并且读到用户空间中,在完成之后释放引用.读页的函数就是把page缓存刷掉.
获取页是通过mapping的radix_tree来找到对应的page引用.
写的过程也类似,同步写也调用了异步写的接口,最后把用户空间的数据拷贝到页当中.address_space_operations
就是对应vma映射的接口.
其中page <-> virtual_address的转换依赖于 kmap把页转换成虚拟地址或者逻辑地址,然后对应的读写操作最后都变成读写虚拟内存,或者逻辑内存.
单就构造一个文件系统来说,目的已经达到了,但是凡事不能不求甚解,下一篇博客准备记录一下内存管理相关的内容.
本文来源:
https://ggaaooppeenngg.github.io/zh-CN/2016/01/04/aufs-%E5%A6%82%E4%BD%95%E8%87%AA%E5%B7%B1%E7%BC%96%E5%86%99%E4%B8%80%E4%B8%AA%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F/