Fuse 文件系统
例如 #ifdef linux 交叉编译的话需要修改5. 部分函数未定义android bionic c库未定义,尝试自己定义或者换一个接口。
背景
用户空间文件系统(Filesystem in Userspace,简称FUSE)是一个面向类Unix计算机操作系统的软件接口,它使无特权的用户能够无需编辑内核代码而创建自己的文件系统。
文件系统是一个通用操作系统重要的组成部分。传统上操作系统在内核层面上对文件系统提供支持、而通常内核态的代码难以调试、效率较低。Linux从2.6.14版本开始通过FUSE模块支持在用户空间实现文件系统。通过FUSE内核模块的支持、开发者只需要根据FUSE提供的接口实现具体的文件操作就可以实现一个文件系统。由于其主要实现代码位于用户空间中、而不需要重新编译内核、这给开发者带来了众多便利。
在用户空间实现文件系统能够大幅提高效率,简化了为操作系统提供新的文件系统的工作量,并且,用户空间下调试工具丰富,出问题不会导致系统崩溃,开发难度相对较低,开发周期较短,特别适用于各种虚拟文件系统和网络文件系统。
但是、在用户态实现文件系统必然会引入额外的内核态/用户态切换带来的开销、对性能会产生一定影响。我们的实验表明、根据所使用的工作负载和硬件、FUSE导致的性能下降可能完全无法察觉、甚至即使经过优化、也可能高达-83%;相对CPU利用率可以提高31%。

完整的FUSE功能包括3层结构协作完成。
- 用户态自定义文件系统 + mount 工具 fusermount
- 用户态Fuse库(libfuse):解析内核态转发出来的协议包、拆解成常规的IO请求;
- 内核支持(Fuse.ko,fs/fuse/*):用来接收vfs传递下来的IO请求,并且把这个IO封装之后通过管道发送到用户态;
FUSE设计
架构
FUSE由内核部分和用户级守护程序(user-level daemon)组成、内核部分实现为Linux内核模块、加载时、该模块向Linux的VFS注册FUSE文件系统驱动程序。
此FUSE驱动程序充当由不同用户级守护程序实现的各种特定文件系统的代理、除了注册新的文件系统外、FUSE的内核模块还注册/dev/FUSE块设备、该块设备作为FUSE守护程序与内核通信的桥梁、FUSE守护程序通过/dev/fuse读取FUSE请求、处理后将应答写入/dev/fuse。
图1显示了FUSE的高级体系结构,当应用挂在FUSE文件系统上,并且执行一些系统调用时,VFS会将这些操作路由至FUSE驱动器,FUSE驱动器创建了一个FUSE请求结构,并将请求保存在FUSE队列。

此时,提交操作的进程通常处于等待状态。然后,FUSE的用户级守护程序通过/dev/fuse读取,从内核队列中挑选并处理请求。处理请求可能需要再次进入内核,例如,如果是一个可堆叠FUSE文件系统,守护程序将操作提交到基础文件系统(例如Ext4),或者,如果是基于块的FUSE文件系统,守护程序从块设备读取或写入。
当处理完请求后,FUSE守护程序会将应答写回/dev/fuse,FUSE驱动器此时把请求标记为已完成,最终唤醒用户进程。
应用程序调用的某些文件系统操作可以在不与用户级FUSE守护程序通信的情况下完成。例如、从其页缓存在内核页缓存中的文件读取、不需要转发到FUSE驱动程序。
实现细节
我们现在讨论几个重要的FUSE实现细节:用户内核协议、库和API级别、内核FUSE队列、拼接、多线程、回写缓存。
用户内核协议
当FUSE的内核驱动程序与用户空间守护程序通信时、它形成了FUSE请求结构。请求具有不同的类型、具体取决于它们传递的操作。
表1列出了所有43种FUSE请求类型、按其语义分组。正如所看到的、大多数请求都直接映射到传统的VFS操作:我们省略了对明显请求的讨论(例如、读取、创建)、而是接下来关注那些不那么直观的请求类型(表1中标记为粗体)。

INIT 请求是由内核在挂载文件系统时产生的、此时、用户空间和内核协商
- 他们将运行的协议版本
- 相互支持的功能集(例如,读取DIRPLUS或FLOCK支持)
- 各种参数设置(例如、FUSE预读大小、时间粒度)
相反、在文件系统卸载过程中、内核会发送 DESTROY 请求、当获得销毁时、守护程序将执行所有必要的清理。此会话的内核将不再有请求、后续从/dev/fuse读取将返回0、导致守护程序优雅退出。如果不再需要任何先前发送的请求(例如、当在READ上阻塞的用户进程终止时)、内核将发出 INTERRUPT 请求。每一个请求都有一个独一无二的序列(中断使用它来识别受害者请求)、序列号由内核分配、也用于在用户空间回复时定位已完成的请求。
每个请求还包含一个node ID--一个无符号的64位整数,用于标识内核和用户空间中的inode。路径到inode的转换由 LOOKUP 请求执行。每次查找现有的inode(或创建新的inode)时,内核都会将inode保留在inode缓存中。从dcache中删除inode时,内核将 FORGET 请求传递给用户空间守护程序。此时,守护程序可能会决定取消分配任何相应的数据结构。
BATCH_FORGET 允许内核通过单个请求忘记多个inode。当用户应用程序打开文件时、就会生成一个 OPEN 请求、这并不奇怪。在回复此请求时、FUSE守护程序有机会选择为打开的文件分配64位文件句柄。然后、内核将返回此文件句柄、以及与打开的文件关联的每个请求。
用户空间守护程序可以使用句柄存储每个打开的文件信息。
例如,可堆叠文件系统可以将在底层文件系统中打开的文件的描述符存储为FUSE文件句柄的一部分。每次关闭打开的文件时都会生成 FLUSH,当不再引用以前打开的文件时发送 RELEASE。
OPENDIR 和 RELEASEDIR 请求分别与 OPEN 和 RELEASE 具有相同的语义,但针对的是目录。
READDIRPLUS 请求返回一个或多个目录条目、如READDIR、但它还包括每个条目的元数据信息。
这允许内核预填充其inode缓存(类似于NFSv3的READDIRPLUS过程)。当内核评估用户进程是否有权访问文件时,它会生成一个 ACCESS 请求。通过处理这个请求,FUSE守护进程可以实现自定义权限逻辑。
但是、通常用户使用默认权限选项挂载FUSE、允许内核根据标准Unix属性(所有权和权限位)授予或拒绝对文件的访问权限、而在这种情况下、不会生成ACCESS请求
库和API级别
从概念上讲,FUSE库由两个级别组成。
较低层负责(1)接收和解析来自内核的请求(2)发送正确格式的回复(3)解析文件系统配置和挂载(4)隐藏内核和用户空间之间的潜在版本差异
高级FUSE API构建在低级API的基础上、允许开发人员跳过路径到inode映射的实现。因此、高级API中既不存在inode也不存在查找操作、从而简化了代码开发。
相反、所有高级API方法都直接在文件路径上操作。高级API还处理请求中断、并提供其他方便的功能。
例如、开发人员可以使用更常见的Chon()、chmod()、truncate() 函数、而不是低层级的setattr()
当然、文件系统开发人员必须自己通过平衡灵活性与开发易用性来决定使用哪种API。
FUSE内核队列
在前文中、我们提到FUSE的内核有一个请求队列。实际上、FUSE在内核中维护了五个队列、分别为:interrupts、forgets、pending、processing、background。一个请求在任何时候只会存在于一个队列中。
- 后台:后台队列用于暂存异步请求。在默认情况下,只有读请求进入后台队列;当回写缓存启用时,写请求也会进入后台队列。当开启回写缓存时,来自用户进程的写请求会先在页缓存中累积,然后当bdflush线程被唤醒时会下刷脏页。在下刷脏页时,FUSE会构造异步请求,并将它们放入后台队列中。
- 挂起:同步请求(例如,元数据)放在挂起队列中,并且挂起队列会周期性接收来自后台的请求。但是挂起队列中异步请求的个数最大为max_background(最大为12),当挂起队列的异步请求未达到12时,后台队列的请求将被移动到挂起队列中。这样做的目的是为了控制挂起队列中异步请求的个数,防止在突发大量异步请求的情况下,阻塞了同步请求。
- 处理:当挂起队列中的请求被转发到Fuse守护程序的同时,也被移动到处理队列。所以处理队列中的请求,表示正在被处理Fuse守护程序处理的请求。当Fuse守护程序真正处理完请求,通过/dev/fuse下发应答时,该请求将从处理队列中删除。
- 中断:用于存放中断请求、比如当发送的请求被用户取消时、内核会发送一个中断请求、来取消已被发送的请求。中断请求的优先级最高、中断中的请求会最先得到处理。
- 忘记:忘记请求用于删除dcache中缓存的inode。

Splicing 和 FUSE buffer
在其基本设置中、FUSE守护程序必须从 /dev/fuse read() 请求、然后write() 响应/dev/fuse。每个这样的调用造成内核和用户空间之间的内存拷贝冗余。为了缓解此问题、FUSE可以使用Linux内核提供的Splicing功能。
Splicing 允许用户空间在两个内核内存缓冲区之间传输数据、而无需将数据复制到用户空间。为了无缝支持拼接、FUSE以两种形式之一表示其缓冲区:
- 由用户守护程序地址空间中的指针标识的常规内存区域
- 由文件描述符指向的内核空间内存
如果用户空间文件系统实现了写BUF()方法、那么FUSE会拼接来自/dev/fuse的数据、并将数据以包含文件描述符的缓冲区的形式直接传递给这个方法。FUSE拼接包含多页数据的写请求。类似的逻辑适用于对超过两页数据的读请求的回复
多线程
随着并行性越来越流行、FUSE添加了多线程支持。在多线程模式下、FUSE的守护进程以一个线程启动、如果待处理队列中有两个或更多请求可用、FUSE会自动生成额外的线程。每个线程一次处理一个请求、处理完请求后、每个线程检查当前运行是否有超过10个线程;如果是这样、该线程退出。
FUSE库创建的线程数没有明确的上限、存在隐式限制有两个原因:(1)默认情况下、一次只能有12个异步请求(最大后台参数)在待处理队列中;(2)等待队列中同步请求的数量取决于用户进程产生的I/O活动总量。
此外、对于每个中断和忘记请求、都会调用一个新线程。在没有中断支持且生成很少忘记的典型系统中、FUSE守护线程的总数最多(12+待处理队列中的请求数)。
回写缓存和最大写入
FUSE的基本写入行为是同步的、只有4KB的数据被发送到用户守护进程进行写入。这会导致某些工作负载出现性能问题;将大文件复制到FUSE文件系统时、/bin/cp间接导致每次4KB的数据同步发送到用户空间。
FUSE实现的解决方案是让FUSE的页面缓存支持回写策略、然后使写入异步。通过该更改、可以将文件数据以最大写入大小(限制为32页)的更大块推送到用户守护程序。
Instrumentation
为了研究FUSE的性能、我们开发了一个简单的可堆叠直通文件系统--称为Stackfs--并检测了FUSE的内核模块和用户空间库、以收集有用的统计数据和跟踪。Stackfs是一个文件系统、它将未经修改的FUSE请求直接传递到底层文件系统、使用Stackfs的原因有两个
- 在检查了所有公开可用的基于FUSE的文件系统的代码后、我们发现它们中的大多数是可堆叠的(即部署在其他通常是内核文件系统的顶部)。
- 我们希望增加尽可能少的开销、以隔离FUSE内核和库的开销。
复杂的生产文件系统通常需要高度的灵活性、因此使用FUSE的低级API。由于这些文件系统是我们的主要重点、我们使用FUSE的低级API实现了Stackfs、这也避免了高级API增加的开销。下面我们介绍Stackfs使用的几个重要数据结构和过程。
inode.Stackfs将每个文件的元数据存储在inode中,Stackfs的inode不是持久的,只有在挂载文件系统时,才存在于内存中。除了记账信息外,inode还存储基础文件的路径、其inode编号和引用计数器。例如,当对Stackfs文件的OPEN请求到达时,使用该路径打开基础文件。
查找。在查找期间,Stackfs使用stat(2)检查基础文件是否存在。每次找到文件时,Stackfs都会分配一个新的inode,并将所需的信息返回给内核。Stackfs为其inode分配与内存中inode结构地址相等的编号(按类型转播),该编号保证是唯一的,这允许Stackfs通过查找快速找到任何操作的inode结构(例如,打开或stat)。
同一个inode可以被多次查找(例如,由于硬链接),因此Stackfs将inode存储在由底层inode编号索引的哈希表中。在处理LOOKUP时,Stackfs检查哈希表以查看该inode之前是否已分配,如果找到,则将其引用计数器增加1。当针对inode的遗忘请求到达时,Stackfs会减少inode的引用计数,并在计数降为零时释放inode。
文件创建并打开。在文件创建过程中、Stackfs在底层文件系统中成功创建相应文件后、会在哈希表中添加一个新的inode。在处理OPEN请求时、Stackfs将底层文件的文件描述符保存在文件句柄中、然后在读写操作期间使用文件描述符、并在文件关闭时释放。
源码分析
FUSE请求包分为两部分:
-
Header:这个是所有请求共用的,比如open请求,读请求,写请求,getxattr请求,头部都至少有这个结构体,报头结构体能描述整个FUSE请求,其中字段能区分请求类型;
-
Payload:这个东西是每个IO类型会是不同的,比如读请求就没这个,写请求就有这个,因为写请求是携带数据的;
type inHeader struct { Len uint32 Opcode uint32 Unique uint64 Nodeid uint64 Uid uint32 Gid uint32 Pid uint32 _ uint32 }
FUSE响应包也分为两部分:
-
Header:这个结构体也是在数据头部的,所有IO类型的响应都至少有这个结构体。该结构体用于描述整个响应请求;
-
Payload:每个请求的类型可能不同,比如读请求就会有这个,因为要携带读出来的用户数据,写请求就不会有;
type outHeader struct {
Len uint32
Error int32
Unique uint64
}
FUSE驱动器加载过程中注册了对/dev/fuse的操作接口 FUSE_dev_Operations。FUSE_dev_do_读取、FUSE_dev_do_写入分别对应FUSE守护程序从内核读取请求,以及处理完请求后写回应答的函数调用。我们分别看下具体的代码片段



当挂起、中断、忘记队列都没有请求时、读进程进入休眠。一旦有请求到达、这个等待队列上的进程将被唤醒。中断和忘记的请求优先级高于挂起队列。当请求的数据内容被拷贝至用户空间后、该请求会被移至处理队列、并且请求->标志会保存当前请求的状态。
3
当Fuse守护程序处理完请求后,会将结果写回到/dev/fuse。写数据保存在结构Fuse复制状态中,并且会根据唯一ID在fc(Fuse连接)中找到对应的请求,并将写回的参数从Fuse复制状态拷贝至请求->输出。
最后我们以断开为例、看下FUSE整体是如何工作的:

注意:上面描述中的所有内容都大大简化了
使用
mount(2)指定的文件系统类型可以是以下类型之一:
fuse
这是挂载 FUSE 文件系统的常用方法。 mount 系统调用的第一个参数可能包含任意字符串,内核不解释该字符串。
fuseblk
文件系统是基于块设备的。 mount 系统调用的第一个参数被解释为设备的名称。
挂载选项
fd=N
The file descriptor to use for communication between the userspace filesystem and the kernel. The file descriptor must have been obtained by opening the FUSE device ('/dev/fuse').
rootmode=M
The file mode of the filesystem's root in octal representation.
user_id=N
The numeric user id of the mount owner.
group_id=N
The numeric group id of the mount owner.
default_permissions
By default FUSE doesn't check file access permissions, the filesystem is free to implement its access policy or leave it to the underlying file access mechanism (e.g. in case of network filesystems). This option enables permission checking, restricting access based on file mode. It is usually useful together with the 'allow_other' mount option.
allow_other
This option overrides the security measure restricting file access to the user mounting the filesystem. This option is by default only allowed to root, but this restriction can be removed with a (userspace) configuration option.
max_read=N
With this option the maximum size of read operations can be set. The default is infinite. Note that the size of read requests is limited anyway to 32 pages (which is 128kbyte on i386).
blksize=N
Set the block size for the filesystem. The default is 512. This option is only valid for 'fuseblk' type mounts.
用户文件系统挂载这就用到了FUSE框架的第3个组件了,fusermount 工具,这个工具就是专门用来方便挂载用户文件系统才诞生的。
fusermount -o fsname=helloworld,subtype=hellofs -- /mnt/myfs/
FUSE的作用在于使用户能够绕开内核代码来编写文件系统、但是请注意、文件系统要实现对具体的设备的操作的话必须要使用设备驱动提供的接口、而设备驱动位于内核空间、这时可以直接读写块设备文件、就相当于只把文件系统摘到用户态、用户直接管理块设备空间。
Control filesystem
FUSE有一个控制文件系统,可以通过以下方式挂载:
mount -t fusectl none /sys/fs/fuse/connections
FUSE这个内核文件系统其实是可以挂载、也可以不挂载、挂载了主要是方便管理多个用户系统而已、FUSE内核文件系统的名称为 fusectl。
将它挂载在'/sys/fs/fuse/connects'目录下使其向后兼容早期版本。在FUSE控制文件系统下、每个连接都有一个由唯一编号命名的目录。对于每个连接、此目录中存在以下文件:
waiting
The number of requests which are waiting to be transferred to userspace or being processed by the filesystem daemon. If there is no filesystem activity and 'waiting' is non-zero, then the filesystem is hung or deadlocked.
abort
Writing anything into this file will abort the filesystem connection. This means that all waiting requests will be aborted an error returned for all aborted and new requests.
只有挂载的所有者可以读取或写入这些文件。
可以用df -aT命令查看:
root@ubuntu:~# df -aT | grep -i fusectl
fusectl fusectl 0 0 0 - /sys/fs/fuse/connections
通过挂载内核FUSE文件系统、可以看到所有实现的用户文件系统、如下:
root@ubuntu:~# ls -l /sys/fs/fuse/connections/
total 0
dr-x------ 2 root root 0 May 29 19:58 39
dr-x------ 2 root root 0 May 29 20:00 42
在/sys/fs/fuse/connects对应两个目录、目录名为唯一标识、能够唯一标识一个用户文件系统。这里表示内核FUSE模块通过/dev/fuse设备文件、建立了两个通信管道、分别对应了两个用户文件系统、可以在用df -aT对照确认:
root@ubuntu:~# df -aT | grep -i fuse
fusectl fusectl 0 0 0 - /sys/fs/fuse/connections
lxcfs fuse.lxcfs 0 0 0 - /var/lib/lxcfs
helloworld fuse.hellofs 0 0 0 - /mnt/myfs
每个 Uniqe ID 名录下,有若干个文件,通过这些文件,我们可以获取到当前用户文件系统的状态,或跟 fuse 文件系统交互,比如:
root@ubuntu:~# ls -l /sys/fs/fuse/connections/42/
total 0
--w------- 1 root root 0 May 29 20:00 abort
-rw------- 1 root root 0 May 29 20:00 congestion_threshold
-rw------- 1 root root 0 May 29 20:00 max_background
-r-------- 1 root root 0 May 29 20:00 waiting
-
waiting 文件:cat一下就能获取到当前正在处理的IO请求数;
-
abort 文件:该文件写入任何字符串,都会终止这个用户文件系统和上面所有的请求;
fuse 和 fuseblk
在上面的实验中,我们发现似乎fuseblk 的用户态文件系统 /sys/fs/fuse/connections 无法控制, 我们观察到 fuse 对应的设备为 /dev/fuse, 而fuseblk对应的实际块设备。这里我们需要了解这两的一些机制与区别。


fuse 类型(如 SSHFS、rclone)的管理机制
常规 FUSE 文件系统的 I/O 流程如下(以读文件为例):
应用层(sshfs read) → VFS → FUSE 内核模块 → /dev/fuse(字符设备) → FUSE 用户态守护进程(如 sshfs) → 网络/本地存储 → 返回数据
关键特点:
-
/dev/fuse是核心通信通道:
所有请求(元数据操作、文件读写)都通过该设备传递。 -
请求类型:
处理的是文件系统语义(如open、read、stat),由 VFS 层转换为 FUSE 协议消息。 -
控制接口:
通过/sys/fs/fuse/connections/<ID>动态管理连接参数(如并发请求数)。 -
/sys/fs/fuse/connections的作用:-
该目录下的每个子目录(如
34)包含 FUSE 连接的控制接口:-
waiting:查看等待处理的请求数。 -
abort:强制终止连接。 -
max_background:调整并发请求数。
-
- 这些接口允许用户或工具直接监控和调试 FUSE 连接。
-
-
设备号与连接 ID 的关系:
-
在
/proc/self/mountinfo中,FUSE 挂载的设备号为0:<ID>,其中<ID>直接对应/sys/fs/fuse/connections下的目录名。# 设备号 0:157 → 对应 /sys/fs/fuse/connections/157 450 14 0:157 / ... - fuse /dev/fuse ...
-
fuseblk 类型(如 NTFS-3G)的特殊性
fuseblk 的 I/O 流程更接近块设备驱动:
应用层(ntfs read) → VFS → 页缓存(Page Cache) → 块设备层(bio 请求) → FUSE 块设备内核模块 → 用户态守护进程(如 ntfs-3g) → 物理设备 → 返回数据
关键特点:
-
绕过了
/dev/fuse:fuseblk的请求通过块设备层(bio 请求)直接传递,而非字符设备/dev/fuse。 -
请求类型:
处理的是块设备语义(如扇区读写),由内核块设备层转换为 FUSE 块协议消息。 -
内核交互对象:
依赖struct block_device而非struct fuse_conn,因此无法通过/sys/fs/fuse/connections管理。 -
设计目标:
-
fuseblk是 FUSE 的扩展,专门用于实现 用户态的块设备文件系统(如 NTFS、exFAT 的ntfs-3g驱动)。 -
与常规 FUSE 不同,
fuseblk需要模拟块设备的行为,因此需要与内核的块设备层深度集成。
-
-
内核管理路径的差异:
-
挂载
fuseblk时,内核会分配一个 独立的次设备号(如45825),但它 不通过/sys/fs/fuse/connections管理。 - 原因在于:
-
fuseblk的通信接口由 块设备子系统 直接处理,而非标准的 FUSE 控制通道。 -
内核将
fuseblk视为一种特殊的块设备驱动,其元数据通过/proc/fs/fuse/dev(linux 5.4以前) 或内核日志输出,而非 sysfs。/proc/fs/fuse/目录不存在通常意味着 FUSE 内核模块未加载或未编译为模块。如果 FUSE 支持被编译为内核的一部分(而不是一个可加载的模块),那么/proc/fs/fuse目录可能不会出现。在这种情况下,FUSE 的功能是直接集成在内核中的,所以内核开发者可能认为没有必要再通过/proc/fs/fuse文件系统来暴露相关的控制或状态信息。 - 如何确认 FUSE 是否在内核中启用?zgrep CONFIG_FUSE_FS /boot/config-$(uname -r)
-
-
| 特性 | fuse(常规 FUSE) |
fuseblk |
|---|---|---|
| 内核对象 | 基于 fuse_conn 结构,每个连接独立管理 |
基于块设备接口 (block_device),与 FUSE 解耦 |
| 控制接口 | 通过 /sys/fs/fuse/connections/<ID> 暴露 |
无专用控制接口,依赖块设备通用机制 |
| 设计目标 | 灵活的用户态文件系统(如 SSHFS) | 高性能块设备兼容(如 NTFS-3G) |
| 动态调整能力 | 支持实时参数调整(如并发请求数) | 参数固化在挂载时,运行时不可动态修改 |
为什么 fuseblk 没有/sys/fs/fuse/connections控制节点?
-
内核路径不同:
-
fuseblk的 I/O 请求直接走块设备层(如bio请求),而非 FUSE 的通用文件请求路径。 -
因此,
/sys/fs/fuse/connections的控制节点(依赖 FUSE 的fuse_conn结构)对fuseblk无效。
-
-
稳定性与性能考量:
-
块设备操作(如磁盘 I/O)需要更高的稳定性和确定性,动态调整参数(如
max_background)可能引入不可预测的延迟或错误。 -
内核开发者刻意限制了
fuseblk的动态可调性。
-
-
用户态驱动差异:
-
fuseblk的用户态驱动(如ntfs-3g)以块设备协议与内核交互,而非 FUSE 的文件协议。 - 控制逻辑(如终止连接)需通过块设备的标准方法(如卸载)实现。
-
Interrupting filesystem operations
如果发出FUSE文件系统请求的进程中断、将发生以下情况:
- 如果请求尚未发送到用户空间并且信号是致命的(SIGKILL或未处理的致命信号)、则请求将出列并立即返回。
- 如果请求尚未发送到用户空间并且信号不是致命的,则为请求设置中断标志。当请求已成功传输到用户空间并设置此标志时,中断请求将排队。
- 如果请求已经发送到用户空间、则中断请求排队。
中断请求优先于其他请求,因此用户空间文件系统将在任何其他请求之前接收排队的中断。
用户空间文件系统可以完全忽略中断请求,或者可以通过发送对原始请求的回复来回复它们,并将错误设置为EINTR。
处理原始请求和中断请求之间也可能存在竞争。有两种可能性:
- 中断请求先处理,再处理原始请求
- 中断请求在原始请求被应答后进行处理
如果文件系统找不到原始请求,它应该等待一些超时和/或一些新请求到达,之后它应该用EGAIN错误回复中断请求。在情况1)中,中断请求将被重新排队。在情况2)中,中断回复将被忽略。
Aborting a filesystem connection
可能出现进入文件系统没有响应的某些情况。原因可能是:
- 中断的用户空间文件系统实现
- 网络连接断开
- 意外死锁
- 恶意死锁
针对上述情况,中止与文件系统的连接可能很有用。有几种方法可以做到这一点:
- 杀死文件系统守护进程。适用于1)和2)
- 杀死文件系统守护进程和文件系统的所有用户。适用于所有情况,除了一些恶意死锁
- 使用强制卸载(umount-f)。适用于所有情况、但前提是文件系统仍然挂载(它没有被惰性卸载)
- 通过FUSE控制文件系统中止文件系统。最强大的方法、总是有效的。
非特权挂载是如何工作的?
由于mount()系统调用是特权操作,因此需要一个辅助程序(Fuse挂载),它挂载在setuid root中。
提供非特权挂载的含义是,挂载所有者不得使用此功能来危害系统。由此产生的明显要求是:
- 挂载所有者不应能够在挂载的文件系统的帮助下获得提升的权限
- 挂载所有者不应非法访问来自其他用户和超级用户进程的信息
- 挂载所有者不应该能够在其他用户或超级用户的进程中引发不良行为
需求是如何满足的?
A.挂载所有者可以通过以下方式获得提升的权限:
- 创建包含设备文件的文件系统,然后打开此设备
- 创建包含suid或sgid应用程序的文件系统,然后执行此应用程序
解决方法是不允许打开设备文件,在执行程序时忽略setuid和setgid位。为确保此fuse挂载始终将"nosuid"和"nodev"添加到非特权挂载的挂载选项中。
B.如果另一个用户正在访问文件系统中的文件或目录、则服务请求的文件系统守护程序可以记录所执行操作的确切顺序和时间。而挂载所有者反而无法访问的话、这算信息泄露。
C.挂载所有者可以通过几种方式在其他用户的进程中诱导不希望的行为、例如:
- 在挂载所有者无法修改(或只能进行有限修改)的文件或目录上挂载文件系统:这在fuse挂载中得到了解决,方法是检查挂载点上的访问权限,并且只有挂载所有者可以进行无限修改(对挂载点具有写访问权限,并且挂载点不是"粘性"目录)时,才允许挂载
- 即使解决了1),挂载所有者也可以更改其他用户进程的行为:
- 它可以减缓或无限期地延迟文件系统操作的执行、从而针对用户或整个系统创建DoS。例如、suid应用程序锁定系统文件、然后访问挂载所有者的文件系统上的文件、可能会被停止、从而导致系统文件永远被锁定。
- 它可以呈现无限长度的文件或目录、或无限深度的目录结构、可能导致系统进程占用磁盘空间、内存或其他资源、再次导致Dos。
这个以及B)的解决方案是不允许进程访问文件系统、否则挂载所有者将无法监视或操作文件系统。因为如果挂载所有者可以ptrace进程、它可以在不使用FUSE挂载的情况下完成上述所有操作、因此可以使用与ptrace中使用的相同标准来检查进程是否被允许访问文件系统。
请注意,ptrace检查并不是防止C/2/1所必需的,检查挂载所有者是否有足够的权限向访问文件系统的进程发送信号就足够了,因为SIGSTOP可用于获得类似的效果
fuse passthrough


使用:
echo "-hello:passthrough" > /sys/fs/fuse/passthrough_filter
默认节点内是空,所有fuse都不开启passthrough,如果要开启passthrough需要在mount前写入fuse文件系统的名字。echo写入fuse文件系统的名字,支持一次写入多个用:链接,不带-的表示开启该名字文件系统的passthrough,带-的表示关闭passthrough
实例
假设开发者发布的基于FUSE开发的文件系统名字为ZFUSE。现在,/mnt/fuse目录已经挂载了ZFUSE。作为使用者,准备在/mnt/fuse目录下创建第一个文件my.log。

打开系统调用进入内核空间,vfs层根据挂载点文件的操作函数对应到Fuse创建打开。
问题1、既然在用户态实现文件系统、那么是否创建代码怎么能在内核空间呢?
Fuse创建打开只是为了对接vfs、真正的处理代码仍然在用户空间实现。
问题2,目前已在内核空间,如何在用户空间处理?
简单讲,在Fuse创建打开内会创建一个包含Fuse创建操作数的消息,将消息通过管道文件(/dev/fuse)发送给管道另一端的用户态接收进程。
问题3,用户态接收进程?
是的,该进程在ZFUSE挂载时由libfuse库代码中创建,作用是读取管道文件消息并根据消息的操作数来执行对应操作,在这里解析到的是FUSE_CREATE操作数,对应libfuse库中的Fuse_lib_create函数。
问题4、目前ZFUSE做了什么?
libfuse只是将操作接口与内核VFS做到一一对接,而真正完成操作的还是ZFUSE,在Fuse_lib_create中会调用ZFUSE内定义的create函数。
问题5,为什么需要libfuse的接口?
解耦合、简化ZFUSE开发。假设ZFUSE不需要lseek操作、那么就不需要实现lseek操作、而我们无法控制用户的行为、这里用户是指ZFUSE文件系统的使用者。假设用户在使用ZFUSE时进行了lseek操作、那么会先由libfuse提供的Fuse_lib_lseek接口处理、libfuse发现ZFUSE没有实现lseek、就直接返回不支持此操作。
假设libfuse没有实现这层接口,那么ZFUSE就必须实现一个lseek接口,并且将里面的逻辑操作设置为直接返回不支持,这在设计上明显笨拙。
到这里,结合下图把过程再串一遍。
- ZFUSE挂载到/mnt/fuse、libfuse会fork出后台进程、用于读取管道文件消息。
- 用户使用ZFUSE文件系统,创建文件my.log
- 调用系统调用
- 经VFS交由FUSE处理
- FUSE下的创建处理、向管道发送带创建操作(FUSE_CREATE)的消息、当前进程A加入wait queue
- libfuse下创建的后台进程读取到消息、解析操作数为FUSE_CREATE、对应到FUSE_lib_create、即low level层接口。
- Fuse_lib_create中调用ZFUSE的上层接口zfuse.create,由ZFUSE来实现创建操作
- 完成创建后、通过libfuse中的 fuse_reply_create 向管道发送完成消息从而唤醒之前加入等待队列的进程A
- 进程A得到创建成功的消息、系统调用结束、/mnt/fuse/my.log文件创建成功

代码
提问:
include/fuse.h 代码中 struct fuse_operations结构体中回调函数定义是int (*getattr) (const char *, struct stat *, struct fuse_file_info *fi);,而 lib/fuse.c 注册时 static struct fuse_lowlevel_ops fuse_path_ops 中 .getattr = fuse_lib_getattr,, 其中函数定义实现为 static void fuse_lib_getattr(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi), 他们入参不匹配是怎么完成回调的?
回答:
在 FUSE 中,存在两种不同的接口风格:高级接口(high-level interface)和低级接口(low-level interface)。高级接口主要用于用户空间实现文件系统时,低级接口则更接近 FUSE 内核模块的接口。
-
高级接口:使用
struct fuse_operations结构体注册回调函数。这些回调函数的签名与标准 POSIX 函数定义类似,例如getattr函数的签名是int (*getattr)(const char *, struct stat *, struct fuse_file_info *)。这些函数由 FUSE 库负责调用,并将参数传递给注册的回调函数。 -
低级接口:使用
struct fuse_lowlevel_ops结构体注册回调函数。这些回调函数的签名不同于标准 POSIX 函数,而是更接近 FUSE 内核模块的接口。例如,getattr函数的签名是void (*getattr) (fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi)。在这种情况下,FUSE 库会将高级接口的调用转换为对应的低级接口调用。
在你提到的情况中,getattr 函数在 struct fuse_lowlevel_ops 结构体中的定义与高级接口的签名不匹配。这是因为 FUSE 库内部会将高级接口的调用转换为低级接口调用,因此在内部实现时可以通过适配器模式(Adapter Pattern)来处理不同的签名。在适配器函数 fuse_lib_getattr 中,FUSE 库会将高级接口的参数转换为低级接口的参数,并调用相应的低级接口函数。
简而言之,FUSE 库通过适配器模式将高级接口的调用转换为低级接口的调用,使得用户可以在编写文件系统时使用标准的 POSIX 函数签名。
注册:
struct fuse_operations
{
int (*function)(argc, argv);
...
}
static struct fuse_lowlevel_ops fuse_path_ops = {
.int = fuse_lib_init,
.lookup = fuse_lib_lookup,
....
}
测试工具调研
测试难点
1. 工作量大:需要完整定制化一个完整的用户态文件系统进行测试,且保证接口全覆盖。
2. 问题定界不清晰:无法清楚区分是用户态文件系统的缺陷还是Fuse 本身设计问题。
libfuse
libfuse/test:https://github.com/libfuse/libfuse
$ source hmsdk 、 source ohsdk
$ mkdir build; cd build
$ meson setup .. (--cross-file=cross_file.txt) # 若需要交叉编译添加后面文件
$ meson configure # list options
$ meson configure -D disable-mtab=true # set an option
$ ninja (-v 打印详细流程)
$ sudo python3 -m pytest test/ (在本地测试,可选)
$ sudo ninja install
$ sudo chown root:root util/fusermount3
$ sudo chmod 4755 util/fusermount3
$ python3 -m pytest (-s) test/ # -s 可以将print输出到控制台
交叉编译 cross_file.txt 文件 (格式参考:Cross compilation):
[binaries]
c = 'clang'
cpp = 'clang++'
ar = 'llvm-ar'
strip = 'llvm-strip'
[properties]
skip_sanity_check = true
sys_root = '/home/ohos/sysroot'
[built-in options]
# 如果需要指定参数的话(有哪些参数参考meson configure 的结果)
# c_args = ['--target=aarch64-linux-ohos --sysroot=/home/ohos/sysroot']
c_args = ['--target=aarch64-linux-ohos', '--sysroot=/home/ohos/sysroot']
c_link_args = ['--target=aarch64-linux-ohos', '--sysroot=/home/ohos/sysroot']
cpp_args = ['--target=aarch64-linux-ohos', '--sysroot=/home/ohos/sysroot']
cpp_link_args = ['--target=aarch64-linux-ohos', '--sysroot=/home/ohos/sysroot']
# build.c_args = ['--target=aarch64-linux-ohos', '--sysroot=/home/ohos/sysroot']
# build.cpp_args = ['--target=aarch64-linux-ohos', '--sysroot=/home/ohos/sysroot']
[host_machine]
system = 'linux'
cpu_family = 'aarch64'
cpu = 'aarch64'
endian = 'little'
[target_machine]
system = 'linux'
cpu_family = 'aarch64'
cpu = 'armv8a'
endian = 'little'
缺少变量定义:
1. 通过 CFLAGS 或 CXXFLAGS 设置
export CFLAGS="-I/path/to/your/headers"
2. 在 Meson 构建配置中设置:
在meson.build 文件添加设置头文件路径
include_directories('/path/to/your/headers')
3. 如果实在设置的参数没有生效
直接 vi build.ninja, 修改命令

4. 如果还是找不到定义,请看.c 文件中是否存在宏隔离导致根本没有包含头文件
例如 #ifdef linux 交叉编译的话需要修改
5. 部分函数未定义
android bionic c库未定义,尝试自己定义或者换一个接口
mount.fuse3 挂载选项参考:mount.fuse3(8) - Linux manual page (man7.org)
二进制挂载选项:
FUSE options:
-h --help print help
-V --version print version
-d -o debug enable debug output (implies -f)
-f foreground operation 前台运行,需要另起一个终端,发现已经挂载成功
-s disable multi-threaded operation
-o clone_fd use separate fuse device fd for each thread
(may improve performance)
-o max_idle_threads the maximum number of idle worker threads
allowed (default: -1)
-o max_threads the maximum number of worker threads
allowed (default: 10)
-o kernel_cache cache files in kernel
-o [no]auto_cache enable caching based on modification times (off)
-o no_rofd_flush disable flushing of read-only fd on close (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 modules=M1[:M2...] names of modules to push onto filesystem stack
-o allow_other allow access by all users
-o allow_root allow access by root
-o auto_unmount auto unmount on process termination
Options for subdir module:
-o subdir=DIR prepend this directory to all paths (mandatory)
-o [no]rellinks transform absolute symlinks to relative
Options for iconv module:
-o from_code=CHARSET original encoding of file names (default: UTF-8)
-o to_code=CHARSET new encoding of the file names (default: UTF-8)
问题:
1. 暂时没有使能python测试
2. 每一个demo测试点分散在不同的demo fs 中,需要利用 pytest 才能完整覆盖
test mount:
mkdir src
mkdir mnt
./hello -f mnt -o "" # -f 将在前端运行,现象是和卡主一样,实际需要在开一个终端,可以发现已经挂载
mount.fuse3 ./hello mnt -o ""
mount.fuse3 ./hello mnt -o drop_privileges
./hello mnt -o clone_fd
mount.fuse3 ./hello mnt -o clone_fd
mount.fuse3 ./hello mnt -o clone_fd,drop_privileges
./hello_ll -f mnt -o ""
mount.fuse3 ./hello_ll mnt -o ""
mount.fuse3 ./hello_ll mnt -o drop_privileges
./hello_ll mnt -o clone_fd
mount.fuse3 ./hello_ll mnt -o clone_fd
mount.fuse3 ./hello_ll mnt -o clone_fd,drop_privileges
# test_examples.py::test_passthrough[False-passthrough-True] SKIPPED (example does not support writeback caching)
./passthrough -f mnt
# test_examples.py::test_passthrough[False-passthrough_plus-True] SKIPPED (example does not support writeback caching)
./passthrough --plus -f mnt
# test_examples.py::test_passthrough[False-passthrough_fh-True] SKIPPED (example does not support writeback caching)
mount.fuse3 ./passthrough_fh mnt
# test_examples.py::test_passthrough[False-passthrough_fh-True] SKIPPED (example does not support writeback caching)
./passthrough_ll -f mnt -d # debug 模式将一直占用控制台
./passthrough_ll -f mnt -o writeback
./passthrough -f mnt -d
# test_examples.py::test_passthrough[True-passthrough-True] SKIPPED (example does not support writeback caching)
./passthrough --plus -f mnt -d
# test_examples.py::test_passthrough[True-passthrough_plus-True] SKIPPED (example does not support writeback caching)
./passthrough_fh -f mnt -d
# test_examples.py::test_passthrough[True-passthrough_fh-True] SKIPPED (example does not support writeback caching)
./passthrough_ll -f mnt -d
./passthrough_ll -f mnt -d -o writeback
./passthrough_hp src mnt --foreground
./passthrough_hp src mnt --foreground --nocache
./ioctl -f mnt
./poll -f mnt
./null -f file # file 是一个 大文件
./notify_inval_entry -f --update-interval=1 --timeout=5 mnt
./notify_inval_entry -f --update-interval=1 --timeout=5 mnt --only-expire
./notify_inval_entry -f --update-interval=1 --timeout=5 mnt --no-notify
./notify_inval_entry -f --update-interval=1 --timeout=5 mnt --no-notify --only-expire
./passthrough_ll -o source=/dev,dev,auto_unmount -f mnt
# test_examples.py::test_dev_auto_unmount[non_root] SKIPPED (needs to run as non-root)
./cuse -f mnt # 后台进程,未注册Init, 不挂载
./release_unlink_race -f mnt # 在 test 目录下
FUSE Diagnostic
FUSE Diagnostic:https://github.com/MatthewDooler/fdt
Tool:
https://www.matthewdooler.co.uk/projects/fdt/ (请切换代理至uk节点访问)
问题:
1. 工具太老,最后更新时间比libfuse老很多
2. 适配工作量大,其使用application GUI等工具,hm可能不支持
3. 工具使用复杂,工具指南没看懂(雾)
exFAT-FUSE
3exFAT-FUSE:https://github.com/relan/exfat
问题:
1. 只是一个基于fuse实现的开源文件系统,里面可能存在潜在Bug
2. 没有专门测试套,需要手动补充测试用例
GooGle-fuse-archive
https://github.com/google/fuse-archive
参考资料
- FUSE — The Linux Kernel documentation
- FUSE文件系统 - 内核工匠 - 博客园
- https://zhuanlan.zhihu.com/p/106719192
- https://xi.infoq.cn/article/655c0893ed150ff65f2b7a16f
- 一个基于FUSE实现的简单文件系统-知乎(zhihu.com)
- https://source.android.com/docs/core/storage/fuse-passthrough?hl=zh-cn
- mount.fuse3(8) - Linux manual page (man7.org)
更多推荐

所有评论(0)