ggaaooppeenngg

为什么计算机科学是无限的但生命是有限的

aufs-如何自己编写一个文件系统

接着上篇文章阐述了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_SIZEPAGE_SIZE是一样的,PAGE_CACHE_SIZE_SHIFT是对应的位数,所以
s_blocksize_bits是块大小的bit位位数. 接着是inode初始化,new_inode为sb创建一个关联的inode结构体,并对inode初始化,包括uid,gid,i_mode.对应的i_fopi_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把页转换成虚拟地址或者逻辑地址,然后对应的读写操作最后都变成读写虚拟内存,或者逻辑内存.

单就构造一个文件系统来说,目的已经达到了,但是凡事不能不求甚解,下一篇博客准备记录一下内存管理相关的内容.

  1. 《Linux 内核探秘》http://book.douban.com/subject/25817503/
  2. ramfs目录 http://lxr.free-electrons.com/source/fs/ramfs/
  3. 内核模块编程教程 http://www.tldp.org/LDP/lkmpg/2.6/html/