fuse

FUSE

Filesystem in Userspace是一个用来实现用户态文件系统的框架。
用户态文件系统并不那么的用户态,因为你的代码并不是真正在用户态去访问文件系统,它其实只是一层代理,当调用文件系统时,最先进去的仍然是 VFS,真正的文件 IO 操作最终还是由内核实现,而在用户态能做的,是作为一个守护进程,将内核传递的 fuse request 经由用户态进行处理。
FUSE 框架包含 3 个组件:

  • 内核模块 fuse.ko :用来接收 vfs 传递下来的 IO 请求,并且把这个 IO 封装之后通过管道发送到用户态;
  • 用户态 lib 库 :解析内核态转发出来的协议包,拆解成常规的 IO 请求;(c版本是libfuse;golang版本是go-fuse)
  • mount 工具 fusermount ;
    其中,libfuse为程序开发提供接口,也是我们实际开发时掉用的接口,我们通过这些接口将请求处理功能注册到fuse中(比如读写文件的接口,遍历目录的接口等)。内核态模块是具体的数据流程的功能实现,它截获文件的访问请求,然后调用用户态注册的函数进行处理。

IO路径

image
假定,/mnt/fuse目录已经挂载了ZFUSE。作为使用者,准备在/mnt/fuse目录下创建第一个文件my.log。
1)ZFUSE挂载到/mnt/fuse,libfuse会fork出后台进程,用于监听并读取管道文件消息。

2)用户使用ZFUSE文件系统,创建文件my.log,即调用系统调用open()

4)经VFS交由fuse处理

5)fuse下的create处理,向管道发送带创建操作(FUSE_CREATE)的消息,当前进程A加入等待队列

6)libfuse下创建的后台进程读取到消息,解析操作数为FUSE_CREATE,对应到fuse_lib_create,即low level层接口。

7)fuse_lib_create中调用ZFUSE的上层接口zfuse.create,由ZFUSE来实现创建操作

8)完成创建后,通过libfuse中的fuse_reply_create向管道发送完成消息从而唤醒之前加入等待队列的进程A

9)进程A得到创建成功的消息,系统调用结束,/mnt/fuse/my.log文件创建成功

image

协议

FUSE 模块其实是一个简单的客户端-服务器协议,它的客户端是内核,用户态的守护进程就是服务端,/dev/fuse设备文件是用户态daemon进程与 FUSE 内核模块交流的纽带。每一个从内核到用户空间的请求也都有一个请求头 fuse_in_header,它的大小是固定的

struct fuse_in_header {
	uint32_t	len; /* 数据长度:包括 header 在内 */
	uint32_t	opcode; /* 操作码*/
	uint64_t	unique; /* 唯一请求 id */
	uint64_t	nodeid; /* 被操作的文件系统对象(文件或目录)的 ID */
	uint32_t	uid; /* 请求进程的 uid */
	uint32_t	gid; /* 请求进程的 gid */
	uint32_t	pid; /* 请求进程的 pid */
	uint32_t	padding;
};

接着在请求头之后,则紧跟着请求体,请求体的长度是可变的,它的具体类型可以通过 opcode区分。用户态处理完后,然后将结果返回给内核态,回包也有一个固定的请求头

struct fuse_out_header {
	uint32_t	len;
	int32_t		error;
	uint64_t	unique;
};

其中,unique是与请求头对应的,用于标识是哪个请求的响应。接着在之后紧跟着响应体(如果有)。

性能

相比于正常的写,路径变长了:

  • 1 多了两次内存copy
  • 2 user app异步通知
  • 3 32个page的限制,会导致一次前端请求变成多次后端请求
    这是因为fuse最多使用32个page,也就是32*4K,前端的每次读写,都会转换成最大128K的小io
-o big_writes -o max_write=N // N就是期望kernel fuse的io大小。不过最大不能超过128K
  • 4 fuse的协议成本,fuse使用另外的80bytes作为控制字段,数据传输变多了,也需要额外的CPU时间来decode控制字段,再根据decode结果,对应不同的处理

FUSE 相对于内核态的文件系统,效率损耗大概在 10% - 20%?

fuse开发

libfuse提供了两套APIs:一个“high-level”同步API;一个“low-level” 异步API
这两种API 都从内核接收请求传递到主程序(fuse_main函数),主程序使用相应的回调函数进行处理。
【fuse用户空间的daemon从/fuse/dev中读取到请求之后,它通过请求号来判别各个请求,并调用这里相应的处理函数】

使用high-level API时,回调函数参数 使用文件名(file names)和路径(paths),而不是索引节点inodes
使用low-level API 时,回调函数参数 使用索引节点inode

参考

https://github.com/skyatlantis/skyatlantis.github.io/blob/master/_posts/2019-05-09-Ceph-Fuse性能调优.md

参考
https://xie.infoq.cn/article/655c0893ed150ff65f2b7a16f
https://blog.csdn.net/weixin_39733146/article/details/111142537
https://zhuanlan.zhihu.com/p/106719192
https://zhuanlan.zhihu.com/p/59354174

posted @ 2024-01-10 17:40  LdreamerD  阅读(52)  评论(0)    收藏  举报