前面我们学习了VFS的框架,VFS被夹在两层之间:上层和下层。上层是系统调用层,在这个层中,用户空间进程进入内核请求服务(这通常通过libc包装器函数完成),较低的一层是一组函数指针,每个文件系统实现一组,当VFS需要执行需要特定文件系统特定信息的操作时,它会调用该操作。
如上图,VFS后的文件系统实现具体有以下几种
本章的重点在关注与用户文件系统FUSE,本文主要是针对的Linux4.9.88内核源码,主要的介绍内容如下:
为什么要强调用户空间呢?接触过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提供的接口实现具体的文件操作就可以实现一个文件系统。由于其主要实现代码位于用户空间中,而不需要重新编译内核,这给开发者带来了众多便利。
文件系统是应用程序访问其数据的最古老的常见方式之一,基于宏内核的文件系统是位于内核之中,处于VFS之下,块设备之上的位置。其作用是对上呈现文件存储实现,对下管理块设备。当时基于为内核思想的操作系统,一些文件系统是在用户龙剑中实现,尽管用户空间文件系统并没有完全取代内核级文件系统,而且此时假设它是不正确和为时过早的,但用户空间文件系统无疑占据了越来越大的位置。
慢慢地,随着时间的推移,用户文件系统越来越流行,其主要经历过
客户不断需要存储解决新功能的方案(快照、加密等),随着软件定义存储范式的不断出现,文件系统的复杂度越来越高,所以用户空间是开发、移植和维护代码的更友好的环境。所以基于此在用户空间实现文件系统有以下优点
当然,在用户空间能做的一切都可以在内核中实现。但是为了使开发随着文件系统的复杂性保持可扩展性,许多公司更喜欢用户空间实现。但是在用户空间开发文件系统,一直也像微内核一样,面临着性能问题,在用户空间实现引起的性能开销有多大也是用户文件系统的争议点。其缺点如下:
fuse主要由三部分组成:FUSE内核模块、用户空间库libfuse以及挂载工具fusermount
下面这张图体现了FUSE工作的基本套路,是根据WIki里的画的,这张图感觉更符合我看到的代码的状况。
一个用户态文件系统,挂载点在/tmp/fuse,用户进程为hello,当执行ls -l /tmp/fuse命令的时候,其流程如下:
简化的 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,
};
用户文件系统会越来越广泛使用,目前andriod12中已经到了fuse的文件系统,Android 实现了自己的 FUSE 守护程序来拦截文件访问,实施额外的安全和隐私功能,并在运行时操作文件。
Performance and Resource Utilization of FUSE User-Space File Systems Performance and Resource Utilization of FUSE User-Space File Systems