FUSE 的全称是“Filesystem in Userspace”,即“用户空间的文件系统”,这是一个内核模块,能够让用户在用户空间实现文件系统并且挂载到某个目录,就像在内核实现的文件系统一样。使用 FUSE 有几个好处:一是因为在用户空间实现,开发和调试都比较方便;二是可以把一些常用的服务以文件系统的形式展现,方便操作,如 ftpfs,sshfs,mailfs 等;另外可以避免一些版权问题,如 Linux 上对 ntfs,zfs 的操作都是通过 FUSE 实现的。当然用户空间的实现也有缺点,最明显的就是由多次在用户态/内核态切换带来的性能下降。
根据参考资料 [1] 的介绍,用户通过 FUSE 和内核的通信过程如下:
+----------------+
| myfs /tmp/fuse |
+----------------+
| ^
+--------------+ v |
| ls /tmp/fuse | +--------------+
+--------------+ | libfuse |
^ | +--------------+
| v | |
+--------------+ +--------------+
| glibc | | glibc |
+--------------+ +--------------+
^ | | ^
~.~.~.|.~|~.~.~.~.~.~.~.~.|.~.|.~.~.~.~.~.~.~.~.
| v v |
+--------------+ +--------------+
| |----| FUSE |
| | +--------------+
| VFS | ...
| | +--------------+
| |----| Ext3 |
+--------------+ +--------------+
从图中可以看到,FUSE 和 ext3 一样,是内核里的一个文件系统模块。例如我们用 FUSE 实现了一个文件系统并挂载在 /tmp/fuse,当我们对该目录执行 ls 时,内核里的 FUSE 从 VFS 获得参数,然后调用我们自己实现的 myfs 中相应的函数,得到结果后再通过 VFS 返回给 ls。
以下实验的环境是 debian 6,需要安装 libfuse-dev,fuse-utils 及其它相关依赖。
hello, world
下面是一个简单的文件系统 oufs,只支持ls操作:
#define FUSE_USE_VERSION 26
#include <stdio.h>
#include <string.h>
#include <fuse.h>
static int ou_readdir(const char* path, void* buf, fuse_fill_dir_t filler,
off_t offset, struct fuse_file_info* fi)
{
return filler(buf, "hello-world", NULL, 0);
}
static int ou_getattr(const char* path, struct stat* st)
{
memset(st, 0, sizeof(struct stat));
if (strcmp(path, "/") == 0)
st->st_mode = 0755 | S_IFDIR;
else
st->st_mode = 0644 | S_IFREG;
return 0;
}
static struct fuse_operations oufs_ops = {
.readdir = ou_readdir,
.getattr = ou_getattr,
};
int main(int argc, char* argv[])
{
return fuse_main(argc, argv, &oufs_ops, NULL);
}
和编译程序的 Makefile:
CC := gcc
CFLAGS := -g -Wall -D_FILE_OFFSET_BITS=64
OBJS := $(patsubst %.c, %.o, $(wildcard *.c))
LIBS := -lfuse
TARGET := oufs
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^ $(LIBS)
.c.o:
$(CC) $(CFLAGS) -c $<
clean:
rm -f $(TARGET) $(OBJS)
编译成功后会看到生成的可执行文件 oufs。建立一个挂载点 /tmp/mnt,然后运行
./oufs /tmp/mnt
成功后试试“ls /tmp/mnt”,就能看到一个文件“hello-world”。要调试的时候可以加上“-d”选项,这样就能看到 FUSE 和自己 printf 的调试输出。
代码第一行指定了要使用的 FUSE API 版本。这里使用的是 2.6 版本。
要实现 ls 的功能,FUSE 需要提供两个函数:readdir() 和 getattr(),这两个接口是 struct fuse_operations 里的两个函数指针(定义在 /usr/include/fuse/fuse.h):
/** Function to add an entry in a readdir() operation
*
* @param buf the buffer passed to the readdir() operation
* @param name the file name of the directory entry
* @param stat file attributes, can be NULL
* @param off offset of the next entry or zero
* @return 1 if buffer is full, zero otherwise
*/
typedef int (*fuse_fill_dir_t) (void *buf, const char *name,
const struct stat *stbuf, off_t off);
struct fuse_operations {
/** Get file attributes.
*
* Similar to stat(). The 'st_dev' and 'st_blksize' fields are
* ignored. The 'st_ino' field is ignored except if the 'use_ino'
* mount option is given.
*/
int (*getattr) (const char *, struct stat *);
/** Read directory
*
* This supersedes the old getdir() interface. New applications
* should use this.
*
* The filesystem may choose between two modes of operation:
*
* 1) The readdir implementation ignores the offset parameter, and
* passes zero to the filler function's offset. The filler
* function will not return '1' (unless an error happens), so the
* whole directory is read in a single readdir operation. This
* works just like the old getdir() method.
*
* 2) The readdir implementation keeps track of the offsets of the
* directory entries. It uses the offset parameter and always
* passes non-zero offset to the filler function. When the buffer
* is full (or an error happens) the filler function will return
* '1'.
*
* Introduced in version 2.3
*/
int (*readdir) (const char *, void *, fuse_fill_dir_t, off_t,
struct fuse_file_info *);
......
};
这里要实现 getattr() 是因为我们要遍历根目录的内容,要通过 getattr() 获取根目录权限等信息。getattr() 实现类似 stat() 的功能,返回相关的信息如文件权限,类型,uid 等。参数里的 path 有可能是文件或目录或软链接或其它,因此除了权限外还要填充类型信息。程序里除了根目录就只有一个文件 hello-world,因此只有目录(S_IFDIR)和普通文件(S_IFREG)两种情况。使用“ls -l”查看 /tmp/mnt 下的内容可以发现,hello-world 的 link 数,修改时间等都是错误的,那是因为 ou_getattr() 实现中没有填充这些信息。查看 manpage 可以知道 stat 都有哪些字段。
readdir() 的参数 fuse_fill_dir_t 是一个函数指针,每次往 buf 中填充一个 entry 的信息。程序中的 ou_readdir() 采用了注释中的第一种模式,即把所有的 entry 一次性放到 buffer 中。如果 entry 较多,把 readdir() 中的 offset 传给 filler 即可,buffer 满了 filler 返回 1。
最后在 main() 函数中调用的是 fuse_main(),把文件系统的操作函数和参数(如这里的挂载点 /tmp/mnt)传给 FUSE。
创建/删除文件
接下来让我们扩充上面的程序,增加创建/删除普通文件的功能:
#define FUSE_USE_VERSION 26
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fuse.h>
#include "list.h"
#define MAX_NAMELEN 255
struct ou_entry {
mode_t mode;
struct list_node node;
char name[MAX_NAMELEN + 1];
};
static struct list_node entries;
static int ou_readdir(const char* path, void* buf, fuse_fill_dir_t filler,
off_t offset, struct fuse_file_info* fi)
{
struct list_node* n;
filler(buf, ".", NULL, 0);
filler(buf, "..", NULL, 0);
list_for_each (n, &entries) {
struct ou_entry* o = list_entry(n, struct ou_entry, node);
filler(buf, o->name, NULL, 0);
}
return 0;
}
static int ou_getattr(const char* path, struct stat* st)
{
struct list_node* n;
memset(st, 0, sizeof(struct stat));
if (strcmp(path, "/") == 0) {
st->st_mode = 0755 | S_IFDIR;
st->st_nlink = 2;
st->st_size = 0;
list_for_each (n, &entries) {
struct ou_entry* o = list_entry(n, struct ou_entry, node);
++st->st_nlink;
st->st_size += strlen(o->name);
}
return 0;
}
list_for_each (n, &entries) {
struct ou_entry* o = list_entry(n, struct ou_entry, node);
if (strcmp(path + 1, o->name) == 0) {
st->st_mode = o->mode;
st->st_nlink = 1;
return 0;
}
}
return -ENOENT;
}
static int ou_create(const char* path, mode_t mode, struct fuse_file_info* fi)
{
struct ou_entry* o;
struct list_node* n;
if (strlen(path + 1) > MAX_NAMELEN)
return -ENAMETOOLONG;
list_for_each (n, &entries) {
o = list_entry(n, struct ou_entry, node);
if (strcmp(path + 1, o->name) == 0)
return -EEXIST;
}
o = malloc(sizeof(struct ou_entry));
strcpy(o->name, path + 1); /* skip leading '/' */
o->mode = mode | S_IFREG;
list_add_prev(&o->node, &entries);
return 0;
}
static int ou_unlink(const char* path)
{
struct list_node *n, *p;
list_for_each_safe (n, p, &entries) {
struct ou_entry* o = list_entry(n, struct ou_entry, node);
if (strcmp(path + 1, o->name) == 0) {
__list_del(n);
free(o);
return 0;
}
}
return -ENOENT;
}
static struct fuse_operations oufs_ops = {
.getattr = ou_getattr,
.readdir = ou_readdir,
.create = ou_create,
.unlink = ou_unlink,
};
int main(int argc, char* argv[])
{
list_init(&entries);
return fuse_main(argc, argv, &oufs_ops, NULL);
}
代码中用到的链表操作 list.h 见附录。本来想用 c++ 的 list,但是用 g++ 编译时出错了,搜错误信息的时候发现了一个 gcc的bug。多嘴插一句,clang3 非常好用,错误提示非常人性化,谁用谁知道。
新增了两个函数 ou_create() 和 ou_unlink(),分别用于创建和删除文件;增加了一个全局变量 entries 用于保存所有的 entry;ou_readdir() 和 ou_getattr() 也做了相应的修改。由于这些函数相当于实现 VFS 中的接口,因此不能在错误时返回 -1 了事,而是要根据不同的错误类型返回不同的值。关于错误代码头文件为 errno.h,值和含义在 /usr/include/asm-generic/errno-base.h 和 /usr/include/asm-generic/errno.h 有定义。
编译挂载后试试执行“touch /tmp/mnt/abc”,然后 ls 看看是否多了一个文件 abc?
其它
参考资料 [1] 中有一个 helloworld 的例子,参考资料 [2] 是一个 FUSE 教程,有兴趣的可以看看;关于 FUSE 的 API 可以参考头文件,所有接口的说明都在里面。
参考资料
[1] Filesystem in Userspace
[2] Writing a FUSE Filesystem: a Tutorial
[3] FUSE Design Doc version 0.6
附录
程序中用到的 list.h(相关原理可以参考 这里):
#ifndef __LIST_H__
#define __LIST_H__
/* doubly linked list implementation from linux kernel */
#define offset_of(type, member) \
((unsigned long)(&(((type*)0)->member)))
#define container_of(ptr, type, member) \
((type*)((unsigned long)(ptr) - offset_of(type, member)))
#define list_entry(ptr, type, member) container_of(ptr, type, member)
struct list_node {
struct list_node *prev, *next;
};
static inline void list_init(struct list_node* list)
{
list->prev = list;
list->next = list;
}
static inline void list_add_prev(struct list_node* node,
struct list_node* next)
{
node->next = next;
node->prev = next->prev;
next->prev = node;
node->prev->next = node;
}
static inline void __list_del(struct list_node* node)
{
node->prev->next = node->next;
node->next->prev = node->prev;
}
static inline void list_del(struct list_node* node)
{
__list_del(node);
list_init(node);
}
#define list_for_each(p, head) \
for ((p) = (head)->next; (p) != (head); (p) = (p)->next)
#define list_for_each_safe(p, n, head) \
for ((p) = (head)->next, (n) = (p)->next; \
(p) != (head); \
(p) = (n), (n) = (p)->next)
#endif
最近在做一个FUSE项目。有好多的问题没有头绪,网上也搜不到多少FUSE的东西个。能方便加下你的QQ 或者 啥联系方式吗 。。 感激不尽。。真的谢谢了
你好,问个问题哈?
我用你的代码一直编译不成功,这是什么原因?
有一大版错误, 在libio.h stdio.h 里面,还有在 像 fuse_fill_dir_t 很多声明也没找到…
ubuntu12.10 gcc 编译~~~
先把依赖都装上
sudo apt-get install libfuse-dev fuse-utils build-essential make
编译时要把与fuse相关的编译选项都加上(参考上面的Makefile)
已经弄好了~~谢谢哈~~主要是相应的相关编译项有些闹不明白还要再看看,自己直接GCC了~~~T_T
您好,我想问下怎么调试,你说的 -d 是在哪里加?