简介
FUSE 是一个用户空间文件系统框架。 它由内核模块 (fuse.ko)、用户空间库 (libfuse.*) 和安装实用程序 (fusermount) 组成。
FUSE 最重要的功能之一是允许安全、非特权安装。 这为文件系统的使用开辟了新的可能性
初始化
代码见:fs/fuse/inode.c
static int __init fuse_init(void)
{
int res;
pr_info("init (API version %i.%i)\n",
FUSE_KERNEL_VERSION, FUSE_KERNEL_MINOR_VERSION);
INIT_LIST_HEAD(&fuse_conn_list);
res = fuse_fs_init();
if (res)
goto err;
res = fuse_dev_init();
if (res)
goto err_fs_cleanup;
res = fuse_sysfs_init();
if (res)
goto err_dev_cleanup;
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;
}
- 初始化全局变量
fuse_conn_list
该变量保存所有的连接。 - 初始化fuse文件系统
- 创建fuse inode 缓存
- 注册fuse文件系统
- 初始化fuse设备
- 创建
fuse_req
对象缓存 - 注册fuse字符设备
- 创建
- 在sys目录创建fuse目录
- 注册fusectl文件系统
初始化fuse文件系统
首先在模块初始化的时候,会调用fuse_fs_init
函数,开始注册文件系统
static struct file_system_type fuse_fs_type = {
.owner = THIS_MODULE,
.name = "fuse",
.fs_flags = FS_HAS_SUBTYPE | FS_USERNS_MOUNT,
.init_fs_context = fuse_init_fs_context,
.parameters = fuse_fs_parameters,
.kill_sb = fuse_kill_sb_anon,
};
static int __init fuse_fs_init(void)
{
int err;
// ...
err = register_filesystem(&fuse_fs_type);
// ...
return 0;
}
在fuse_fs_type
中,我们主要关心init_fs_context
回调函数
static const struct fs_context_operations fuse_context_ops = {
.free = fuse_free_fc,
.parse_param = fuse_parse_param,
.reconfigure = fuse_reconfigure,
.get_tree = fuse_get_tree,
};
static int fuse_init_fs_context(struct fs_context *fc)
{
struct fuse_fs_context *ctx;
ctx = kzalloc(sizeof(struct fuse_fs_context), GFP_KERNEL);
if (!ctx)
return -ENOMEM;
// ...
fc->fs_private = ctx;
fc->ops = &fuse_context_ops;
return 0;
}
在执行挂载时,会调用这个回调函数,初始化fs_context,一般来说主要是初始化文件系统私有对象和fs_context函数指针。
这里我们看到在fuse_init_fs_context
函数中,将fuse_context_ops
赋值给了fs_context->ops
在fuse_context_ops
中我们主要关心get_tree
回调函数
在init_fs_context
之后,就会调用get_tree
用于生成文件系统的根节点和创建/初始化superblock。
在fuse_get_tree
中,可以看到一些前置判断。
可以看出,在mount参数中,fd/rootmode/user_id/group_id是必填参数。
static int fuse_get_tree(struct fs_context *fc)
{
struct fuse_fs_context *ctx = fc->fs_private;
if (!ctx->fd_present || !ctx->rootmode_present ||
!ctx->user_id_present || !ctx->group_id_present)
return -EINVAL;
return get_tree_nodev(fc, fuse_fill_super);
}
接下来,进入fuse_fill_super
函数
int fuse_ctl_add_conn(struct fuse_conn *fc)
{
struct dentry *parent;
char name[32];
if (!fuse_control_sb)
return 0;
parent = fuse_control_sb->s_root;
inc_nlink(d_inode(parent));
sprintf(name, "%u", fc->dev);
parent = fuse_ctl_add_dentry(parent, fc, name, S_IFDIR | 0500, 2,
&simple_dir_inode_operations,
&simple_dir_operations);
if (!parent)
goto err;
if (!fuse_ctl_add_dentry(parent, fc, "waiting", S_IFREG | 0400, 1,
NULL, &fuse_ctl_waiting_ops) ||
!fuse_ctl_add_dentry(parent, fc, "abort", S_IFREG | 0200, 1,
NULL, &fuse_ctl_abort_ops) ||
!fuse_ctl_add_dentry(parent, fc, "max_background", S_IFREG | 0600,
1, NULL, &fuse_conn_max_background_ops) ||
!fuse_ctl_add_dentry(parent, fc, "congestion_threshold",
S_IFREG | 0600, 1, NULL,
&fuse_conn_congestion_threshold_ops))
goto err;
return 0;
err:
fuse_ctl_remove_conn(fc);
return -ENOMEM;
}
int fuse_fill_super_common(struct super_block *sb, struct fuse_fs_context *ctx)
{
struct fuse_conn *fc = get_fuse_conn_super(sb);
int err;
// ...
err = fuse_ctl_add_conn(fc);
if (err)
goto err_unlock;
list_add_tail(&fc->entry, &fuse_conn_list);
// ...
return 0;
}
static int fuse_fill_super(struct super_block *sb, struct fs_context *fsc)
{
struct fuse_fs_context *ctx = fsc->fs_private;
struct file *file;
int err;
struct fuse_conn *fc;
err = -EINVAL;
file = fget(ctx->fd);
/*
* Require mount to happen from the same user namespace which
* opened /dev/fuse to prevent potential attacks.
*/
if ((file->f_op != &fuse_dev_operations) ||
(file->f_cred->user_ns != sb->s_user_ns))
goto err_fput;
ctx->fudptr = &file->private_data;
fc = kmalloc(sizeof(*fc), GFP_KERNEL);
fuse_conn_init(fc, sb->s_user_ns, &fuse_dev_fiq_ops, NULL);
// ...
err = fuse_fill_super_common(sb, ctx);
/*
* atomic_dec_and_test() in fput() provides the necessary
* memory barrier for file->private_data to be visible on all
* CPUs after this
*/
fput(file);
fuse_send_init(get_fuse_conn_super(sb));
return 0;
}
在fuse_fill_super
函数中的主要逻辑有
- 初始化fuse连接对象
fuse_conn
。fuse_conn_init
- 填充superblock。
fuse_fill_super_common
- 初始化文件系统根节点
- 创建连接对象sys文件。
fuse_ctl_add_conn
- 将连接对象加入全局列表。
list_add_tail(&fc->entry, &fuse_conn_list);
- 发送
FUSE_INIT
给用户空间。fuse_send_init(get_fuse_conn_super(sb));
在fuse_ctl_add_conn
函数中会 在fusectl文件系统下,创建以设备ID为名字的目录,在这个目录下包含waiting、abort、max_backgroud、congestion_threshold文件
之后会发送FUSE_INIT
消息到用户进程。所以fuse发送给用户进程的第一个消息一定是FUSE_INIT
。
总结
- 挂载fuse文件系统时,必须传入fd/rootmode/user_id/group_id四个参数
- 每挂载一次fuse文件系统,都会生成一个fuse连接对象
fuse_conn
,这个对象的作用是和用户空间交互。(后面会详细介绍)。 - 每挂载一个fuse文件系统,都会在fusectl挂载点上创建以设备号为名字的目录,该目录下包含waiting、abort、max_backgroud、congestion_threshold文件。
- 发送
FUSE_INIT
消息给用户进程。
注册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 = compat_ptr_ioctl,
};
EXPORT_SYMBOL_GPL(fuse_dev_operations);
static struct miscdevice fuse_miscdevice = {
.minor = FUSE_MINOR,
.name = "fuse",
.fops = &fuse_dev_operations,
};
int __init fuse_dev_init(void)
{
int err = -ENOMEM;
// ...
err = misc_register(&fuse_miscdevice);
return 0;
}
该函数主要是创建fuse
设备,并且设置对应的回调函数fuse_dev_operations
在挂载fuse文件系统时传入的fd就是打开该设备时返回的fd
int fd = open("/dev/fuse", O_RDWR);
在fuse_fill_super
函数中也会判断传入的fd是否正确
static int fuse_fill_super(struct super_block *sb, struct fs_context *fsc)
{
struct fuse_fs_context *ctx = fsc->fs_private;
struct file *file;
int err;
struct fuse_conn *fc;
err = -EINVAL;
file = fget(ctx->fd);
/*
* Require mount to happen from the same user namespace which
* opened /dev/fuse to prevent potential attacks.
*/
if ((file->f_op != &fuse_dev_operations) ||
(file->f_cred->user_ns != sb->s_user_ns))
goto err_fput;
// ...
}
创建fuse的sysfs文件系统挂载点
static int fuse_sysfs_init(void)
{
int err;
fuse_kobj = kobject_create_and_add("fuse", fs_kobj);
err = sysfs_create_mount_point(fuse_kobj, "connections");
return 0;
}
该函数创建如下目录,/sys/fs/fuse/connections
初始化fusectl文件系统
fusectl文件系统主要是用于监控和配置fuse
static int fuse_ctl_fill_super(struct super_block *sb, struct fs_context *fctx)
{
static const struct tree_descr empty_descr = {""};
struct fuse_conn *fc;
int err;
err = simple_fill_super(sb, FUSE_CTL_SUPER_MAGIC, &empty_descr);
if (err)
return err;
mutex_lock(&fuse_mutex);
BUG_ON(fuse_control_sb);
fuse_control_sb = sb;
list_for_each_entry(fc, &fuse_conn_list, entry) {
err = fuse_ctl_add_conn(fc);
if (err) {
fuse_control_sb = NULL;
mutex_unlock(&fuse_mutex);
return err;
}
}
mutex_unlock(&fuse_mutex);
return 0;
}
static int fuse_ctl_get_tree(struct fs_context *fc)
{
return get_tree_single(fc, fuse_ctl_fill_super);
}
static const struct fs_context_operations fuse_ctl_context_ops = {
.get_tree = fuse_ctl_get_tree,
};
static int fuse_ctl_init_fs_context(struct fs_context *fc)
{
fc->ops = &fuse_ctl_context_ops;
return 0;
}
static struct file_system_type fuse_ctl_fs_type = {
.owner = THIS_MODULE,
.name = "fusectl",
.init_fs_context = fuse_ctl_init_fs_context,
.kill_sb = fuse_ctl_kill_sb,
};
int __init fuse_ctl_init(void)
{
return register_filesystem(&fuse_ctl_fs_type);
}
这段逻辑比较简单,就是在初始化的时候,为已经存在的fuse_conn
,创建sysfs对应目录。
在这里会存在一个疑问,fusectl什么时候被挂载?
root@ubuntu:~# mount
# ...
fusectl on /sys/fs/fuse/connections type fusectl (rw,nosuid,nodev,noexec,relatime)
事实上是systemd做了这件事情
/usr/lib/systemd/system/sys-fs-fuse-connections.mount
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
[Unit]
Description=FUSE Control File System
Documentation=https://www.kernel.org/doc/Documentation/filesystems/fuse.txt
Documentation=https://www.freedesktop.org/wiki/Software/systemd/APIFileSystems
DefaultDependencies=no
ConditionPathExists=/sys/fs/fuse/connections
ConditionCapability=CAP_SYS_ADMIN
ConditionVirtualization=!private-users
Before=sysinit.target
# These dependencies are used to make certain that the module is fully
# loaded. Indeed udev starts this unit when it receives an uevent for the
# module but the kernel sends it too early, ie before the init() of the module
# is fully operational and /sys/fs/fuse/connections is created, see issue#17586.
After=modprobe@fuse.service
Requires=modprobe@fuse.service
[Mount]
What=fusectl
Where=/sys/fs/fuse/connections
Type=fusectl
Options=nosuid,nodev,noexec
总结
- 在fuse内核模块初始化时,会注册fuse文件系统、注册fuse字符设备、创建/sys/fs/fuse/connections挂载点、注册fusectl文件系统
- systemd 在 文件系统安装完毕后,会挂载fusectl文件系统到/sys/fs/fuse/connections目录上。
- 在挂载fuse文件系统前,需要先打开/dev/fuse设备,生成文件描述符(fd)。
- 在挂载fuse文件系统时,会将上一步生成的fd传入挂载参数中,除此之外还必须传rootmode、user_id、group_id参数。
- 挂载后会生成一个fuse连接对象
fuse_conn
,该对象用于和用户进程交互。 - 挂载后会在/sys/fs/fuse/connections,生成以设备号为名字的目录,该目录下包含waiting、abort、max_backgroud、congestion_threshold文件。
- 发送
FUSE_INIT
消息给用户进程。
打开文件
打开文件的代码在 fs/fuse/file.c
static int fuse_open(struct inode *inode, struct file *file)
{
return fuse_open_common(inode, file, false);
}
如何找到这个函数,大概调用逻辑如下
- 在打开文件前,需要先找到该文件,此时会调用目录的lookup函数。
- 挂载的时候会创建根目录,这个时候会设置根目录的inode文件操作函数
fuse_fill_super_common
–> fuse_get_root_inode
–> fuse_iget
–> fuse_init_inode
–> fuse_init_dir
static const struct inode_operations fuse_dir_inode_operations = {
.lookup = fuse_lookup,
// ..
};
void fuse_init_dir(struct inode *inode)
{
struct fuse_inode *fi = get_fuse_inode(inode);
inode->i_op = &fuse_dir_inode_operations;
inode->i_fop = &fuse_dir_operations;
}
接着跟踪fuse_lookup函数
fuse_lookup
–> fuse_lookup_name
–> fuse_iget
–> fuse_init_inode
–> fuse_init_file_inode
static const struct file_operations fuse_file_operations = {
.open = fuse_open,
};
void fuse_init_file_inode(struct inode *inode)
{
struct fuse_inode *fi = get_fuse_inode(inode);
inode->i_fop = &fuse_file_operations;
inode->i_data.a_ops = &fuse_file_aops;
}
最后定位到open回调函数是fuse_open
。
大家有兴趣可以翻阅代码,这里就不展开了。
打开文件操作
fuse_open
–> fuse_open_common
–> fuse_do_open
–> fuse_send_open
static void queue_request_and_unlock(struct fuse_iqueue *fiq,
struct fuse_req *req)
__releases(fiq->lock)
{
req->in.h.len = sizeof(struct fuse_in_header) +
fuse_len_args(req->args->in_numargs,
(struct fuse_arg *) req->args->in_args);
list_add_tail(&req->list, &fiq->pending);
fiq->ops->wake_pending_and_unlock(fiq);
}
static void __fuse_request_send(struct fuse_conn *fc, struct fuse_req *req)
{
struct fuse_iqueue *fiq = &fc->iq;
req->in.h.unique = fuse_get_unique(fiq);
/* acquire extra reference, since request is still needed
after fuse_request_end() */
__fuse_get_request(req);
queue_request_and_unlock(fiq, req);
request_wait_answer(fc, req);
/* Pairs with smp_wmb() in fuse_request_end() */
smp_rmb();
}
ssize_t fuse_simple_request(struct fuse_conn *fc, struct fuse_args *args)
{
struct fuse_req *req;
ssize_t ret;
req = fuse_get_req(fc, false);
/* Needs to be done after fuse_get_req() so that fc->minor is valid */
fuse_adjust_compat(fc, args);
fuse_args_to_req(req, args);
__fuse_request_send(fc, req);
ret = req->out.h.error;
if (!ret && args->out_argvar) {
BUG_ON(args->out_numargs == 0);
ret = args->out_args[args->out_numargs - 1].size;
}
fuse_put_request(fc, req);
return ret;
}
static int fuse_send_open(struct fuse_conn *fc, u64 nodeid, struct file *file,
int opcode, struct fuse_open_out *outargp)
{
struct fuse_open_in inarg;
FUSE_ARGS(args);
memset(&inarg, 0, sizeof(inarg));
inarg.flags = file->f_flags & ~(O_CREAT | O_EXCL | O_NOCTTY);
if (!fc->atomic_o_trunc)
inarg.flags &= ~O_TRUNC;
args.opcode = opcode;
args.nodeid = nodeid;
args.in_numargs = 1;
args.in_args[0].size = sizeof(inarg);
args.in_args[0].value = &inarg;
args.out_numargs = 1;
args.out_args[0].size = sizeof(*outargp);
args.out_args[0].value = outargp;
return fuse_simple_request(fc, &args);
}
通过源码可以看到最后会将请求放入 fuse_conn->iq.pending
列表中
此时,用户进程可以通过fd读取请求内容。
具体逻辑在 fs/fuse/dev.c:fuse_dev_do_read,这里的逻辑有点多,不贴代码了。有兴趣可以自行翻阅。
如果不需要等待响应则直接调用fuse_request_end
,如果需要等待响应则还要等待用户程序写入。
但无论如何都会调用到fuse_request_end
函数
void fuse_request_end(struct fuse_conn *fc, struct fuse_req *req)
{
struct fuse_iqueue *fiq = &fc->iq;
// ...
if (test_and_set_bit(FR_FINISHED, &req->flags))
goto put_request;
if (test_bit(FR_BACKGROUND, &req->flags)) {
// ...
} else {
/* Wake up waiter sleeping in request_wait_answer() */
wake_up(&req->waitq);
}
// ...
put_request:
fuse_put_request(fc, req);
}
最后会唤醒req->waitq
,也就是内核发送消息等待响应的进程。
fuse还对中断做了处理,这里没有分析,感兴趣可以看看,逻辑也比较简单。
总结
- 在挂载fuse文件系统时,会创建根目录,并且设置根目录的文件操作函数为
fuse_dir_inode_operations
- 当查找文件的时候,会调用
fuse_dir_inode_operations->lookup
函数,即fuse_lookup
- 最后也会为文件设置操作函数
fuse_file_operations
。具体的读写函数都在这里面。 - 以打开文件为例:
- 内核会生成一个
fuse_req
,该对象包含具体的操作(opcode)、文件的nodeid和请求参数。 - 之后会将req对象放入等待队列中
fc->iq.pending
,然后等待请求结束。 - 用户进程通过
/dev/fuse
的文件描述符读取fuse_req
对象,当该请求是不需要响应时,内核直接执行步骤5。否则需要等待用户进程完成响应逻辑后,将结果返回给内核。 - 内核将用户进程传入的参数写到
fuse_req->args->out_args
- 唤醒内核。内核处理用户进程返回。
- 内核会生成一个