FUSE文件系统

发布时间 2023-08-28 18:56:48作者: nisonGe

简介

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;
}
  1. 初始化全局变量fuse_conn_list该变量保存所有的连接。
  2. 初始化fuse文件系统
    1. 创建fuse inode 缓存
    2. 注册fuse文件系统
  3. 初始化fuse设备
    1. 创建fuse_req对象缓存
    2. 注册fuse字符设备
  4. 在sys目录创建fuse目录
  5. 注册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函数中的主要逻辑有

  1. 初始化fuse连接对象fuse_connfuse_conn_init
  2. 填充superblock。fuse_fill_super_common
    1. 初始化文件系统根节点
    2. 创建连接对象sys文件。fuse_ctl_add_conn
    3. 将连接对象加入全局列表。list_add_tail(&fc->entry, &fuse_conn_list);
  3. 发送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

总结

  1. 挂载fuse文件系统时,必须传入fd/rootmode/user_id/group_id四个参数
  2. 每挂载一次fuse文件系统,都会生成一个fuse连接对象fuse_conn,这个对象的作用是和用户空间交互。(后面会详细介绍)。
  3. 每挂载一个fuse文件系统,都会在fusectl挂载点上创建以设备号为名字的目录,该目录下包含waiting、abort、max_backgroud、congestion_threshold文件。
  4. 发送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

总结

  1. 在fuse内核模块初始化时,会注册fuse文件系统、注册fuse字符设备、创建/sys/fs/fuse/connections挂载点、注册fusectl文件系统
  2. systemd 在 文件系统安装完毕后,会挂载fusectl文件系统到/sys/fs/fuse/connections目录上。
  3. 在挂载fuse文件系统前,需要先打开/dev/fuse设备,生成文件描述符(fd)。
  4. 在挂载fuse文件系统时,会将上一步生成的fd传入挂载参数中,除此之外还必须传rootmode、user_id、group_id参数。
  5. 挂载后会生成一个fuse连接对象fuse_conn,该对象用于和用户进程交互。
  6. 挂载后会在/sys/fs/fuse/connections,生成以设备号为名字的目录,该目录下包含waiting、abort、max_backgroud、congestion_threshold文件。
  7. 发送FUSE_INIT消息给用户进程。

打开文件

打开文件的代码在 fs/fuse/file.c

static int fuse_open(struct inode *inode, struct file *file)
{
	return fuse_open_common(inode, file, false);
}

如何找到这个函数,大概调用逻辑如下

  1. 在打开文件前,需要先找到该文件,此时会调用目录的lookup函数。
  2. 挂载的时候会创建根目录,这个时候会设置根目录的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还对中断做了处理,这里没有分析,感兴趣可以看看,逻辑也比较简单。

总结

  1. 在挂载fuse文件系统时,会创建根目录,并且设置根目录的文件操作函数为fuse_dir_inode_operations
  2. 当查找文件的时候,会调用fuse_dir_inode_operations->lookup函数,即fuse_lookup
  3. 最后也会为文件设置操作函数fuse_file_operations。具体的读写函数都在这里面。
  4. 以打开文件为例:
    1. 内核会生成一个fuse_req,该对象包含具体的操作(opcode)、文件的nodeid和请求参数。
    2. 之后会将req对象放入等待队列中fc->iq.pending,然后等待请求结束。
    3. 用户进程通过/dev/fuse的文件描述符读取fuse_req对象,当该请求是不需要响应时,内核直接执行步骤5。否则需要等待用户进程完成响应逻辑后,将结果返回给内核。
    4. 内核将用户进程传入的参数写到fuse_req->args->out_args
    5. 唤醒内核。内核处理用户进程返回。