创建虚拟文件系统

[由于libfs接口的改变, 这篇文章已经重新编辑了;不过你们仍然可以参考原始版本。]

[这篇文章是LWN移植驱动到2.6内核系列的一部分]

Linus和很多其它内核开发者都不喜欢ioctl()这个内核调用,因为他们觉得这是在以一种不可控的方式向内核添加新的文件系统调用。直接往/proc添加新的文件会把那片区域弄得很乱,因此也是不提倡的。所以我们推荐那些采用ioctl或者proc方式移植他们代码的开发者们可以创建一个独立的虚拟文件系统。文件系统可以让它们的接口在用户空间清晰、直观、可见,而且也方便编写脚本来执行管理程序。不过Linux文件系统的编写却不是一个简单的活。我们也可以理解那些想要提高他们驱动接口效率的开发者要硬着头皮去学习VFSAPI

2.6版本的内核包含一些叫做libfs库函数,它们可以简化我们编写文件系统的工作。Libfs解决了许多实现Linux文件系统API中比较单调的工作,允许非文件系统开发者们更多关注于它们要提供的特定功能。然而,最大的遗憾是……缺少相应的文档。这篇文章就是想小小地填补一下这个空白。

下面我们要做的事实际上并没有啥屌的:导出一个充满计数文件的简单的文件系统(lwnfs类型)。读取下面任何一个包含计数器的文件时都会使其自动加1。然后我们就实现了下面的某种程度让人小小激动的交互:

        # cat /lwnfs/counter

        0

        # cat /lwnfs/counter

        1

        # ...

够无聊的话,你也可以用这种方式将计数器加到1000,不过更多的人应该会很快就厌倦了这种游戏。没有耐心的同学可以采用直接往计数文件写入的方式得到更大的值:

        # echo 1000 > /lwnfs/counter

        # cat /lwnfs/counter

        1000

         #

好吧,Linux发行商们应该不会对lwnfs性能感兴趣。但是它确实提供了一种方式创建虚拟文件系统。那些感兴趣的同学可以从这里(http://lwn.net/Articles/57371/)获得全部源代码。

文件系统的初始化与超级块的安装

我们来开始吧!!在装载的时候,一个实现了文件系统的可装载模块必须将文件系统注册到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提供的一个通用函数,它仅仅在文件系统卸载时清理所有的核心数据结构,而编写一个简单的文件系统不需要考虑这方面的事情。(当然,在卸载时注销文件系统是必须的,详情参见lwnfsexit函数)。

在许多情况下,创建超级块的任务必须由文件系统作者完成——不过可以看下面一种更简单的方式小节。下面的处理涉及到一些样板代码。因此,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变量(就像stat系统调用返回的mode值 一样。)这里我们传入了S_IFDIR,那么返回的inode就表示一个目录。再次说明,我们赋值给inode的文件和目录操作都来自libfs

目录inode必须放入目录缓存(是由目录项(dentry)结构体实现的)VFS才能找到。可以实现如下:

        root_dentry = d_alloc_root(root);

        if (! root_dentry)

        goto out_iput;

        sb->s_root = root_dentry;

创建文件

现在超级块已经完全初始化了根目录。所有真正的目录操作都将由libfsVFS层处理,就这么简单。然而,也有libfs不能替我们做的事,向根目录放入我们感兴趣的东西。所以,lfs_fill_super()返回前还需要调用:

        lfs_create_files(sb, root_dentry);

在我们的简单模块中,lfs_create_files()在根目录下创建一个计数器文件,在子目录下创建另一个。多数情况我们只关注根目录级的文件。计数器是用atomic_t型变量实现的。我们最上层的计数器设置如下:

        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;

参数有超级块结构(super_block)、目录(dir)结构(包含这个文件的目录的目录项(dentry))。当前情况下,dir就是我们之前创建的根目录,但它可以是文件系统内的任何目录。

我们第一个任务就是为新文件创建一个目录项:

        qname.name = name;

        qname.len = strlen (name);

        qname.hash = full_name_hash(name, qname.len);

        dentry = d_alloc(dir, &qname);

为了加快在目录项缓存中的查询速度,我们在qname的设定对文件名做了哈希。这些完成以后,我们就在当前父目录下创建了目录项。不过,文件还需要一个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域设定了我们读写计数器(counter)的文件操作。

u_generic_ip指针指向一个结构,里面有一个关联到这个文件的atomic_t类型的计数器(counter)。

也就是说,i_fop定义了特定文件的特定操作,u.generic_ip存了特定文件的数据。所有实际的虚拟文件系统都是利用了这两个域来设定需要的操作行为。

创建文件的最后一步是将它加入目录项缓存中:

d_add(dentry, inode);

return dentry;

inode加入目录项缓存使得VFS不用调用我们自己文件系统的目录项操作就能找到该文件。反过来,这意味着我们的文件系统没必要提供任何实际的目录项操作。我们的整个虚拟文件系统存在于内核缓存层,所以我们的模块没有必要记录它设定的文件系统的结构,也没必要实现一个lookup(用于跟踪路径)的操作。不用我说,这更简单了。

创建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表示一个普通文件还是一个目录(或者其他)。

文件操作的实现

关于这一点,我们前面几乎没有看到什么能让计数器文件工作。那些都是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);

在开始处读取并增加计数器(counter)的值:

atomic_t *counter = (atomic_t *) filp->private_data;

int v = atomic_read(counter);

atomic_inc(counter);

代码已经做了简化了,还有一些无关的鸡肋代码参见模块的源码。一些读者也注意到了此处存在资源竞争:可能存在两个进程在他们增加计数器前都读了原来计数器的值。在极端情况下,结果会发生同样的计数器值被返回两次。一个严谨的模块可能会用一个自旋锁(spinlock)序列化获取计数器的值。不过,我们这里只是做一个简单的示例。

不管怎样,一旦我们取得了计数器的值,就必须把它返回给用户空间。这意味着需要用字符方式编码计数器的值,并弄清楚在哪里以及如何放到用户态缓存。别忘了,一个用户态进程可以在我们的虚拟文件中定位(seek)。

len = snprintf(tmp, TMPSIZE, "%d\n", v);

if (*offset > len)

return 0;

if (count > len - *offset)

count = len - *offset;

一旦我们弄清楚了我们可以复制回多少数据,我们就可以改变文件当前位置(offset)并完成它。

if (copy_to_user(buf, tmp + *offset, count))

return -EFAULT;

*offset += count;

return count;

然后,就是lfs_write_file()了,它允许用户设定其中一个计数器(counter)的值:

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,用于在文件系统中创建目录。详情参见完整源代码。

一种更简单的方式

上面的例子中包含了大量不明觉厉的模版代码。那些模版对于许多应用来说都是必需的,但是对于很多其它应用还有一条捷径。如果在编译的时候你就知道要创建哪些文件,并且你不需要创建子目录,继续往下看这种捷径。

这个小节,我们将会讨论一个不同的lwnfs模块版本——减少了近三分之一的代码。它实现了一个包含四个计数器的简单数组,没有子目录。再次提醒,如果你感兴趣,可以获取完整源代码。

我们来看一个函数lfs_fill_super(),它填充了文件系统超级块,创建根目录,并将文件填入根目录。在这个简单版本中,整个函数变成这样了:

static int lfs_fill_super(struct super_block *sb, void *data, int silent)

    {

    return simple_fill_super(sb, LFS_MAGIC, OurFiles);

    }

Simple_fill_super()libfs中的一个完成了几乎所有我们需要做的事情的函数。它的原型如下:

int simple_fill_super(struct super_block *sb, int magic, 

                          struct tree_descr *files);

超级块结构参数可以直接传入,幻数(magic)和上面见到的那个一样。Files参数描述了需要在文件系统中创建的文件。相关结构定义如下:

struct tree_descr { 

    char *name; 

    struct file_operations *ops; 

    int mode; 

    };

这些变量含义已经很明显了,每个结构给出了待创建文件的名字,相关文件的操作以及文件的保护位【】。然而,一些关于tree_descr结构指针如何创建的注意事项:

NULL填充的入口被简单地忽略了。除非你想处理一堆错误,否则不要在列表结尾放一些填充了NULL的结构。

相反,列表(数组)在一个name域被设置为空的地方终止。

inode号为下标的结构被赋值为生成文件。由此,在文件操作函数中,我们可以知道那个文件被打开了。但这种特性也使得列表(数列)第一项不能用,因为0inode节点表示文件系统的根目录。所以,当你创建tree_descr列表(数列)的时候,第一项应该被赋值为NULL

硬着头皮看完上面的东西后,你已经可以按如下方式设置你的四计数器(counter)列表:

static struct tree_descr OurFiles[] = {

    { NULL, NULL, 0 },   /* Skipped */

    { .name = "counter0", /* Inode 1 */

      .ops = &lfs_file_ops,

      .mode = S_IWUSR|S_IRUGO },

    { .name = "counter1", /* Inode 2 */

      .ops = &lfs_file_ops,

      .mode = S_IWUSR|S_IRUGO },

    { .name = "counter2", /* Inode 3 */

      .ops = &lfs_file_ops,

      .mode = S_IWUSR|S_IRUGO },

    { .name = "counter3", /* Inode 4 */

      .ops = &lfs_file_ops,

      .mode = S_IWUSR|S_IRUGO },

    { "", NULL, 0 }  /* Terminates the list */

    };

一旦函数simple_fill_super()返回,我们的任务就完成了,文件系统也构建好了。最后一个细节就是你的open()方法了。如果你有多个文件共享同一个file_operations结构,你就需要弄清楚那个是正在工作的。关键就是inode号,我们可以在i_ino域中找到它。我们新版的lfs_open()函数按如下方式找到正确的计数器(counter):

static int lfs_open(struct inode *inode, struct file *filp)

    {

    if (inode->i_ino > NCOUNTERS)

    return -ENODEV;  /* Should never happen.  */

    filp->private_data = counters + inode->i_ino - 1;

    return 0;

    }

Read()write()函数利用private_data域(位于inode中),所以不需要改变之前的版本。

小结

这里展示的libfs代码对很多特定驱动的虚拟文件系统已经足够了。更多的例子可以在2.5版内核源码的一些地方找到:

drivers/hotplug/pci_hotplug_core.c

drivers/usb/core/inode.c

drivers/oprofile/oprofilefs.c

fs/ramfs/inode.c

fs/nfsd/nfsctl.c (simple_fill_super() example)

……还有其它一些例子,grep可以帮你找到。

你要记住2.6驱动模型让驱动在它们自己的虚拟文件系统中导出资料变得更容易了。对于许多应用来说,这是一种比较好的方式与用户态通信。驱动程序移植系列(Driver Porting Series)还有一些关于驱动模型和sysfs的文章。然而,如果只是想要自定义文件系统,libfs库可以让编码工作相对简单一些。

你可能感兴趣的:(Linux系统架构)