Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2033983
  • 博文数量: 369
  • 博客积分: 10093
  • 博客等级: 上将
  • 技术积分: 4271
  • 用 户 组: 普通用户
  • 注册时间: 2005-03-21 00:59
文章分类

全部博文(369)

文章存档

2013年(1)

2011年(2)

2010年(10)

2009年(16)

2008年(33)

2007年(146)

2006年(160)

2005年(1)

分类: LINUX

2006-10-30 23:45:06

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的定义,它在这个文件系统中创建目录,参考完整的源码,弄明白它是如何工作的。

译者注:
第一次翻译国外的技术文章,有些吃力,比较晚了,明天再翻译省下的部分!
晚安!
阅读(4137) | 评论(1) | 转发(1) |
给主人留下些什么吧!~~

liuxuejin2011-06-06 11:22:52

真的不错的一篇文章,在大学的时候就已经知道了lwnfs,但是没能看懂,因为本人是是搞java的,跟数据库打交道多,而不是文件系统,平时工作也用不了,今天看到这篇文章,才知道其中的东西!文章我收下了,希望能看到更好的文章!我觉得这些文章对于那些读大学的学生应该有很大的帮助