接着上篇文章 阐述了VFS以后,这篇文章主要想讲述一下在内核当中如何创建一个文件系统.其实根据上一篇博客来说,我们的文件系统主要能够满足VFS的抽象,就可以在内核中构建一个自己的文件系统.一个文件系统满足的功能其实就是针对文件的增删改查,目录的管理,还有链接等等,这是从用户的角度来看,而文件系统本身也要有自己的状态信息,维护在超级块里,可以被挂载,然后向下要提交IO请求(一般是磁盘也可以是网络,甚至是内存).这里的实现我们选择在内存当中实现一个文件系统.
代码参考了《Linux内核探秘》[1],以及内核代码ramfs的部分[2],基于内存构建一个文件系统.完整代码可以在这里 查看,代码是基于2.6.32的内核的,当中涉及了一些模块编程的内容可以参考”The Linux Kernel Module Programming Guide”[3]
为了实现一个文件系统,首先我们需要定义一个文件系统.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <linux/module.h> #include <linux/fs.h> static struct file_system_type au_fs_type = { .owner = THIS_MODULE, .name = "aufs", }; static int __init aufs_init(void) { register_filesystem(&au_fs_type); return 0; } static void __exit aufs_exit(void) { unregister_filesystem(&au_fs_type); } module_init(aufs_init); module_exit(aufs_exit); MODULE_LICENSE("GPL");
执行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”也调用了内核函数.
看一下具体代码.
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 34 35 36 37 static int aufs_fill_super(struct super_block *sb, void *data, int silent) { struct inode *inode = NULL; struct dentry *root; int err; sb->s_maxbytes = MAX_LFS_FILESIZE; sb->s_blocksize = PAGE_CACHE_SIZE; sb->s_blocksize_bits = PAGE_CACHE_SHIFT; sb->s_magic = AUFS_MAGIC; inode = new_inode(sb); if (!inode) { err = -ENOMEM; goto fail; } inode->i_mode = 0755; inode->i_uid = current_fsuid(); inode->i_gid = current_fsgid(); inode->i_atime = inode->i_mtime = inode->i_ctime = CURRENT_TIME; inode->i_mode |= S_IFDIR; inode->i_fop = &simple_dir_operations; inode->i_op = &simple_dir_inode_operations; // inc reference count for ".". inc_nlink(inode); root = d_alloc_root(inode); sb->s_root = root; if (!root) { err = -ENOMEM; goto fail; } return 0; fail: return err; }
为了填充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
.以下是实现.
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 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_atime = inode->i_mtime = inode->i_ctime = CURRENT_TIME; switch (mode & S_IFMT) { case S_IFDIR: // TODO: inode->i_op = &aufs_dir_inode_operations; inode->i_fop = &simple_dir_operations; /* i_nlink = 2 */ inc_nlink(inode); } } return inode; } static int aufs_mknod(struct inode *dir, struct dentry *dentry, int mode, dev_t dev) { struct inode *inode = aufs_get_inode(dir->i_sb, mode, dev); int error = -ENOSPC; if (inode) { // inherits gid. if (dir->i_mode & S_ISGID) { inode->i_gid = dir->i_gid; if (S_ISDIR(mode)) inode->i_mode |= S_ISGID; } d_instantiate(dentry, inode); // get dentry reference count. dget(dentry); error = 0; dir->i_mtime = dir->i_ctime = CURRENT_TIME; } return error; } static int aufs_mkdir(struct inode *dir, struct dentry *dentry, int mode) { int reval; retval = aufs_mknod(dir, dentry, mode | S_IFDIR, 0); printk("aufs: mkdir"); if (!retval) inc_nlink(dir); // . return retval; } static int aufs_create(struct inode *dir, struct dentry *dentry, int mode, struct nameidata *nd) { return aufs_mknod(dir, dentry, mode | S_IFREG, 0); } static const struct inode_operations aufs_dir_inode_operations = { .create = aufs_create, .lookup = simple_lookup, // get dentry. .link = simple_link, // same inode, different dentry. .unlink = simple_unlink, .symlink = aufs_symlink, // 之后再讲,目前没有做mapping会panic. .mkdir = aufs_mkdir, .rmdir = simple_rmdir, .mknod = aufs_mknod, .rename = simple_rename, // exchange dentry and dir. };
其实很简单,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保留.
1 2 3 4 5 6 7 8 9 10 11 int simple_link(struct dentry *old_dentry, struct inode *dir, struct dentry *dentry) { struct inode *inode = old_dentry->d_inode; inode->i_ctime = dir->i_ctime = dir->i_mtime = CURRENT_TIME; inc_nlink(inode); atomic_inc(&inode->i_count); dget(dentry); d_instantiate(dentry, inode); return 0; }
软链有点复杂,所以放到后面讲.
当我们能够完成目录和文件的创建和删除之后,我们可以继续文件的读写了,换句话说我们要定义普通文件的inode的file_operations
接口. 为了能够添加文件我们增加如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 static const struct address_space_operations aufs_aops = { .readpage = simple_readpage, .write_begin = simple_write_begin, .write_end = simple_write_end, /* .set_page_dirty = __set_page_dirty_no_writeback, 内核私有函数 */ }; static const struct file_operations aufs_file_operations = { .read = do_sync_read, // file read get mapping page and copy to userspace. .aio_read = generic_file_aio_read, .write = do_sync_write, .aio_write = generic_file_aio_write, .mmap = generic_file_mmap, .fsync = simple_sync_file, .splice_read = generic_file_splice_read, .splice_write = generic_file_splice_write, .llseek = generic_file_llseek, }; static const struct inode_operations aufs_file_inode_operations = { .getattr = simple_getattr, };
并把aufs_get_inode改成
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 34 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_mapping->a_ops = &aufs_aops; mapping_set_gfp_mask(inode->i_mapping, GFP_HIGHUSER); mapping_set_unevictable(inode->i_mapping); 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_IFDIR: inode->i_op = &aufs_dir_inode_operations; inode->i_fop = &simple_dir_operations; /* i_nlink = 2 for "." */ inc_nlink(inode); break; case S_IFREG: inode->i_op = &aufs_file_inode_operations; inode->i_fop = &aufs_file_operations; break; case S_IFLNK: inode->i_op = &page_symlink_inode_operations; break; } } return inode; }
这样以后我们就能对文件进行读写了,实际上文件的读写首先要依赖于mmap操作,把对应的页映射到虚拟内存当中来进行读写.编译并添加模块再挂载以后我们发现touch的文件可以读写了. 现在具体举一段代码路径分析一下,从read开始.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ssize_t do_sync_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos) { struct iovec iov = { .iov_base = buf, .iov_len = len }; struct kiocb kiocb; ssize_t ret; init_sync_kiocb(&kiocb, filp); kiocb.ki_pos = *ppos; kiocb.ki_left = len; for (;;) { ret = filp->f_op->aio_read(&kiocb, &iov, 1, kiocb.ki_pos); if (ret != -EIOCBRETRY) break; wait_on_retry_sync_kiocb(&kiocb); } if (-EIOCBQUEUED == ret) ret = wait_on_sync_kiocb(&kiocb); *ppos = kiocb.ki_pos; return ret; }
read其实还是依赖了aio_read的接口,只不过加上了wait的部分,保证同步,kiocb
是”kernel I/O control block”记录I/O的信息,这里标记了偏移和剩余量. 再看aio_read的接口
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 ssize_t generic_file_aio_read(struct kiocb *iocb, const struct iovec *iov, unsigned long nr_segs, loff_t pos) { struct file *filp = iocb->ki_filp; ssize_t retval; unsigned long seg; size_t count; loff_t *ppos = &iocb->ki_pos; count = 0; retval = generic_segment_checks(iov, &nr_segs, &count, VERIFY_WRITE); if (retval) return retval; /* coalesce the iovecs and go direct-to-BIO for O_DIRECT */ if (filp->f_flags & O_DIRECT) { loff_t size; struct address_space *mapping; struct inode *inode; mapping = filp->f_mapping; inode = mapping->host; if (!count) goto out; /* skip atime */ size = i_size_read(inode); if (pos < size) { retval = filemap_write_and_wait_range(mapping, pos, pos + iov_length(iov, nr_segs) - 1); if (!retval) { retval = mapping->a_ops->direct_IO(READ, iocb, iov, pos, nr_segs); } if (retval > 0) *ppos = pos + retval; if (retval) { file_accessed(filp); goto out; } } } for (seg = 0; seg < nr_segs; seg++) { read_descriptor_t desc; desc.written = 0; desc.arg.buf = iov[seg].iov_base; desc.count = iov[seg].iov_len; if (desc.count == 0) continue; desc.error = 0; do_generic_file_read(filp, ppos, &desc, file_read_actor); retval += desc.written; if (desc.error) { retval = retval ?: desc.error; break; } if (desc.count > 0) break; } out: return retval; }
struct iovec
是一个数组每个元素是一段数据的开始和长度,这个结构和后面的io有关. 如果是不是DIRECT_IO的话,就会把iovector组装成read_descriptor_t
传入do_generic_file_read
当中.do_generic_file_read
的读的具体过程是
1 2 3 4 5 6 7 8 9 10 struct address_space *mapping = filp->f_mapping; ... for { index = *ppos >> PAGE_CACHE_SHIFT; // 循环读取ppos,ppos每次都会更新,然后右移,相当于模一个页的大小,找到以页偏移的单位. find_get_page(mapping,index); // 获取对应的page引用. mapping->a_ops->readpage(filp, page); // 读取对应的页. ... page_cache_release(page); }
一般是通过mapping获取页缓存中的页并且读到用户空间中,在完成之后释放引用.读页的函数就是把page缓存刷掉.
1 2 3 4 5 6 7 8 int simple_readpage(struct file *file, struct page *page) { clear_highpage(page); flush_dcache_page(page); SetPageUptodate(page); unlock_page(page); return 0; }
获取页是通过mapping的radix_tree来找到对应的page引用. 写的过程也类似,同步写也调用了异步写的接口,最后把用户空间的数据拷贝到页当中.address_space_operations
就是对应vma映射的接口.
其中page <-> virtual_address的转换依赖于 kmap把页转换成虚拟地址或者逻辑地址,然后对应的读写操作最后都变成读写虚拟内存,或者逻辑内存.
单就构造一个文件系统来说,目的已经达到了,但是凡事不能不求甚解,下一篇博客准备记录一下内存管理相关的内容.
《Linux 内核探秘》http://book.douban.com/subject/25817503/
ramfs目录 http://lxr.free-electrons.com/source/fs/ramfs/
内核模块编程教程 http://www.tldp.org/LDP/lkmpg/2.6/html/