纲要:
0. 简介
1. FUSE的下载, 安装, 参考资料来源
2. 带FUSE的程序的总体分析以及编译方法
3. 遇到的问题, 解决方案和注意事项
简介
FUSE,,全称Filesystem in Userspace。从名字上看,并不怎么容易理解,其中有一个意义模糊的词Userspace。我以为,此处的User,是相对于kernel而言的。对于一个传统的文件系统,往往是从内核的层面上对一个文件系统进行支持,比如一些和文件操作相关的系统调用(例如mkdir,open等)都是在内核的层次上实现,从而完成对文件的相关操作。一方面,内核态的代码难以调试,开发效率低;另一方面,如果开发一个文件系统就需要程序员对于操作系统的内核了然于胸,导致开发成本进一步提高。而FUSE这个库,把内核关于文件的操作封装起来,转化成一组相对简单的API供开发人员直接使用。这样,程序员不必深入了解操作系统内核就可以开发文件系统了。
下载安装
FUSE是sourceforge上的一个开源项目,可以免费的下载源代码并使用FUSE进行二次开发。
在sourceforge的FUSE页面(http://fuse.sourceforge.net/ )可以获取到FUSE的源码压缩包。将压缩包解压缩,里面主要有这些东西:doc文件夹,里面有一个kernel.txt,对整个FUSE进行了总体上的介绍;example文件夹,里面包含了几个例子,其中对初学者比较有参考价值的,一个是hello,一个是fusexmp,在下文中会进一步介绍;include文件夹,包含了FUSE的头文件,其中比较重要的是fuse.h,一方面使用FUSE开发文件系统时只要包含这个头文件就可以完成大部分工作,另一方面这个文件中声明了FUSE最核心的API,并且在注释中有一些说明,起到了文档的效果;lib文件夹,包含了FUSE的实现,如果仅仅是使用FUSE开发而不是研究FUSE的源码的话这里就可以略过了;FAQ文件,里面包含了一些常见的问题和解决方案;README里面包含了一些介绍信息;Filesystems包含了一些用FUSE实现的文件系统的网站链接。学习使用FUSE,主要的参考资料便是上面的这些内容。
安装的话,configure + make + make install即可,和在linux上安装一般的程序没什么区别。
在FUSE主页上就有关于安装的简要介绍,另外在安装包中有一个名字为INSTALL的文本文件,里面详细的介绍了安装过程,这里不再赘述了。
如何使用
首先,从FUSE提供的例子入手。经过了make之后, FUSE自带的例子都编译出了可执行程序。比如hello这个例子,在fuse/example/目录下新建目录tmp/然后运行 ./hello tmp/ 这样就将这个hello文件系统挂载到tmp/这个目录上了。打开这个目录(在终端中使用ls命令或者使用图形化的文件管理工具都可以),可以看到tmp目录下有一个hello文件,用文本编辑器打开这个文件,可以看到里面的字符串“helloworld”。由于这仅仅是一个最简单的示例性的文件系统,有些基本功能并不完备,比如hello文件中的内容不能被修改(修改了也没有办法保存,因为没有实现相应的操作。实际上,hello这个文件中的内容是在源码中直接写进去的)。如果要尝试更多的操作,可以使用example中fusexmp这个例子。这个例子是把根目录挂载到挂载点上,里面的操作也提供的比较完备。
下面简要分析下hello这个例子的源码。源码在hello.c这个文件中,里面只有不到100行C源码。
整体看起来是这样的:最开始处包含了fuse.h这个头文件,然后定义了一些函数。之后定义了一个fuse_operations结构,虽然此处的语法有些晦涩,但是也不难猜出fuse_operations的成员是一些函数指针,然后使用上面实现的函数对这些函数指针进行赋值。最后就是main函数,main函数把所有的工作都交给了fuse_main进行处理了。
那么主要的核心就集中在fuse_operations结构上了。打开fuse.h文件,可以看到fuse_operations是长成这个样子的:里面有很多函数指针。每一个函数指针都对应着一个和文件系统相关的基本操作。程序员为这些接口提供能够操作自己文件系统的具体实现,从而将自己的文件系统和操作系统对接到一起。举个例子,比如在终端中使用ls命令列出目录中的文件,那么如果路径指向的是操作系统自身文件系统中的目录,那么实际上调用的是系统中原有的系统调用readdir;如果路径指向的是程序员自己的文件系统的目录,实际上调用的就是fuse_operations结构中的readdir函数指针指向的函数。每个函数指针的实现规格(输入,输出,注意事项)虽然在注释中有所体现,但是过于简要,很多地方还是需要对操作系统有一定的熟悉程度才会容易理解一些。
总体来看,使用FUSE开发文件系统,是这样的一个过程:定义一个fuse_operations结构,为这个结构的成员提供符合自己需求的实现即可。
遇到的问题,解决方案和注意事项
使用FUSE的程序的编译
通过configure生成的makefile文件,直接make即可将FUSE中所有的example都进行编译。但是如果要单独编译其中的某个例子,或者编译自己写好的文件系统,按照文档的说法是这样的:
Gcc -Wall `pkg-config fuse --cflags --libs` hello.c -o hello
注意“ ` ”是键盘左上角,数字1左边的那个按键,而不是单引号。但实际上使用这个命令编译时会报链接错误:“fuse_main_real()函数找不到实现……”之类的。这是一个很典型的链接错误,说明编译器只看到了函数的声明而没有找到这个函数的实现。实际上这个函数的实现在helper.c中,并且被编译到了相应的链接库中。究其原因,其实是gcc的命令行参数传入的顺序问题。`pkg-config fuse --cflags --libs`这个东西,可以把它理解成一个在fuse安装时定义的变量,可以在终端中输入echo `pkg-config fuse --cflags --libs`即可看到这个变量展开之后所表示的gcc的命令行参数,应该是这种形式“-D_FILE_OFFSET_BITS64 -I/usr/includefuse -pthread -lfuse -lrt=”。其中包含了一个-lfuse命令,它的作用是链接fuse相应的链接库,也就是告诉编译器去哪里能找到fuse_main_real()的实现。事实上gcc要求链接库的命令行参数要放在源代码文件之后,所以将上面的命令改成:
Gcc -Wall hello.c -o hello `pkg-config fuse --cflags --libs`
即可顺利通过编译。关于gcc的参数说明,具体详情参见gcc的官方文档,此处不再引用。
使用g++编译FUSE程序
FUSE除了提供C语言的API,还为多种语言提供了编程接口,比如Java,C#,Python等(详见FUSE主页上language binding)。对于C++来说,FUSE也提供了C++风格的API,但实际上由于C和C++的兼容性,C++也可以直接使用C的API,但是需要做出少许的修改。例如像结构体的指定初始化这种C99标准中的语法(也就是hello.c中fuse_operation结构体函数指针成员赋值的晦涩语法),g++是不能编译通过的。将其修改成在全局定义一个fuse_operation对象,然后在main函数中对这个fuse_operation对象的成员依次赋值即可。其余的命令行参数都和gcc相同了。
FUSE的一个非常有用的命令行参数-d
通过实现一些自定制的函数和FUSE提供的API对接起来从而可以完成整个文件系统的实现。那么怎样知道FUSE的某个API什么时候会被调用呢?或者说实现了一个API之后怎样测试它是否正确?这里就用到了FUSE的一个命令行参数-d。在使用FUSE程序进行挂载的时候,同时使用-d参数,这样在对自己的文件系统进行操作的时候就会在终端中打印一些调试信息。此时最好使用终端通过命令行的方式操作文件系统,如果使用图形化文件管理工具的话,可能一个操作就会打印几十甚至上百条调试信息,分析起来会很麻烦。调试信息中主要包括这样的信息:一个当前操作的序号,操作的名称(虽然和提供的API并不是一一对应,但大部分是和API具有相同名字),输入输出的数据量的大小以及该操作失败时的错误返回码。关于这些调戏信息的具体规格,并没有找到相关的详细说明,所幸大部分信息都不难理解。
FUSE程序的调试
关于FUSE的调试确实没有想到什么很好的方法。由于FUSE程序的特殊性质以及自身缺少系统级别的编程经验,没用成功的使用gdb对FUSE的代码直接进行跟踪。FUSE的官网上也推荐了一些系统调试工具,由于精力有限也没有一一尝试。一个简洁有效的方法是通过使用cerr打印一些调试信息,并且配合assert进行定位。另一方面,对自己的文件系统的接口部分进行充分的单元测试,这样可以大大降低和FUSE对接之后的调试成本。另外在当FUSE的接口只完成一部分的时候,对这些接口单独测试可能会出现一些很莫名奇妙的bug,比如我就遇到了这样的问题:实现了getattr和readdir命令后,在终端中使用ls命令,打印的文件和目录名有时候会多出一些多余的符号,有时候又会报“IO错误”。幸运的是,当我对FUSE的其他API进一步完善之后,这个问题神奇的自动消失了。好吧,首先要承认带着bug还继续添加代码是一件非常糟糕的事情,但是FUSE的所有API是一个整体,而终端在执行命令的过程中究竟在后面都做了什么事情,我们也不得而知。总之,我要说明的是,如果使用FUSE的过程中出现了一些和预期不符的东西,首先排除它不是由文件系统自身带来的,然后再确定对FUSE的使用方式是否恰当,然后确定测试思路是否正确。