fuse接口用法说明
fuse介绍
fuse即Filesystem in Userspace,用户空间文件系统,可以在应用程序中实现文件系统,能够在用户态使用标准的文件操作,如cat、ls、grep、重定向等功能,虽然效率比内核态要低,但胜在方便。很多场景下用起来还是非常方便的。
fuse本身是内核提供的一个功能,内核开启fuse支持后,会在/dev目录下,生成fuse设备节点,应用层可以通过该设备节点完成用户态文件系统。
libfuse是一个对fuse功能封装的库,提供一系列的api,使用户可以更方便、更简单的使用fuse功能,后面的内容都是基于libfuse来说明的。
目前个人有嵌入式设备上使用fuse的场景,打造一个应用层的proc系统,使用libuse实现起来还是非常方便的,下面主要介绍fuse的基本用法。
libfuse用的还是比较广,它的主页上介绍了一些基于libfuse打造的文件系统,但这个库文档不是很多,文档都是doxygen生成的,也就是说看官方文档,和看头文件差不多,代码里面的注释还是很全面的,实际使用的过程中,看注释也能用,但有些地方还是不容易弄明白。
libfuse的编译
fuse需要内核支持,编译内核时,需把fuse功能选上,fuse可以编译成模块:
File systems —>
< > FUSE (Filesystem in Userspace ) support
fuse驱动加载完成后,可以在/dev/看到创建了一个名为fuse的设备文件,fuse即通过此设备来中转用户态与内核间的信息。
libfuse-fuse-3.2.6.tar.gz需使用meson-0.47.1.tar.gz进行编译,meson又依赖python等,由于个人需求与libfuse版本关系不大,所有用了libfuse-fuse-2.9.8.tar.gz版本
编译过程:
sw@sw:libfuse-fuse-2.9.8$ ./makeconf.sh
sw@sw:libfuse-fuse-2.9.8$ ./configure --host=arm-linux
sw@sw:libfuse-fuse-2.9.8$ make -j4
编译完成后,在lib/.lib/目录下生成了libfuse.a,头文件在include目录下,示例程序在example目录下。
如要查看编译选项,可以使用make V=1编译,这样就可以看到编译选项。
如果需要增加或修改编译选项(如嵌入式可以把O2改成Os,减少库大小),可以在configure.ac中增加,如:
CFLAGS="-Wall -W -Wno-sign-compare -Wstrict-prototypes -Wmissing-declarations -Wwrite-strings -g -O2 -fno-strict-aliasing -ffunction-sections -fdata-sections "
这里增加了 -ffunction-sections -fdata-sections,为了减少应用程序大小(效果不明显),大家可以根据要求改编译选项。
示例程序说明
libfuse提供了几个示例程序,都在example目录下:
- hello.c :最简单的libfuse用法,基本api用法
- hello_ll.c :ll是low level的缩写,展示了低层次接口的基本用法
- null.c :实现了一个类似linux下的/dev/null的设备
- fioc.c/fioclient.c :fioc是fuse ioctl的缩写,这个例子展示的就像很多驱动一样,能够使用ioctl接口进行交互
- fsel.c/fselclient.c : fsel是fuse select缩写,比较高级的用法。
- cusexmp.c/fusexmp.c/fusexmp_fh.c : 这几个例子都是文件映射相关的,有一定的实用意义,实现的接口较全面。
验证测试程序,这里只是看一下基本用法,进到example目录下,先确认一下设备存在
sw@sw:example$ ls /dev/fuse
/dev/fuse
sw@sw:example$ mkdir hfs
sw@sw:example$ ./hello hfs
sw@sw:example$ ls -l hfs/
total 0
-r–r--r-- 1 root root 13 1月 1 1970 hello
sw@sw:example$ cat hfs/hello
Hello World!
使用ps可以看到./hello hfs是在后台运行的,此时hfs不能操作(如进到这个目录创建文件),如想删除hfs文件夹,需先umount
sw@sw:example$ sudo umount hfs
如果umount之前,进程被杀,对该文件操作会报错:Transport endpoint is not connected,此时umount一下就可以了。
这个hello程序也可以前台运行,libfuse支持很多选项,先看一下:
sw@sw:example$ ./hello -h
usage: /home/sw/mypro/libfuse-fuse-2.9.8/example/.libs/lt-hello mountpoint [options]general options:
-o opt,[opt…] mount options
-h --help print help
-V --version print versionFUSE 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
…
libfuse支持很多选项,如
- -o 这个选项就像mount的-o选项一样,可以定义一些挂载选项,
- -d 这是个调试选项,这个选项建议在开发阶段都加上,对调试帮助很大
- -f 这个就是让程序运行到前台的选项,如不使用fuse_main这种最简单的api,选项用处就不大
加上参数-d与-f再次运行hello程序,如下:
sw@sw:example$ ./hello hfs -d -f
FUSE library version: 2.9.8
nullpath_ok: 0
nopath: 0
utime_omit_ok: 0
unique: 1, opcode: INIT (26), nodeid: 0, insize: 56, pid: 0
INIT: 7.23
flags=0x0003f7fb
max_readahead=0x00020000
INIT: 7.19
flags=0x00000011
max_readahead=0x00020000
max_write=0x00020000
max_background=0
congestion_threshold=0
unique: 1, success, outsize: 40unique: 2, opcode: GETATTR (3), nodeid: 1, insize: 56, pid: 11375
getattr /
unique: 2, success, outsize: 120
unique: 3, opcode: GETATTR (3), nodeid: 1, insize: 56, pid: 11379
getattr /
unique: 3, success, outsize: 120
unique: 4, opcode: OPENDIR (27), nodeid: 1, insize: 48, pid: 11379
unique: 4, success, outsize: 32
unique: 5, opcode: READDIR (28), nodeid: 1, insize: 80, pid: 11379
readdir[0] from 0
…
可以看到,有一些调试信息,在其他终端cat一下hello文件,会看到各种opcode(operate code操作码),可以根据此调试信息来决定代码中某接口是否需实现。
此时再ctrl+c退出程序,再用ps查看,可以看到程序已退出了,这就是前台运行。
大家如果有调试测试程序的需求(适用gdb跟踪,熟悉api用法,或看源码),要注意了,测试程序都是用libtool编译的,生成的都是脚本,不能直接调试,大家可以make V=1查看编译选项,如下:
/bin/sh …/libtool --tag=CC --mode=link gcc -Wall -W -Wno-sign-compare -Wstrict-prototypes -Wmissing-declarations -Wwrite-strings -g -O2 -fno-strict-aliasing -o hello hello.o …/lib/libfuse.la
改成
gcc -Wall -W -Wno-sign-compare -Wstrict-prototypes -Wmissing-declarations -Wwrite-strings -g -O2 -fno-strict-aliasing -o hello hello.o …/lib/.libs/libfuse.a -lpthread -ldl
这样就可以编译出hello可执行程序了,需链接pthread和dl库。
libfuse的api使用
libfuse提供了3套不同层次的接口,大家可以根据自己情况选用。
简单(lazy)接口
前面hello例子中用的fuse_main就是最简单的接口,只需要实现一个struct fuse_operations类型的操作函数,就可以完成一个用户态文件系统。用法简单,此接口退出控制比较麻烦,默认捕获了SIGHUP、SIGINT、SIGTERM、SIGPIPE等几个信号,收到这几个信号就退出,不灵活。一般用来功能验证。
基本接口
在fuse.h还提供了一套基本接口,此接口能满足一般程序需求,退出控制较完善,只是初始化去初始化不同,操作函数编程与简单接口一致,适用于简单的应用。函数调用方式与说明如下:
/* fuse_parse_cmdline函数不是必须的,mountpoint可以直接写死目录,可以不从参数里获取 */
int fuse_parse_cmdline(struct fuse_args *args, char **mountpoint, int *multithreaded, int *foreground);
/* 创建挂载点,args可以为NULL,返回的是一个fuse所谓的channel */
struct fuse_chan *fuse_mount(const char *mountpoint, struct fuse_args *args);
/* 创建fuse操作函数,这里可以传入用户数据 */
struct fuse *fuse_new(struct fuse_chan *ch, struct fuse_args *args,const struct fuse_operations *op, size_t op_size,void *user_data);
/* 循环,这个是阻塞的,可以通过fuse_exit退出 */
int fuse_loop(struct fuse *f);
/* 释放以上函数资源 */
void fuse_exit(struct fuse *f);
void fuse_unmount(const char *mountpoint, struct fuse_chan *ch);
void fuse_destroy(struct fuse *f);
void fuse_opt_free_args(struct fuse_args *args);
示例程序里并没有基本接口的用法,这里在hello的基础上改了一个测试程序,大家可以参考:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#define FUSE_USE_VERSION 26
#include "fuse.h"
int user_data = 2018;
static const char *hello_str = "Hello World!";
static const char *hello_path = "/hello";
void thread_fuse(void * arg)
{
struct fuse * fuse = arg;
fuse_loop(fuse);
}
static int api_getattr(const char *path, struct stat *stbuf)
{
int res = 0;
memset(stbuf, 0, sizeof(struct stat));
if (strcmp(path, "/") == 0)
{
stbuf->st_mode = S_IFDIR | 0755;
stbuf->st_nlink = 2;
}
else if (strcmp(path, hello_path) == 0)
{
stbuf->st_mode = S_IFREG | 0444;
stbuf->st_nlink = 1;
/* 注意,如不清楚具体长度,这个长度需写长一点,否则无法输出 */
stbuf->st_size = 1024;
} else
res = -ENOENT;
return res;
}
static int api_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
off_t offset, struct fuse_file_info *fi)
{
if (strcmp(path, "/") != 0)
return -ENOENT;
filler(buf, ".", NULL, 0);
filler(buf, "..", NULL, 0);
filler(buf, hello_path + 1, NULL, 0);
return 0;
}
static int api_open(const char *path, struct fuse_file_info *fi)
{
if ((fi->flags & 3) != O_RDONLY)
return -EACCES;
/* 参考fuse_file_info结构体的注释,可以在open时填充fh的值,这个就是open的handle,后续read、write中都可以用 */
fi->fh = 1234;
return 0;
}
static int api_read(const char *path, char *buf, size_t size, off_t offset,
struct fuse_file_info *fi)
{
if(strcmp(path, hello_path) != 0)
return -ENOENT;
/* 通过fuse_get_context可以取出fuse_new传入的用户参数,该函数在fuse_operations所有的函数中都可以调用 */
struct fuse_context * fc = fuse_get_context();
int * data = fc->private_data;
int len = 0;
/* 长度返回0时,本次read就结束了 */
if( *data % 2 == 0)
{
len = sprintf(buf,"%s user_data:%d open handle:%lld \n",hello_str,*data,fi->fh);
}
(*data)++;
return len;
}
static struct fuse_operations api_oper = {
.getattr = api_getattr,
.readdir = api_readdir,
.open = api_open,
.read = api_read,
};
#define MOUNT_POINT "hfs"
int main()
{
char * mountpoint = NULL;
char * argv[] = {"usfs", "-d", MOUNT_POINT, NULL};
struct fuse_args args = FUSE_ARGS_INIT(3, argv);
fuse_parse_cmdline(&args, &mountpoint, NULL, NULL);
struct fuse_chan * ch = fuse_mount(mountpoint, &args);
struct fuse * fuse = fuse_new(ch, &args, &api_oper, sizeof(struct fuse_operations), &user_data);
pthread_t tid_fuse;
pthread_create(&tid_fuse, NULL, (void *)thread_fuse, fuse);
while (getchar() != 'q')
{
usleep(1000);
}
fuse_exit(fuse); /* 退出fuse_loop */
fuse_unmount(mountpoint, ch); /* 对应fuse_mount */
fuse_destroy(fuse); /* 对应fuse_new */
fuse_opt_free_args(&args); /* fuse_parse_cmdline里面会申请内存,需释放 */
free(mountpoint);
return 0;
}
程序没有错误处理,仅供参考,编译方式如下:
gcc test.c -o test -D_FILE_OFFSET_BITS=64 -I./include -L./lib -lfuse -lpthread -ldl
需用到fuse的头文件和库,头文件在include目录下,库在lib/.libs目录下。
上面例子用到了几个特性:
- 用户参数通过fuse_new传进去,在所有的fuse_operations中,都可以通过fuse_get_context来得到这个user_data的指针
- open操作的时候可以把句柄写入到fh中,后续其他操作可以获取到该fh的值
- read操作,最后一次需return 0,且getattr长度需正确,否则得不到正确的输出
以上测试程序输出如下:
sw@sw:hfs$ cat hello
Hello World! user_data:2024 open handle:1234
sw@sw:hfs$ cat hello
Hello World! user_data:2026 open handle:1234
可以看到user_data两次输出的不一样,在read中输出了open时给的fh值。
此测试程序可以按q退出,完成清理工作。
高级接口
libfuse提供了low level接口,示例程序有几个例子是用该接口实现的。此类接口更灵活,使用稍微复杂一些。libfuse的注释就是文档,非常详细,编程的时候根据注释提示来就可以了。
/**
* Read data
*
* Read should send exactly the number of bytes requested except
* on EOF or error, otherwise the rest of the data will be
* substituted with zeroes. An exception to this is when the file
* has been opened in 'direct_io' mode, in which case the return
* value of the read system call will reflect the return value of
* this operation.
*
* fi->fh will contain the value set by the open method, or will
* be undefined if the open method didn't set any value.
*
* Valid replies:
* fuse_reply_buf
* fuse_reply_iov
* fuse_reply_data
* fuse_reply_err
*
* @param req request handle
* @param ino the inode number
* @param size number of bytes to read
* @param off offset to read from
* @param fi file information
*/
void (*read) (fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
struct fuse_file_info *fi);
看这个read的注释,read需配合fuse_reply_buf、fuse_reply_iov、fuse_reply_data、fuse_reply_err这几个接口来完成功能,每个fuse_operations的有效接口都不一样,详细的用法大家可以参考人家写好的文件系统。