FUSE的使用及示例

背景

文件系统,是任何OS都不可却少的。想要编写一个属于自己的文件系统很容易,但调试十分不方便。为了方便调试,提高开发效率,可以使用FUSE(Filesystem in userspace)框架进行开发。这是一个内核模块,能够让用户在用户空间实现文件系统并且挂载到某个目录,就像在内核实现的文件系统一样。使用 FUSE 有几个好处:一是因为在用户空间实现,开发和调试都比较方便;二是可以把一些常用的服务以文件系统的形式展现,方便操作,如 ftpfs,sshfs,mailfs 等;另外可以避免一些版权问题,如 Linux 上对 ntfs,zfs 的操作都是通过 FUSE 实现的。当然用户空间的实现也有缺点,最明显的就是由多次在用户态/内核态切换带来的性能下降。

FUSE架构

FUSE Frame
将文件系统需要处理的读、写、创建、删除等函数,以回调的方式注册到FUSE模块中,当用户访问挂载目录时,FUSE模块回调相应的注册接口。

FUSE安装

ubuntu 16.04 安装

需要安装libfuse2libfuse-dev

1
sudo apt-get install libfuse2 libfuse-dev pkg-config

版本:

1
2
3
FUSE library version: 2.9.4
fusermount version: 2.9.4
using FUSE kernel interface version 7.19

OSX 10.11.6 安装

需要安装Caskroom/cask/osxfuse

1
brew cask install osxfuse

版本:

1
2
3
OSXFUSE 3.5.4
FUSE library version: 2.9.7
fuse: no mount point

FUSE使用

FUSE有两种接口,一种是fuse_operations另一种是fuse_lowlevel_ops

  • fuse_operations是较为上层的接口,我们可以使用fuse_main函数将其传入FUSE中
    eg:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    static struct fuse_operations cryptfs_ops = {
    .init = cfs_init,
    .destroy = cfs_destroy,
    .open = cfs_open,
    .read = cfs_read,
    .write = cfs_write,
    .release = cfs_release,
    .readdir = cfs_readdir,
    .getattr = cfs_getattr,
    };
  • fuse_lowlevel_ops是较底层的接口,我们可以使用fuse_session_loop函数实现
    eg:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    static struct fuse_lowlevel_ops  lowlevel_handler =   
    {
    .lookup = lowlevel_lookup,
    .getattr = lowlevel_getattr,
    .readdir = lowlevel_readdir,
    .mkdir = lowlevel_mkdir,
    .rmdir = lowlevel_rmdir,
    .open = lowlevel_open,
    .read = lowlevel_read,
    .write = lowlevel_write,
    .unlink = lowlevel_unlink,
    .rename = lowlevel_rename,
    };

fuse_operations使用简单,容易上手。fuse_lowlevel_ops灵活性大,需要有FS开发经验。

Helloworld示例

实现ls ./mp系统提示Hello-world(./mp为挂载目录)
ls会用到的方法代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <string.h>
#include <fuse.h>

static int cfs_readdir(const char* path, void* buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info* fi)
{
fprintf(prd->logfile, "cfs_readdir\t path : %s\n", path);
fflush(prd->logfile);

return filler(buf, "Hello-world", NULL, 0);
}

static int cfs_getattr(const char* path, struct stat *stbuf)
{
fprintf(prd->logfile, "cfs_getattr\t path : %s\n", path);
fflush(prd->logfile);
if(strcmp(path, "/") == 0)
stbuf->st_mode = 0755 | S_IFDIR;
else
stbuf->st_mode = 0644 | S_IFREG;
return 0;
}

fuse_main将接口注册到FUSE中:

1
2
3
4
5
6
7
8
9
10
11
static struct fuse_operations cryptfs_ops = {
.readdir = cfs_readdir,
.getattr = cfs_getattr,
};

int main(int argc, char *argv[])
{
int ret = 0;
ret = fuse_main(argc, argv, &cryptfs_ops);
return ret;
}

编译

ubuntu 16.04

编译命令:

1
gcc myfuse.c -o myfuse -DFUSE_USE_VERSION=22 `pkg-config fuse --cflags --libs` -g

or

1
gcc myfuse.c -o myfuse -DFUSE_USE_VERSION=22 -D_FILE_OFFSET_BITS=64 -I/usr/include/fuse -lfuse -pthread -g

使用后者编译时不需要安装包pkg-config,由于不同操作系统fuse安装位置不一样,所以还是推荐使用pkg-config的方式

调试

可使用GDB调试,myfuse会以守护进程方式启动,调试有三种方法。

  • myfuse进程启动后,使用GDB中的attach方法进行调试
  • 使用GDB调试myfuse,参数中增加-d参数,该参数可以使myfuse不已守护进程方式启动 ( Version: 2.9.9上无法实现 )
  • myfuse中增加调试打印信息进行调试

挂载FUSE

先创建一个目录(mkdir ./mp),用于挂载myfuse文件系统,然后调用myfuse进行挂载。
eg:

1
sudo ./myfuse ./mp

对于mac系统挂载文件系统需要使用参数-o allow_other,否则挂在后,挂载目录将无权访问。
eg:

1
sudo ./myfuse ./mp -o allow_other

使用mount查看挂载的文件系统。
eg:

1
2
3
$ mount
...
/home/xxxx/xxxx/myfuse on /home/xxxx/xxxx/mp type fuse.myfuse (rw,nosuid,nodev,relatime,user_id=1000,group_id=1000)

使用ls ./mp查看挂载点内容,根据实现应提示Hello-world
eg:

1
2
$ ls ./mp/
Hello-world

卸载FUSE

使用umount卸载myfuse文件系统,需要输入挂载点的全路径
eg:

1
sudo umount /home/xxxx/xxxx/mp

FUSE Options说明

fuse_main函数自带usage信息,只需要将--help参数传入,便能将usage信息打出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
$ ./myfuse --help
usage: ./myfuse mountpoint [options]

general options:
-o opt,[opt...] mount options
-h --help print help
-V --version print version

FUSE options:
-d -o debug enable debug output (implies -f)
-f foreground operation
-s disable multi-threaded operation

-o allow_other allow access to other users
-o allow_root allow access to root
-o auto_unmount auto unmount on process termination
-o nonempty allow mounts over non-empty file/dir
-o default_permissions enable permission checking by kernel
-o fsname=NAME set filesystem name
-o subtype=NAME set filesystem type
-o large_read issue large read requests (2.4 only)
-o max_read=N set maximum size of read requests

-o hard_remove immediate removal (don't hide files)
-o use_ino let filesystem set inode numbers
-o readdir_ino try to fill in d_ino in readdir
-o direct_io use direct I/O
-o kernel_cache cache files in kernel
-o [no]auto_cache enable caching based on modification times (off)
-o umask=M set file permissions (octal)
-o uid=N set file owner
-o gid=N set file group
-o entry_timeout=T cache timeout for names (1.0s)
-o negative_timeout=T cache timeout for deleted names (0.0s)
-o attr_timeout=T cache timeout for attributes (1.0s)
-o ac_attr_timeout=T auto cache timeout for attributes (attr_timeout)
-o noforget never forget cached inodes
-o remember=T remember cached inodes for T seconds (0s)
-o nopath don't supply path if not necessary
-o intr allow requests to be interrupted
-o intr_signal=NUM signal to send on interrupt (10)
-o modules=M1[:M2...] names of modules to push onto filesystem stack
-o max_write=N set maximum size of write requests
-o max_readahead=N set maximum readahead
-o max_background=N set number of maximum background requests
-o congestion_threshold=N set kernel's congestion threshold
-o async_read perform reads asynchronously (default)
-o sync_read perform reads synchronously
-o atomic_o_trunc enable atomic open+truncate support
-o big_writes enable larger than 4kB writes
-o no_remote_lock disable remote file locking
-o no_remote_flock disable remote file locking (BSD)
-o no_remote_posix_lock disable remove file locking (POSIX)
-o [no_]splice_write use splice to write to the fuse device
-o [no_]splice_move move data while splicing to the fuse device
-o [no_]splice_read use splice to read from the fuse device

FUSE Operations说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
struct fuse_operations {
int (*getattr) (const char *, struct stat *);
/* 这个函数与 stat() 类似。st_dev 和 st_blksize 域都可以忽略。st_ino 域也会被忽略,除非在执行 mount 时指定了 use_ino 选项 */

int (*readlink) (const char *, char *, size_t);
/* 这个函数会读取一个符号链接的目标。缓冲区应该是一个以 null 结束的字符串。缓冲区的大小参数包括这个 null 结束字符的空间。如果链接名太长,不能保存到缓冲区中,就应该被截断。成功时的返回值应该是 “0” */

int (*getdir) (const char *, fuse_dirh_t, fuse_dirfil_t);
/* 这个函数会读取一个目录中的内容。这个操作实际上是在一次调用中执行 opendir()、readdir()、...、closedir() 序列。对于每个目录项来说,都应该调用 filldir() 函数 */

int (*mknod) (const char *, mode_t, dev_t);
/* 这个函数会创建一个文件节点。此处没有 create() 操作;mknod() 会在创建非目录、非符号链接的节点时调用 */

int (*mkdir) (const char *, mode_t);
int (*rmdir) (const char *);
/* 这两个函数分别用来创建和删除一个目录 */

int (*unlink) (const char *);
int (*rename) (const char *, const char *);
/* 这两个函数分别用来删除和重命名一个文件 */

int (*symlink) (const char *, const char *);
/* 这个函数用来创建一个符号链接 */

int (*link) (const char *, const char *);
/* 这个函数创建一个到文件的硬链接 */

int (*chmod) (const char *, mode_t);
int (*chown) (const char *, uid_t, gid_t);
int (*truncate) (const char *, off_t);
int (*utime) (const char *, struct utimbuf *);
/* 这 4 个函数分别用来修改文件的权限位、属主和用户、大小以及文件的访问/修改时间 */

int (*open) (const char *, struct fuse_file_info *);
/* 这是文件的打开操作。对 open() 函数不能传递创建或截断标记(O_CREAT、O_EXCL、O_TRUNC)。这个函数应该检查是否允许执行给定的标记的操作。另外,open() 也可能在 fuse_file_info 结构中返回任意的文件句柄,这会传递给所有的文件操作 */
int (*read) (const char *, char *, size_t, off_t, struct fuse_file_info *);
/* 这个函数从一个打开文件中读取数据。除非碰到 EOF 或出现错误,否则 read() 应该返回所请求的字节数的数据;否则,其余数据都会被替换成 0。一个例外是在执行 mount 命令时指定了 direct_io 选项,在这种情况中 read() 系统调用的返回值会影响这个操作的返回值 */

int (*write) (const char *, const char *, size_t, off_t,struct fuse_file_info *);
/* 这个函数将数据写入一个打开的文件中。除非碰到 EOF 或出现错误,否则 write() 应该返回所请求的字节数的数据。一个例外是在执行 mount 命令时指定了 direct_io 选项(这于 read() 操作的情况类似) */

int (*statfs) (const char *, struct statfs *);
/* 这个函数获取文件系统的统计信息。f_type 和 f_fsid 域都会被忽略 */

int (*flush) (const char *, struct fuse_file_info *);
/* 这表示要刷新缓存数据。它并不等于 fsync() 函数 —— 也不是请求同步脏数据。每次对一个文件描述符执行 close() 函数时,都会调用 flush();因此如果文件系统希望在 close() 中返回写错误,并且这个文件已经缓存了脏数据,那么此处就是回写数据并返回错误的好地方。由于很多应用程序都会忽略 close() 错误,因此这通常用处不大 */

int (*release) (const char *, struct fuse_file_info *);
/* 这个函数释放一个打开文件。release() 是在对一个打开文件没有其他引用时调用的 —— 此时所有的文件描述符都会被关闭,所有的内存映射都会被取消。对于每个 open() 调用来说,都必须有一个使用完全相同标记和文件描述符的 release() 调用。对一个文件打开多次是可能的,在这种情况中只会考虑最后一次 release,然后就不能再对这个文件执行更多的读/写操作了。release 的返回值会被忽略 */

int (*fsync) (const char *, int, struct fuse_file_info *);
/* 这个函数用来同步文件内容。如果 datasync 参数为非 0,那么就只会刷新用户数据,而不会刷新元数据 */

int (*setxattr) (const char *, const char *, const char *, size_t, int);
int (*getxattr) (const char *, const char *, char *, size_t);
int (*listxattr) (const char *, char *, size_t);
int (*removexattr) (const char *, const char *);
/* 这些函数分别用来设置、获取、列出和删除扩展属性 */
......
};

更多详细说明,请见fuse.h文件。

参考&鸣谢