前面我们学习了VFS的框架,VFS被夹在两层之间:上层和下层。上层是系统调用层,在这个层中,用户空间进程进入内核请求服务(这通常通过libc包装器函数完成),较低的一层是一组函数指针,每个文件系统实现一组,当VFS需要执行需要特定文件系统特定信息的操作时,它会调用该操作。
VFS最重要的对象
- filesystem types: 文件系统类型
- superblock: 整个文件系统的元信息
- inode: 单个文件的元信息
- dentry: 目录项,一个文件目录对应一个dentry
- file: 进程打开的一个文件
如上图,VFS后的文件系统实现具体有以下几种
- 基于块设备的文件系统(Block-based FS) :ext2-4, btrfs, ifs, xfs, iso9660, gfs, ocfs, …基于物理存储设备的文件系统,用来管理设备的存储空间
- 网络文件系统(Network FS) :NFS, coda, smbfs, ceph, …用于访问网络中其他设备上的文件。网络文件系统的目标是网络设备,所以它不会调用系统的Block层
- 伪文件系统(Pseudo FS) :proc, sysfs, pipefs, futexfs, usbfs, …因为并不管理真正的存储空间,所以被称为伪文件系统。它组织了一些虚拟的目录和文件,通过这些文件可以访问系统或硬件的数据。它不是用来存储数据的,而是把数据包装成文件用来访问,所以不能把伪文件系统当做存储空间来操作。
- 特殊文件系统(Special Purpose FS) :tmpfs, ramfs, devtmpfs,特殊文件系统也是一种伪文件系统,它使用起来更像是一个磁盘文件系统,但读写通常是内存而不是磁盘设备。
- 堆栈式文件系统(Stackable FS) :ecryptfs(加密文件系统), overlayfs(不直接参与磁盘空间结构的划分,仅将原来文件系统中不同目录和文件进行“合并”), unionfs(联合文件系统), wrapfs,叠加在其他文件系统之上的一种文件系统,本身不存储数据,而是对下层文件的扩展。
- 用户空间文件系统(FUSE): 它提供一种方式可以让开发者在用户空间实现文件系统,而不需要修改内核。这种方式更加灵活,但效率会更低。FUSE 直接面向的是用户文件系统,也不会调用Block层。
本章的重点在关注与用户文件系统FUSE,本文主要是针对的Linux4.9.88内核源码,主要的介绍内容如下:
- FUSE的基本概念
- 为什么需要FUSE
- FUSE的实现原理
- 如何使用FUSE
- 常见的FUSE实现有哪些
1 Fuse的基本概念
为什么要强调用户空间呢?接触过Linux内核的同学大概会知道,文件系统一般是实现在内核里面的,比如,Ext4、Fat32等常见的文件系统,其代码都在内核中,而FUSE特殊之处就是,其文件系统的 核心逻辑 是在 用户空间 实现的。
FUSE 是 Filesystem in Userspace 的缩写,也就是常说的 用户态文件系统 。Linux内核官方文档对 FUSE 的解释如下:
What is FUSE?FUSE is a userspace filesystem framework. It consists of a kernel module (fuse.ko), a userspace library (libfuse.*) and a mount utility (fusermount).
fuse内核模块的支持,开发者只需要根据fuse提供的接口实现具体的文件操作就可以实现一个文件系统。由于其主要实现代码位于用户空间中,而不需要重新编译内核,这给开发者带来了众多便利。
2 为什么需要FUSE
文件系统是应用程序访问其数据的最古老的常见方式之一,基于宏内核的文件系统是位于内核之中,处于VFS之下,块设备之上的位置。其作用是对上呈现文件存储实现,对下管理块设备。当时基于为内核思想的操作系统,一些文件系统是在用户龙剑中实现,尽管用户空间文件系统并没有完全取代内核级文件系统,而且此时假设它是不正确和为时过早的,但用户空间文件系统无疑占据了越来越大的位置。
慢慢地,随着时间的推移,用户文件系统越来越流行,其主要经历过
- 许多在基于文件系统之上添加专门功能的用户空间可堆叠的文件系统获得了普及,例如,重复数据删除和压缩文件系统
- 几个现有的内核文件系统被移植到用户空间,例如ZFS,NTFS,一些人试图将部分文件系统作为专门解决方案
- 越来越多的公司依赖用户空间实现其存储产品,IBM 的 GPFS [ 47 ] 和 LTFS [ 43 ]、Nimble Storage 的 CASL [ 41 ]、Apache 的 HDFS [ 4 ]、Google 文件系统 [ 26 ] ]、RedHat 的 GlusterFS [ 46 ]、Data Domain 的重复数据删除文件系统 [ 63 ] 等等。一些在用户空间实现文件系统,使用 Google Drive、Amazon S3 [ 45 ] 和 DropBox [ 38 ] 等服务在云中在线存储数据。
客户不断需要存储解决新功能的方案(快照、加密等),随着软件定义存储范式的不断出现,文件系统的复杂度越来越高,所以用户空间是开发、移植和维护代码的更友好的环境。所以基于此在用户空间实现文件系统有以下优点
- 开发效率高: 方便调式,许多用于跟踪、调试和分析用户程序的用户空间工具,不会出现因为内核态出现的bug就导致OS宕机而重启动的情况,所有现场都丢失,你只能通过日志,kdump 等手段来排查,而用户态的程序你可以随意 debug
- 更易于开发,可以快速试验新的文件系统,更容易快速实现新的功能
- 由于处于应用开发,有大量的第三方库可以使用,开发人员不仅限于几种面向系统的编程语言(例如,C),还可以轻松使用几种更高级的语言,每种语言都最适合其目标,可以更轻松地尝试新的和更有效的算法来实现文件系统,以提高性能。
- 可移植性: 对于需要在多个平台上运行的文件系统,在用户空间中开发可移植代码要比在内核中容易得多。
当然,在用户空间能做的一切都可以在内核中实现。但是为了使开发随着文件系统的复杂性保持可扩展性,许多公司更喜欢用户空间实现。但是在用户空间开发文件系统,一直也像微内核一样,面临着性能问题,在用户空间实现引起的性能开销有多大也是用户文件系统的争议点。其缺点如下:
- 访问路径长,需要用户态和内核态频繁的切换
- IO吞吐量较低
- 增加了数据的copy
3 FUSE的实现原理
fuse主要由三部分组成:FUSE内核模块、用户空间库libfuse以及挂载工具fusermount
- 一个内核模块: 加载时被注册成 Linux 虚拟文件系统的一个 fuse 文件系统驱动。实现和VFS的对接,实现一个能被用户空间打开的设备。该块设备作为fuse daemon与内核通信的桥梁,fuse daemon通过/dev/fuse读取fuse request,处理后将reply写入/dev/fuse。
- 基于用户空间库libfuse一个 用户空间守护进程 (下文称fuse daemon):负责和内核空间通信,接收来自/dev/fuse的请求,并将其转化为一系列的函数调用,将结果写回到/dev/fuse
- 挂载工具:实现对用户态文件系统的挂载
下面这张图体现了FUSE工作的基本套路,是根据WIki里的画的,这张图感觉更符合我看到的代码的状况。
一个用户态文件系统,挂载点在/tmp/fuse,用户进程为hello,当执行ls -l /tmp/fuse命令的时候,其流程如下:
- IO先进内核,经过VFS传递给内核的FUSE文件系统模块
- 内核FUSE模块把请求发送给用户态,由hello程序接受并处理,处理完成后,响应原路返回
简化的 IO 动画示意图:
内核 FUSE 模块在内核态中间做协议封装和协议解析的工作,它接收从VFS下来的请求并按照 FUSE 协议转发到用户态,然后接收用户态的响应,并随后回复给用户。FUSE在这条IO路径是做了一个透明中转站的作用,用户完全不感知这套框架。
内核 fuse.ko用于接收VFS下来的IO请求,然后封装成 FUSE 数据包,转发给用户态,其内核也是一个文件系统,其满足文件系统的几个数据结构
fs/fuse/inode.c —> 主要完成fuse文件驱动模块的注册,提供对supper block的维护函数以及其它(驱动的组织开始文件)
fs/fuse/dev.c —> fuse 的(虚拟)设备驱动
fs/fuse/control.c —> 提供对于dentry的维护及其它
fs/fuse/dir.c —> 主要提供对于目录inode索引节点的维护
fs/fuse/file.c —> 主要提供对于文件inode索引节点的维护
主要完成fuse文件驱动模块的注册
static int __init fuse_init(void)
{
int res;
printk(KERN_INFO "fuse init (API version %i.%i)\n",
FUSE_KERNEL_VERSION, FUSE_KERNEL_MINOR_VERSION);
// 1. 注册fuse文件系统,创建fuse_inode高速缓存
INIT_LIST_HEAD(&fuse_conn_list);
res = fuse_fs_init();
if (res)
goto err;
// 2. 创建fuse_req高速缓存,加载fuse设备驱动,用于用户空间与内核空间交换信息
// 创建设备文件/dev/fuse
res = fuse_dev_init();
if (res)
goto err_fs_cleanup;
// 3. 在/sys/fs目录下增加fuse节点,在fuse节点下增加connections节点
res = fuse_sysfs_init();
if (res)
goto err_dev_cleanup;
// 4. 注册fuse控制文件系统, 用于查看某个连接的请求情况或者强制结束一个连接
res = fuse_ctl_init();
if (res)
goto err_sysfs_cleanup;
sanitize_global_limit(&max_user_bgreq);
sanitize_global_limit(&max_user_congthresh);
return 0;
err_sysfs_cleanup:
fuse_sysfs_cleanup();
err_dev_cleanup:
fuse_dev_cleanup();
err_fs_cleanup:
fuse_fs_cleanup();
err:
return res;
}
我们按照写一个文件系统的几个步骤,首先需要在内核中注册一个file_system_type,其实现如下:
static int __init fuse_fs_init(void)
{
int err;
fuse_inode_cachep = kmem_cache_create("fuse_inode",
sizeof(struct fuse_inode), 0,
SLAB_HWCACHE_ALIGN|SLAB_ACCOUNT,
fuse_inode_init_once);
err = -ENOMEM;
if (!fuse_inode_cachep)
goto out;
//return register_filesystem(&fuseblk_fs_type);
err = register_fuseblk();
if (err)
goto out2;
err = register_filesystem(&fuse_fs_type);
if (err)
goto out3;
return 0;
out3:
unregister_fuseblk();
out2:
kmem_cache_destroy(fuse_inode_cachep);
out:
return err;
}
FUSE模块加载注册了fuseblk_fs_type和fuse_fs_type两种文件类型,默认情况下使用的是fuse_fs_type即mount 函数指针被初始化为fuse_mount, 而fuse_mount实际调用mount_nodev
static struct file_system_type fuse_fs_type = {
.owner = THIS_MODULE,
.name = "fuse",
.fs_flags = FS_HAS_SUBTYPE,
.mount = fuse_mount,
.kill_sb = fuse_kill_sb_anon,
};
然后提供super_block,文件系统的总体信息,会提供super_operations相关结构体,这个是在fuse_mount接口中完成初始化,详细的流程就不详细介绍,其基本都类似
static const struct super_operations fuse_super_operations = {
.alloc_inode = fuse_alloc_inode,
.destroy_inode = fuse_destroy_inode,
.evict_inode = fuse_evict_inode,
.write_inode = fuse_write_inode,
.drop_inode = generic_delete_inode,
.remount_fs = fuse_remount_fs,
.put_super = fuse_put_super,
.umount_begin = fuse_umount_begin,
.statfs = fuse_statfs,
.show_options = fuse_show_options,
};
然后提供inode的操作集和dentry的操作集,如下所示
void fuse_init_common(struct inode *inode)
{
inode->i_op = &fuse_common_inode_operations;
}
void fuse_init_dir(struct inode *inode)
{
inode->i_op = &fuse_dir_inode_operations;
inode->i_fop = &fuse_dir_operations;
}
void fuse_init_symlink(struct inode *inode)
{
inode->i_op = &fuse_symlink_inode_operations;
}
static const struct address_space_operations fuse_file_aops = {
.readpage = fuse_readpage,
.writepage = fuse_writepage,
.writepages = fuse_writepages,
.launder_page = fuse_launder_page,
.readpages = fuse_readpages,
.set_page_dirty = __set_page_dirty_nobuffers,
.bmap = fuse_bmap,
.direct_IO = fuse_direct_IO,
.write_begin = fuse_write_begin,
.write_end = fuse_write_end,
};
void fuse_init_file_inode(struct inode *inode)
{
inode->i_fop = &fuse_file_operations;
inode->i_data.a_ops = &fuse_file_aops;
}
下面我们看看整个过程,当用户输入ls -l /tmp/fuse
回车后,这个时候ls会调用系统调用,kernel fuse模块接受到用户请求,会进入VFS处理,然后会根据这个分区的文件系统,找到对应文件系统的实现接口,这个时候会调用到内核提供的fuse驱动,具体的调用过程后面详细介绍。
const struct file_operations fuse_dev_operations = {
.owner = THIS_MODULE,
.open = fuse_dev_open,
.llseek = no_llseek,
.read_iter = fuse_dev_read,
.splice_read = fuse_dev_splice_read,
.write_iter = fuse_dev_write,
.splice_write = fuse_dev_splice_write,
.poll = fuse_dev_poll,
.release = fuse_dev_release,
.fasync = fuse_dev_fasync,
.unlocked_ioctl = fuse_dev_ioctl,
.compat_ioctl = fuse_dev_ioctl,
};
EXPORT_SYMBOL_GPL(fuse_dev_operations);
static struct miscdevice fuse_miscdevice = {
.minor = FUSE_MINOR,
.name = "fuse",
.fops = &fuse_dev_operations,
};
4 总结
用户文件系统会越来越广泛使用,目前andriod12中已经到了fuse的文件系统,Android 实现了自己的 FUSE 守护程序来拦截文件访问,实施额外的安全和隐私功能,并在运行时操作文件。