

Linux用于支持用户空间文件系统的内核模块名叫FUSE,FUSE一词有时特指Linux下的用户空间文件系统。文件系统是一个通用操作系统重要的组成部分。传统上操作系统在内核层面上对文件系统提供支持;而通常内核态的代码难以调试,生产率较低。
通常,应用程序开发只需要使用系统 API 进行文件读写操作,不需要了解文件系统的细节。在 Chromium 中为了实现跨平台,甚至对各操作系统的文件 API 进行了封装。在代码中使用封装 API,都不需要了解各操作系统所提供的文件系统 API。但由于我们的浏览器产品中使用了 FUSE 进行加密存储,所以有必要了解 FUSE 和 libfuse。
文件系统为应用程序提供了一个访问数据的通用接口。文件系统可以在用户态中实现,比如像鸿蒙这样的微内核。但大多数宏内核操作系统(如Linux),文件系统是在内核态中实现,以保证性能。然而随着文件系统复杂性的增加,用户态文件系统的使用逐渐增多,特别是在快速开发和实验性研究领域中。采用用户态文件系统有如下优势:
1.开发效率更高,易于维护和移植。
2.用户态中的错误影响范围有限,降低了系统崩溃的风险。
3.支持多平台的丰富编程语言和库。
而用户态文件系统最大的缺点是性能开销较大,特别是在用户态和内核态之间的通信和上下文切换时。这一点在后面介绍 FUSE 的架构时会有更详细的说明。
FUSE是当前最广泛使用的用户态文件系统框架。据保守估计,至少有 100 种基于 FUSE 的文件系统可以在互联网上找到。FUSE 得到广泛使用主要得益于其提供的简单 API。FUSE 项目由两个组件组成:由常规内核代码库维护的 fuse 内核模块和 libfuse 用户空间库。libfuse 提供了与 FUSE 内核模块通信的参考实现。对于应用开发者而言,通常只需要使用 libfuse,无需了解 fuse 内核模块。不过为了更好的使用 libfuse 开发文件系统,最好理解 FUSE 的高层设计,了解其实现的一些细节。所以这里先介绍 FUSE 的高层架构,并解释一些重要的实现细节,接着重点介绍 libfuse 的 API,并介绍在 UOS/Deepin 上的编译与运行示例。
FUSE结构
FUSE主要是由以下的三部分构成。
* 内核模块fuse
* 用户空间库libfuse
* mount/umount程序fusermount
用户空间进程在实行操作文件的系统调用的时候,在内核空间,VFS就会调用各文件系统定义的对应操作函数。FUSE内核模块中被定义的操作函数是把,和它对应的请求送到实现文件系统的用户空间进程(FUSE文件系统.后台程序),并等待回应。FUSE内核模块和FUSE文件系统。后台程序间的通信是通过设备文件/dev/fuse进行的。FUSE文件系统.后台程序把定义的FUSE操作函数群的地址登录到fuse_operations结构体中,并通过把fuse_operations的地址作为参数,调用fuse_main()函数。
在库函数fuse_main()中,实行以下的动作。
1. 打开设备文件/dev/fuse
2. 挂载FUSE文件系统
3. 做成FUSE文件系统句柄
4. 登录FUSE操作函数到FUSE文件系统句柄中
5. 登录信号处理器
6. 实行事件循环
A) 从设备文件/dev/fuse中读取来自内核模块的请求
B) 实行和这个请求对应的操作函数
C) 写入返回给内核模块的应答到设备文件/dev/fuse中
7. 卸载FUSE文件系统
现在,API库里已经定义了31个接口(getattr,readlink,getdir,mknod,mkdir,unlink,rmdir,symlink,rename,link,chmod,chown,truncate,utime,open,
read,write,statfs,flush,release,fsync,setxattr,getxattr,listxattr,removexattr,opendir,readdir,releasedir,
fsyncdir,init,destroy),它们可以被任意使用,FUSE文件系统,后台程序实际上是否真的实现是自由的。并且它们的大部分是把文件的路径作为第一个参数。
Linux从2.6.14版本开始通过FUSE模块支持在用户空间实现文件系统。在用户空间实现文件系统能够大幅提高生产率,简化了为操作系统提供新的文件系统的工作量,特别适用于各种虚拟文件系统和网络文件系统,ZFS和glusterfs都属于网络文件系统。但在用户态实现文件系统必然会引入额外的内核态/用户态切换带来的开销,对性能会产生一定影响。
Linux中FUSE的运行机制
FUSE 高层架构
FUSE 由内核部分和用户级守护进程组成。内核部分实现为一个 Linux 内核模块,当加载时,会向 Linux 的虚拟文件系统(VFS)注册一个 FUSE 文件系统驱动程序。该 FUSE 驱动程序充当由不同用户级守护进程实现的各种特定文件系统的代理。除了注册一个新文件系统外,FUSE 的内核模块还注册了一个 /dev/fuse 块设备。该设备作为用户态 FUSE 守护进程与内核之间的接口。通常,守护进程从 /dev/fuse 读取 FUSE 请求,处理后将回复写回 /dev/fuse。
图:FUSE 架构
FUSE 的高层架构如上图 所示。当用户空间程序对挂载的 FUSE 文件系统发起操作时,其调用流程如下:
1.用户程序通过系统调用请求文件系统操作(如读取文件)。
2.Linux 虚拟文件系统(VFS)将请求转发到 FUSE 的内核模块。
3.FUSE 内核模块将请求打包成 FUSE request 数据结构,存入内核的 FUSE 队列,同时将调用进程挂起等待结果。
4.用户空间守护进程(FUSE daemon)从 /dev/fuse 设备读取请求,处理后再将结果写回 /dev/fuse。
5.FUSE 内核模块从 /dev/fuse 接收到结果后,将结果返回给挂起的进程。
6.用户程序接收到文件系统操作的结果。
从上面的流程可以看出,每次文件操作都需要从用户态程序进入内核态,然后通过 FUSE 内核模块将请求发送到用户态的 FUSE 守护进程,处理完成后再切换回内核态,最后返回给用户程序。这个调用过程不仅是增加了调用层次,而且系统调用和上下文切换增加了额外的开销。请求被放入 FUSE 队列后,必须等待用户态守护进程来处理。守护进程可能由于调度延迟或处理能力不足而导致请求滞后。这些都会导致 FUSE 性能下降。
libfuse 介绍
libfuse 是一个用户空间库,作为用户空间程序与 Linux 内核中的 FUSE 模块之间的接口。其为开发者提供了操作文件系统所需的功能,使其能够通过一个简洁的 API 实现复杂的文件系统行为。
开发者使用 libfuse 来实现自定义的文件系统逻辑,而内核通过 fuse 模块与用户空间的 libfuse 进行通信。
libfuse 提供了一套 API,供用户空间文件系统与内核交互。这些 API 主要集中在对文件系统操作的实现上,如打开、读写文件、删除文件等。以下是 libfuse 提供的一些常用 API:
1. 文件系统操作的回调函数
开发者需要实现一些回调函数,这些回调函数处理用户空间的文件系统操作。每个文件系统操作(如读取、写入、创建文件等)都需要对应的回调函数。
fuse_operations 结构体:
该结构体定义了文件系统的各类操作(如 read, write, create, unlink 等)对应的回调函数。
示例:
struct fuse_operations oper = {
.getattr = my_getattr,
.read = my_read,
.write = my_write,
// 其他操作...
};
每个操作函数都有对应的签名,例如:
getattr:获取文件属性
int (*getattr)(const char *path, struct stat *stbuf);
read:从文件中读取数据
int (*read)(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi);
2. 文件系统启动与关闭
fuse_main:
启动 FUSE 文件系统并进入事件循环。开发者需要传入 fuse_operations 结构体和命令行参数来启动文件系统。
int fuse_main(int argc, char *argv[], const struct fuse_operations *op, size_t op_size);
fuse_unmount:
卸载挂载的 FUSE 文件系统。
int fuse_unmount(const char *mountpoint, struct fuse_args *args);
3. 文件和目录操作
fuse_mkdir:
创建目录。
int fuse_mkdir(const char *path, mode_t mode);
fuse_rmdir:
删除目录。
int fuse_rmdir(const char *path);
fuse_open:
打开文件,返回文件描述符。
int fuse_open(const char *path, struct fuse_file_info *fi);
fuse_read:
从文件中读取数据。
int fuse_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi);
fuse_write:
向文件中写入数据。
int fuse_write(const char *path, const char *buf, size_t size, off_t offset, struct fuse_file_info *fi);
fuse_unlink:
删除文件。
int fuse_unlink(const char *path);
4. 目录和文件信息
fuse_getattr:
获取文件或目录的属性(如权限、大小、类型等)。
int fuse_getattr(const char *path, struct stat *stbuf);
fuse_readlink:
获取符号链接的目标。
int fuse_readlink(const char *path, char *buf, size_t size);
5. 错误处理和返回值
fuse_reply_err:
向 FUSE 内核模块返回错误代码。
void fuse_reply_err(struct fuse_req *req, int errcode);
fuse_reply_buf:
返回一个数据缓冲区作为回应。
void fuse_reply_buf(struct fuse_req *req, const void *buf, size_t size);
libfuse 源码编译与使用
如果对 libfuse 的版本没有什么要求,可以直接通过 apt install 命令进行安装:
apt install libfuse-dev
deepin v23 上 libfuse 的版本是 2.9.9.1。由于项目中使用的版本是 libfuse 3.10.5,因此需要手动编译安装。
1. 下载源码
从 libfuse 官方网站 下载源码,并切换到 tag/fuse-3.10.5。
$ git clone https://github.com/libfuse/libfuse
$ cd libfuse
$ git checkout -b tag/fuse-3.10.5 fuse-3.10.5
2. 安装 meson 构建系统
libfuse 采用了比较少见的构建系统 meson,这是一套基于 Python3 和 Ninja 的构建系统。首先安装 Python3 和 Ninja:
$ apt-get install python3 python3-pip python3-setuptools python3-wheel ninja-build
接下来安装 meson:
$ pip3 install meson
3. 编译安装 libfuse
$ mkdir build
$ cd build
$ meson ..
$ ninja
$ sudo ninja install
可以跑一下测试程序:
$ chown root:root util/fusermount3
$ chmod 4755 util/fusermount3
$ python3 -m pytest test/
一个简单示例
为了更好的理解如何使用fuse,我们这里实现一个具体的实例。大多数程序示例都是以 Hello World 开头的,这里我们也实现一个 Hello World 文件系统。为简单起见,这里并不实现一个真正的文件系统,也不会访问磁盘,而是在该文件系统的根目录中显示一个固定的文件,也就是 Hello-world 文件。
#define FUSE_USE_VERSION 29
#include <stdio.h>
#include <fuse.h>
/* 这里实现了一个遍历目录的功能,当用户在目录执行ls时,会回调到该函数,我们这里只是返回一个固定的文件Hello-world。 */
static int test_readdir(const char* path, void* buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info* fi) {
printf( "tfs_readdir path : %s ", path);
return filler(buf, "Hello-world", NULL, 0);
}
/* 显示文件属性 */
static int test_getattr(const char* path, struct stat *stbuf) {
printf("tfs_getattr path : %s ", path);
if (strcmp(path, "/") == 0)
stbuf->st_mode = 0755 | S_IFDIR;
else
stbuf->st_mode = 0644 | S_IFREG;
return 0;
}
/*这里是回调函数集合,这里实现的很简单*/
static struct fuse_operations tfs_ops = {
.readdir = test_readdir,
.getattr = test_getattr,
};
int main(int argc, char *argv[]) {
int ret = 0;
ret = fuse_main(argc, argv, &tfs_ops, NULL);
return ret;
}
假设在 /tmp 目录下面有一个 file_on_fuse_fs 目录,如果没有可以手动创建一个。然后执行如下命令可以在该目录挂载新的文件系统。
$ ./fuse_user /tmp/file_on_fuse_fs
此时实际上已经有一个新的文件系统挂载在 /tmp/file_on_fuse_fs 目录下面,可以通过 mount 命令查看一下。
$ mount
sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
udev on /dev type devtmpfs (rw,nosuid,relatime,size=16334592k,nr_inodes=4083648,mode=755)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000)
tmpfs on /run type tmpfs (rw,nosuid,nodev,noexec,relatime,size=3279000k,mode=755)
/dev/sdc3 on / type ext4 (rw,relatime)
securityfs on /sys/kernel/security type securityfs (rw,nosuid,nodev,noexec,relatime)
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
tmpfs on /run/lock type tmpfs (rw,nosuid,nodev,noexec,relatime,size=5120k)
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)
pstore on /sys/fs/pstore type pstore (rw,nosuid,nodev,noexec,relatime)
efivarfs on /sys/firmware/efi/efivars type efivarfs (rw,nosuid,nodev,noexec,relatime)
bpf on /sys/fs/bpf type bpf (rw,nosuid,nodev,noexec,relatime,mode=700)
systemd-1 on /proc/sys/fs/binfmt_misc type autofs (rw,relatime,fd=32,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=3173)
mqueue on /dev/mqueue type mqueue (rw,nosuid,nodev,noexec,relatime)
debugfs on /sys/kernel/debug type debugfs (rw,nosuid,nodev,noexec,relatime)
hugetlbfs on /dev/hugepages type hugetlbfs (rw,nosuid,nodev,relatime,pagesize=2M)
tracefs on /sys/kernel/tracing type tracefs (rw,nosuid,nodev,noexec,relatime)
fusectl on /sys/fs/fuse/connections type fusectl (rw,nosuid,nodev,noexec,relatime)
configfs on /sys/kernel/config type configfs (rw,nosuid,nodev,noexec,relatime)
/dev/sda1 on /work type ext4 (rw,relatime)
/dev/sdc1 on /boot/efi type vfat (rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro)
/dev/sdb on /data type ext4 (rw,relatime)
binfmt_misc on /proc/sys/fs/binfmt_misc type binfmt_misc (rw,nosuid,nodev,noexec,relatime)
tmpfs on /run/user/1000 type tmpfs (rw,nosuid,nodev,relatime,size=3278996k,nr_inodes=819749,mode=700,uid=1000,gid=1000)
gvfsd-fuse on /run/user/1000/gvfs type fuse.gvfsd-fuse (rw,nosuid,nodev,relatime,user_id=1000,group_id=1000)
portal on /run/user/1000/doc type fuse.portal (rw,nosuid,nodev,relatime,user_id=1000,group_id=1000)
/dev/nvme1n1p2 on /media/alex/eaf5b6c4-7ee8-4afb-8294-3d73338106f1 type ext4 (rw,nosuid,nodev,relatime,errors=remount-ro,uhelper=udisks2)
/dev/nvme0n1p2 on /media/alex/5C6EC55E6EC53216 type fuseblk (rw,nosuid,nodev,relatime,user_id=0,group_id=0,default_permissions,allow_other,blksize=4096,uhelper=udisks2)
/work/browser/fuse/hello_fuse on /tmp/file_on_fuse_fs type fuse.hello_fuse (rw,nosuid,nodev,relatime,user_id=1000,group_id=1000)
最后一行就是我们挂载的文件系统。可以通过 ls 命令查看一下这个目录:
$ ls -alh /tmp/file_on_fuse_fs
总计 0
-rw-r--r-- 0 root root 0 1970年 1月 1日 Hello-world
注意,这里只是一个非常简单的示例代码,并不存在 Hello-world 文件,而是通过虚拟文件系统,让 ls 命令返回这样一个结果。
通过 FUSE,开发者可以轻松地在用户空间实现文件系统逻辑,而无需深入内核开发。这种灵活性使得 FUSE 在透明加密、虚拟文件系统等领域得到了广泛应用。而 libfuse 的出现,更是降低了开发 FUSE 文件系统的门槛。
目前Linux,FreeBSD,NetBSD,OpenSolaris和Mac OSX支持用户空间态文件系统,比较知名的用户空间文件系统
* ExpanDrive: 商业文件系统,实现了SFTP/FTP/FTPS协议;
* GlusterFS: 用于集群的分布式文件系统,可以扩展到PB级;
* SSHFS: 通过SSH协议访问远程文件系统;
* GmailFS: 通过文件系统方式访问GMail;
* EncFS: 加密的虚拟文件系统
* NTFS-3G和Captive NTFS, 在非Windows中对NTFS文件系统提供支持;
* WikipediaFS : 支持通过文件系统接口访问Wikipedia上的文章;
* Sun公司的Lustre: 和GlusterFS类似但更早的一个集群文件系统
* ZFS: Luster的Linux版;
* archivemount:
* HDFS: Hadoop提供的分布式文件系统。HDFS可以通过一系列命令访问,并不一定经过Linux FUSE;
With FUSE it is possible to implement a fully functional filesystem in a userspace program. Features include:
* Simple library API
* Simple installation (no need to patch or recompile the kernel)
* Secure implementation
* Userspace - kernel interface is very efficient
* Usable by non privileged users
* Runs on Linux kernels 2.4.X and 2.6.X
* Has proven very stable over time
How does it work?
The following figure shows the path of a filesystem call (e.g. stat) in the above hello world example:
The FUSE kernel module and the FUSE library communicate via a special file descriptor which is obtained by opening /dev/fuse. This file can be opened multiple times, and the obtained file descriptor is passed to the mount syscall, to match up the descriptor with the mounted filesystem.
在centos下对其的安装分体系架构而不同,先看x64的安装过程:
[root@smail ~]# rpm -aq|grep kernel
kernel-module-openafs-2.6.18-92.1.6.el5-1.4.7-68.2.SL5.i686
kernel-headers-2.6.18-92.1.6.el5.i386
kernel-module-fuse-2.6.18-92.1.6.el5-2.6.3-1.sl5.i686
kernel-devel-2.6.18-92.1.6.el5.i686
kernel-2.6.18-92.1.6.el5.i686
查看所安装的kernel包中,有没有安装kernel-devel这个包,没有则安装之;其次还要注意的是其版本一定要和主kernel版本完全一致(2.6.18-92.1.6.el5)。否则即使安装成功了fuse相关包后亦不能正常加载fuse模块。
如果通过yum查找不到,则可到rpm 相关下载站上进行搜索下载。在x64的机器上可能通过安装如下的软件包来实现fuse的加载:
#yum install fuse fuse-libs.x86_64 fuse-devel.x86_64 dkms-fuse.noarch
而在x86的机器上操作如下:
#yum install fuse.i386 fuse-devel.i386 fuse-libs.i386
当然debian系列的操作系统就更好安装了,安装其基本包及开发包就可以了。
FAQ
---------------------
关于这两个模块(FUSE、LUFS)的不同和为什么又开始了新的工程,说明一下好吗?
从SourceForge 上发布的日期来看,FUSE的最初发布比LUFS还要早一年。但在相当长的时间内,它们之前都没有相互注意。
在LUFS里,文件系统是根据lufsmount被载入的共享对象(.so),而在FUSE里,文件系统是使用fuse库的各自实行可能的对象,这就是他们之间所谓的区别。实际的API都非常相似,都是载入LUFS模块/使用FUSE内核模块,然后能实行API(参照FUSE页的lufis)。并且,LUFS在缓存目录和文件属性这一点上有一些不同。FUSE不是那样做的,但它提供了简单的接口。
---------------------
fuse_operations结构体里没有close()函数?
close()函数没有包含在fuse_operations结构体里有理由吗?大概得有必要知道什么时候文件被关闭吧。
这并不简单。请考虑一下mmap()函数。把文件映射到内存里后,即使关闭了文件,那通过内存地址也能对这个文件进行读写操作。然而,close()相似的函数还有flush和release函数。flush是 调用close(),release是包含内存映射的文件不再使用的时候在被调用的。
---------------------
FUSE为什么不支持ioctl。
因为对于规则文件实行ioctl没有什么意义,对于设备文件,文件系统的实现基本上不关心ioctl,所以怎么样使用它还不是很清楚。例如,为了支持设备ioctl即使做了一些改良,因为它们包含任意的结构化的数据(不是读/写场合的那些长度),对于它们好像什么也不能做。
使用getxattr()和setxattr()的话,跟ioctl()比起来也是没有问题,这些都在fuse-2.0里已经实现了。