Linus和其他很多内核开发人员都不喜欢ioctl()这个系统调用,认为那是以一种不可控制的方式向内核添加新的系统调用。同时,也不赞成向/proc下添加新的文件,因为那里已经是一片狼藉。他们提倡那些尝试在他们的代码中添加ioctl()或者/proc文件的开发者用一个单独的虚拟文件系统来替代。文件系统使得接口在用户空间清晰可见;同时也为通过脚本来管理系统提供了方便。但是,写一个Linux文件系统通常都是让人望而却步的。很难想像,一个想快速开发一个驱动程序的开发者被迫学习VFS API的那种窘态!
2.6内核包含了一个成为“libfs”的函数集,设计它的目的是为了降低实现虚拟文件系统的难度。libfs接管了很多实现Linux文件系统API的普遍性操作,让那些非文件系统开发人员能(大多数情况下)集中精力实现他们要提供的功能。这篇文章试图添补这个方面的空白。
我们要完成的任务并非雄心勃勃:导出一个充满计数器文件的简单文件系统。读其中的任何一个文件中所包含的计数器的值,都将导致计数器加1。交互过程如下所示:
# cat /lwnfs/counter 0 # cat /lwnfs/counter 1 # ...
|
无聊的人可能会这样将计数器值涨到上千,也许等会儿真的会有人这么做。但是,一些不安分的急性子能够通过写计数器文件来快速达到目的:
# echo 1000 > /lwnfs/counter # cat /lwnfs/counter 1000 # |
好了,Linux发行商们不会对这个新的lwnfs的“性能”感冒。但是,它却能展示给你如何创建一个虚拟文件系统。感兴趣的人可以在查看。
初始化和创建超级块那么让我们开始吧!一个实现了文件系统的可加载内核模块必须在加载的时候向VFS层注册其文件系统,lwnfs模块的初始化代码很简单:
static int __init lfs_init(void) { return register_filesystem(&lfs_type); } module_init(lfs_init);
|
参数lfs_type是一个按照如下方式建立的结构体:
static struct file_system_type lfs_type = { .owner = THIS_MODULE, .name = "lwnfs", .get_sb = lfs_get_super, .kill_sb = kill_litter_super, };
|
这是向内核描述文件系统类型的基本数据结构。owner用来管理模块的引用计数,防止在文件系统代码使用其间卸载内核模块。name就是最终在用户空间的mount命令行上出现的文件系统类型。接下来的两个函数是用来管理文件系统的超级块的,所谓的超级块也就是文件系统数据结构的根。kill_litter_super()是由VFS提供的通用函数,当文件系统卸载(unmount)的时候,它简单地释放所有内置的结构体,简单虚拟文件系统的作者不用担心这些事情。(在模块卸载(unload)的时候,必须注销其文件系统,请参考lwnfs的exit函数。)
在很多情况下,超级块的创建都必须由文件系统的编写者来完成,但是请看下面“更简单的方式”部分。这需要调用少量的模板代码。在这个例子中,lfs_get_super()的实现如下:
static struct super_block *lfs_get_super(struct file_system_type *fst, int flags, const char *devname, void *data) { return get_sb_single(fst, flags, data, lfs_fill_super); } |
再说一遍,get_sb_single()是一个通用函数,它接管了很多超级块的创建工作。但是,它会调用lfs_fill_super()来建立我们这个特殊的小文件系统的特征。它的原型如下:
static int lfs_fill_super (struct super_block *sb, void *data, int silent); |
待创建的超级块和其他两个我们可以忽略的参数一起被传入函数。尽管如此,我们还是不得不初始化一些结构成员。代码如下所示:
sb->s_blocksize = PAGE_CACHE_SIZE; sb->s_blocksize_bits = PAGE_CACHE_SHIFT; sb->s_magic = LFS_MAGIC; sb->s_op = &lfs_s_ops; |
大多数虚拟文件系统的实现都有类似这样的代码; 它只是设置文件系统的块大小,一个用来辨识超级块的“幻数”(Magic number),和超级块的操作函数集。这些操作对于只写一个简单的虚拟文件系统来说是非必须的,因为libfs已经提供了它所需的东西。因此lfs_s_ops定义(在文件头部)如下:
static struct super_operations lfs_s_ops = { .statfs = simple_statfs, .drop_inode = generic_delete_inode, }; |
创建根目录回到lfs_fill_super()的实现,我们剩下的最大工作就是为我们的新文件系统创建并导出根目录。第一步,为这个目录创建inode:
root = lfs_make_inode(sb, S_IFDIR | 0755); if (! root) goto out; root->i_op = &simple_dir_inode_operations; root->i_fop = &simple_dir_operations; |
我们最终会发现lfs_make_inode() 是个模板程序;就目前来说,我们假设它返回一个新建的,初始化过的,并且可用的inode。它需要超级块和一个mode参数,这个mode参数就像用stat()系统调用返回的mode值。我们传入了S_IFDIR,因此返回的inode将代表一个目录。我们所赋给inode的文件和目录的操作函数集再一次来自libfs.
为了让VFS能够找到这个目录,它的inode必须放进目录缓存(一个“dentry”结构体);具体做法如下:
root_dentry = d_alloc_root(root); if (! root_dentry) goto out_iput; sb->s_root = root_dentry; |
创建文件目前,超级块中已经有了一个初始化过的根目录。所有实际的目录操作都将被libfs和VFS层所接管,所以,生活如此简单。但是,把一些有趣的文件放进根目录中是libfs不能代劳的,因为那是我们的事。因此,lfs_fill_super()返回前所做的最后一件事就是:
lfs_create_files(sb, root_dentry); |
在我们的示例代码中,lfs_create_files()在这个文件系统的根目录和其子目录中各创建一个计数器文件。我们主要看根目录下的文件。计数器作为一个atomic_t变量实现,我们的顶层计数器(被极具想像力地称为“counter)的实现如下:
static atomic_t counter;
static void lfs_create_files (struct super_block *sb, struct dentry *root) { /* ... */ atomic_set(&counter, 0); lfs_create_file(sb, root, "counter", &counter); /* ... */ } |
在在一个目录中创建一个文件的工作中,lfs_create_file做了实际的工作,它被实现地尽可能简单,但是,仍旧有一些步骤要执行。函数开始部分如下:
static struct dentry *lfs_create_file (struct super_block *sb, struct dentry *dir, const char *name, atomic_t *counter) { struct dentry *dentry; struct inode *inode; struct qstr qname; |
参数包括常见的超级块结构体和将要包含这个文件的目录dir。目前,dir是我们之前创建的根目录,但是它可以是这个文件系统中的任何目录。
我们的首要任务是为这个新建的文件创建一个目录入口(directory entry):
qname.name = name; qname.len = strlen (name); qname.hash = full_name_hash(name, qname.len); dentry = d_alloc(dir, &qname); |
qname的初始化只是为了hash文件名,以使它能在目录入口缓存被快速找到。一旦此事完成,我们就和我们的父目录一起创建入口。这个文件也需要一个inode,我们可以如此创建:
inode = lfs_make_inode(sb, S_IFREG | 0644); if (! inode) goto out_dput; inode->i_fop = &lfs_file_ops; inode->u.generic_ip = counter; |
我们再一次调用lfs_make_inode(我保证,我们不久将会看到它),但是这次我们用它创建一个普通文件。在虚拟文件系统中,特殊目的文件的创建的关键在于另外两个赋值操作:
- i_fop项被设置为我们用来读写计数器值的文件操作函数集。
- 我们用inode中的u.generic_ip指针来指向和这个文件相关联的atomic_t的计数器。
换句话说,i_fop定义了这个特定文件的行为,并且u.generic_ip是文件相关的数据。所有有意思的虚拟文件系统都是用这两项来建立指定的行为的。
创建文件的最后一步是将它加入目录入口缓存:
d_add(dentry, inode); return dentry; |
把inode放进目录入口缓存,使得VFS可以在不调用文件系统目录操作函数的条件下找到文件。并且,也意味着我们的文件系统没有必要有任何目录操作函数集。我们虚拟文件系统的所有数据项都位于内核的缓存中,因此我们的模块也没有必要记得它所创建的文件系统的结构,也没有必要实现查找操作。没有必要也就是说,它使得生活变得更简单。
Inode的创建前面,我们已经深入了解了计数器的实际实现,是时候看看lfs_make_inode()了。这个函数相当模板化,如下所示:
static struct inode *lfs_make_inode(struct super_block *sb, int mode) { struct inode *ret = new_inode(sb);
if (ret) { ret->i_mode = mode; ret->i_uid = ret->i_gid = 0; ret->i_blksize = PAGE_CACHE_SIZE; ret->i_blocks = 0; ret->i_atime = ret->i_mtime = ret->i_ctime = CURRENT_TIME; } return ret; } |
它简单地申请新的inode结构,并且用适合虚拟文件的值来初始化它。mode的赋值是比较有意思的,这个inode最终是代表一个普通文件还是一个目录(或者是其他的东西)完全取决于传入的mode值。
实现文件操作函数集直到此刻,我们对于使得计数器文件实际工作的东西知之甚少;前面都是一些让我们有放计数器文件的小文件系统的VFS模板化操作。现在,让我们了解实际工作是如何完成的时候到了!
对计数器自身的操作在我们赋给计数器文件的inode的file_operations结构里面:
static struct file_operations lfs_file_ops = { .open = lfs_open, .read = lfs_read_file, .write = lfs_write_file, }; |
记住,一个指向这个结构的指针被我们用lfs_create_file()保存在了inode里面。
最简单的操作函数是open():
static int lfs_open(struct inode *inode, struct file *filp) { filp->private_data = inode->u.generic_ip; return 0; } |
这个函数做的唯一一件事就是把指向atomic_t的指针值拷贝到file结构中,这使得它更容易被获得。
有趣的工作是由函数read()完成的,它必须增加计数器并且将它的值返回给用户空间程序。它用普通的read()操作原型:
static ssize_t lfs_read_file(struct file *filp, char *buf, size_t count, loff_t *offset) |
它从读和增加计数开始:
atomic_t *counter = (atomic_t *) filp->private_data; int v = atomic_read(counter); atomic_inc(counter); |
这段代码简化了点儿,详情请看模块的源码。一些读者或许已经注意到了此处的竞争:两个进程会在他们任何一个增加它之前读它,这将导致同样的计数器值被返回两次,这确实够“可怕”的。一个严谨的模块应该用自旋锁(spinlock)串行化对计数器的访问。但是,这个只是作为一个简单的演示而已!
因此,无论如何,一旦我们获得了计数器的值,我们就要把它返回给用户空间。那意味着把它编码成字符串的形式,并且指出它在用户空间缓冲区的位置和布局。毕竟,一个用户空间程序能够在我们的虚拟文件上进行查找。
len = snprintf(tmp, TMPSIZE, "%d\n", v); if (*offset > len) return 0; if (count > len - *offset) count = len - *offset; |
一旦我们指出我们将拷贝多少数据,我们做就是了,调整好文件偏移量,然后,工作完成。
if (copy_to_user(buf, tmp + *offset, count)) return -EFAULT; *offset += count; return count; |
然后,还有lfs_write_file(), 它让用户设置我们其中一个计数器的值。
static ssize_t lfs_write_file(struct file *filp, const char *buf, size_t count, loff_t *offset) { atomic_t *counter = (atomic_t *) filp->private_data; char tmp[TMPSIZE];
if (*offset != 0) return -EINVAL; if (count >= TMPSIZE) return -EINVAL;
memset(tmp, 0, TMPSIZE); if (copy_from_user(tmp, buf, count)) return -EFAULT; atomic_set(counter, simple_strtol(tmp, NULL, 10)); return count; }
|
这里只是粗略的介绍。这个模块还包含lfs_create_dir的定义,它在这个文件系统中创建目录,参考完整的源码,弄明白它是如何工作的。
译者注:第一次翻译国外的技术文章,有些吃力,比较晚了,明天再翻译省下的部分!
晚安!
阅读(4150) | 评论(1) | 转发(1) |