FUSE

投稿者: | 2024年2月17日

参考文献

FUSE – George’s Blog (georgesims21.github.io)

アーキ

FUSEドライバとFUSEデーモンからなる。

FUSEドライバは/dev/fuseと名前のmiscキャラクタデバイスを作成し、FUSEデーモンと通信する。miscキャラクタデバイスにはdev.cでfuse_dev_operationsが定義されて関数が紐づけられている。fuse_dev_operationsの関数は、FUSEデーモンからのシステムコールに応じてfuse_reqを制御する。

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);

FUSEデーモンはFUSEライブラリを介してFUSEドライバとのやりとりと、登録されたlowlevelまたはhighlevelのユーザ定義関数を呼び出す。

FUSEドライバ5種類のキューを管理し、それぞれ別の目的で使用する。これらはfuse_dev_readでキューを読み出す際の優先度付けされる。

  • Interrupts (highest priority) – For all interrupt requests
  • Forgets – For all forget requests
  • Pending – Latency sensitive requests (related to metadata)
  • Background – All other requests (read/write etc)。リード、ライト要求はBackgroudキューを使う。
  • Processing – The requests that are currently being processed by the daemon
/FUSE/FUSE-queue-structure.jpg

FUSEのコマンド処理

アプリからのコマンド処理は以下の関数フローによりしょりされる。
FUSE — The Linux Kernel documentation

|  "rm /mnt/fuse/file"               |  FUSE filesystem daemon
|                                    |
|                                    |  >sys_read()
|                                    |    >fuse_dev_read() ★ /dev/fuseに対しブロッキングモードのread。
|                                    |      >request_wait()
|                                    |        [sleep on fc->waitq]
|                                    |
|  >sys_unlink()                     |
|    >fuse_unlink()                  |
|      [get request from             |
|       fc->unused_list]             |
|      >request_send()               |
|        [queue req on fc->pending]  |
|        [wake up fc->waitq]         |        [woken up]
|        >request_wait_answer()      |
|          [sleep on req->waitq]     |
|                                    |      <request_wait()
|                                    |      [remove req from fc->pending]
|                                    |      [copy req to read buffer] ★read応答でfuse_reqをユーザ空間にコピー。。
|                                    |      [add req to fc->processing]
|                                    |    <fuse_dev_read()
|                                    |  <sys_read()
|                                    |
|                                    |  [perform unlink]
|                                    |
|                                    |  >sys_write() ★ackを返す。
|                                    |    >fuse_dev_write()
|                                    |      [look up req in fc->processing]
|                                    |      [remove from fc->processing]
|                                    |      [copy write buffer to req]
|          [woken up]                |      [wake up req->waitq]
|                                    |    <fuse_dev_write()
|                                    |  <sys_write()
|        <request_wait_answer()      |
|      <request_send()               |
|      [add request to               |
|       fc->unused_list]             |
|    <fuse_unlink()                  |
|  <sys_unlink()                     |

Filesystem in Userspace (FUSE) のカーネルとデーモン間の通信 #Linux – Qiita

FUSEデーモンとFUSEドライバ間の通信

FUSEライブラリの処理

FUSEライブラリの本体ははfuse_loop_mt.cのdo_workのwhile文になる。

static void *fuse_do_work(void *data)
{
	struct fuse_worker *w = (struct fuse_worker *) data;
	struct fuse_mt *mt = w->mt;

	while (!fuse_session_exited(mt->se)) {
		int isforget = 0;
		int res;

		pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
		res = fuse_session_receive_buf_int(mt->se, &w->fbuf, w->ch);★Filesystem Daemon は libfuse を通して /dev/fuse を read しブロックされた状態で操作リクエストを待つ
		pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
		if (res == -EINTR)
			continue;
		if (res <= 0) {
			if (res < 0) {
				fuse_session_exit(mt->se);
				mt->error = res;
			}
			break;
		}

		pthread_mutex_lock(&mt->lock);
		if (mt->exit) {
			pthread_mutex_unlock(&mt->lock);
			return NULL;
		}

		/*
		 * This disgusting hack is needed so that zillions of threads
		 * are not created on a burst of FORGET messages
		 */
		if (!(w->fbuf.flags & FUSE_BUF_IS_FD)) {
			struct fuse_in_header *in = w->fbuf.mem;

			if (in->opcode == FUSE_FORGET ||
			    in->opcode == FUSE_BATCH_FORGET)
				isforget = 1;
		}

		if (!isforget)
			mt->numavail--;
		if (mt->numavail == 0 && mt->numworker < mt->max_threads)
			fuse_loop_start_thread(mt);
		pthread_mutex_unlock(&mt->lock);

		fuse_session_process_buf_int(mt->se, &w->fbuf, w->ch);★操作リクエストがあると read の内容としてリクエストの情報が読まれ、libfuse が定義された関数を呼び出す

		pthread_mutex_lock(&mt->lock);
		if (!isforget)
			mt->numavail++;

		/* creating and destroying threads is rather expensive - and there is
		 * not much gain from destroying existing threads. It is therefore
		 * discouraged to set max_idle to anything else than -1. If there
		 * is indeed a good reason to destruct threads it should be done
		 * delayed, a moving average might be useful for that.
		 */
		if (mt->max_idle != -1 && mt->numavail > mt->max_idle && mt->numworker > 1) {
			if (mt->exit) {
				pthread_mutex_unlock(&mt->lock);
				return NULL;
			}
			list_del_worker(w);
			mt->numavail--;
			mt->numworker--;
			pthread_mutex_unlock(&mt->lock);

			pthread_detach(w->thread_id);
			free(w->fbuf.mem);
			fuse_chan_put(w->ch);
			free(w);
			return NULL;
		}
		pthread_mutex_unlock(&mt->lock);
	}

	sem_post(&mt->finish);

	return NULL;
}
  1. Filesystem Daemon は libfuse を通して /dev/fuse を read しブロックされた状態で操作リクエストを待つ (fuse_session_receive_buf_int)
  2. 操作リクエストがあると read の内容としてリクエストの情報が読まれ、fuse_lowlevel.cでfuse_ll_opsに定義した関数を呼び出す (fuse_session_process_buf_int)。例えば、リクエスト種別がREADの場合、do_readが、WRITEの場合はdo_writeが呼ばれる。
  3. 操作の結果を /dev/fuse に write で書き込む

image.png

内部的には fuse_conn という構造体で、Filesystem Daemon とのコネクションを管理しています。このコネクションは Filesystem Daemon ごとに作成されます。コネクションは fuse_iqueue という入力キューを持っており、ファイル操作のリクエストを管理しています。

このコネクションの状態の一部は /sys/fs/fuse/connections/ で確認できる(fs/fuse/control.c)。

FUSEドライバの処理(linux/fs/fuse)

アプリとの通信

アプリとFUSEドライバ間の通信処理本体は、file.cでfile_operationsとして定義された割り当てられたfuse_file_operationsである。

static const struct file_operations fuse_file_operations = {
	.llseek		= fuse_file_llseek,
	.read_iter	= fuse_file_read_iter,
	.write_iter	= fuse_file_write_iter,
	.mmap		= fuse_file_mmap,
	.open		= fuse_open,
	.flush		= fuse_flush,
	.release	= fuse_release,
	.fsync		= fuse_fsync,
	.lock		= fuse_file_lock,
	.get_unmapped_area = thp_get_unmapped_area,
	.flock		= fuse_file_flock,
	.splice_read	= filemap_splice_read,
	.splice_write	= iter_file_splice_write,
	.unlocked_ioctl	= fuse_file_ioctl,
	.compat_ioctl	= fuse_file_compat_ioctl,
	.poll		= fuse_file_poll,
	.fallocate	= fuse_file_fallocate,
	.copy_file_range = fuse_copy_file_range,
};
Writeデータコピー

上位からのFile Write op処理はfuse_file_write_iter→fuse_cache_write_iterfuse_perform_writefuse_send_write_pagesとなる。Writeデータは、fuse_perform_writeで、ユーザ空間アクセス用に一時ページを割当と(fuse_pages_alloc)、コピー(fuse_fill_write_pages)する。

FUSEデバイスからユーザ空間のFUSEデーモンのWRITE要求の引数はfuse_write_args_fillで作成する。

static void fuse_write_args_fill(struct fuse_io_args *ia, struct fuse_file *ff,
				 loff_t pos, size_t count)
{
	struct fuse_args *args = &ia->ap.args;

	ia->write.in.fh = ff->fh;
	ia->write.in.offset = pos;  ★一時メモリ領域
	ia->write.in.size = count;
	args->opcode = FUSE_WRITE;
	args->nodeid = ff->nodeid;
	args->in_numargs = 2; ★WRITEの引数は2個。
	if (ff->fm->fc->minor < 9)
		args->in_args[0].size = FUSE_COMPAT_WRITE_IN_SIZE;
	else
		args->in_args[0].size = sizeof(ia->write.in);
	args->in_args[0].value = &ia->write.in;
	args->in_args[1].size = count;
	args->out_numargs = 1; ★WRITEの戻り値数は1個。
	args->out_args[0].size = sizeof(ia->write.out);
	args->out_args[0].value = &ia->write.out;
}

WRITE要求は、FUSEデーモンのreadシステムコールの延長のfuse_dev_do_readでキューからデキューされる。WRITEデータはFUSEデーモンに対しreadの応答としてユーザ空間にコピーされる。


FUSEデーモンとの通信

FUSEデーモンとFUSEドライバ間の通信処理本体は、dev.cでmiscキャラクタデバイスに割り当てられたfuse_dev_operationsである。

const struct file_operations fuse_dev_operations = {
	.owner		= THIS_MODULE,
	.open		= fuse_dev_open,
	.llseek		= no_llseek,
	.read_iter	= fuse_dev_read, ★FUSEデーモンからのリクエスト要求を処理。
	.splice_read	= fuse_dev_splice_read,
	.write_iter	= fuse_dev_write, ★FUSEデーモンからのリクエスト応答を処理。
	.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);

要求処理はfuse_dev_do_readで行われる。fuse_dev_do_readでは、FUSEドライバは優先度順にキューを探索し、リクエスト要求があればFUSEデーモンに通知する。

FUSEデーモンのWRITE処理

Write処理ではFUSEデーモンはカーネル空間のデータをユーザ空間に読み出す必要がある。読出し処理はユーザが開発したFUSEデーモン内でFUSEライブラリ関数を呼び出すことで行う。以下はpassthrough_llサンプル処理である。

static void lo_write_buf(fuse_req_t req, fuse_ino_t ino,
			 struct fuse_bufvec *in_buf, off_t off,
			 struct fuse_file_info *fi)
{
	(void) ino;
	ssize_t res;
	struct fuse_bufvec out_buf = FUSE_BUFVEC_INIT(fuse_buf_size(in_buf));

	out_buf.buf[0].flags = FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK;★パススルー先バッファ種別をFDに設定。
	out_buf.buf[0].fd = fi->fh;
	out_buf.buf[0].pos = off;

	if (lo_debug(req))
		fuse_log(FUSE_LOG_DEBUG, "lo_write(ino=%" PRIu64 ", size=%zd, off=%lu)\n",
			ino, out_buf.buf[0].size, (unsigned long) off);

	res = fuse_buf_copy(&out_buf, in_buf, 0); ★ユーザ空間のデータ読出しから、パススルーライト処理の入り口
	if(res < 0)
		fuse_reply_err(req, -res);
	else
		fuse_reply_write(req, (size_t) res);
}

ssize_t fuse_buf_copy(struct fuse_bufvec *dstv, struct fuse_bufvec *srcv,
		      enum fuse_buf_copy_flags flags)
{
	size_t copied = 0;

	if (dstv == srcv)
		return fuse_buf_size(dstv);

	for (;;) {
		const struct fuse_buf *src = fuse_bufvec_current(srcv);
		const struct fuse_buf *dst = fuse_bufvec_current(dstv);
		size_t src_len;
		size_t dst_len;
		size_t len;
		ssize_t res;

		if (src == NULL || dst == NULL)
			break;

		src_len = src->size - srcv->off;
		dst_len = dst->size - dstv->off;
		len = min_size(src_len, dst_len);

		res = fuse_buf_copy_one(dst, dstv->off, src, srcv->off, len, flags);★読出し・書き出し処理本体
		if (res < 0) {
			if (!copied)
				return res;
			break;
		}
		copied += res;

		if (!fuse_bufvec_advance(srcv, res) ||
		    !fuse_bufvec_advance(dstv, res))
			break;

		if (res < len)
			break;
	}

	return copied;
}

static ssize_t fuse_buf_copy_one(const struct fuse_buf *dst, size_t dst_off,
				 const struct fuse_buf *src, size_t src_off,
				 size_t len, enum fuse_buf_copy_flags flags)
{
	int src_is_fd = src->flags & FUSE_BUF_IS_FD;
	int dst_is_fd = dst->flags & FUSE_BUF_IS_FD;

	if (!src_is_fd && !dst_is_fd) {
		char *dstmem = (char *)dst->mem + dst_off;
		char *srcmem = (char *)src->mem + src_off;

		if (dstmem != srcmem) {
			if (dstmem + len <= srcmem || srcmem + len <= dstmem)
				memcpy(dstmem, srcmem, len);
			else
				memmove(dstmem, srcmem, len);
		}

		return len;
	} else if (!src_is_fd) {
		return fuse_buf_write(dst, dst_off, src, src_off, len);★ここ。srcはfdではない(TBD:確認)。
	} else if (!dst_is_fd) {
		return fuse_buf_read(dst, dst_off, src, src_off, len);
	} else if (flags & FUSE_BUF_NO_SPLICE) {
		return fuse_buf_fd_to_fd(dst, dst_off, src, src_off, len);
	} else {
		return fuse_buf_splice(dst, dst_off, src, src_off, len, flags);
	}
}

static ssize_t fuse_buf_write(const struct fuse_buf *dst, size_t dst_off,
			      const struct fuse_buf *src, size_t src_off,
			      size_t len)
{
	ssize_t res = 0;
	size_t copied = 0;

	while (len) {
		if (dst->flags & FUSE_BUF_FD_SEEK) {
			res = pwrite(dst->fd, (char *)src->mem + src_off, len,
				     dst->pos + dst_off);
		} else {
			res = write(dst->fd, (char *)src->mem + src_off, len);
		}
		if (res == -1) {
			if (!copied)
				return -errno;
			break;
		}
		if (res == 0)
			break;

		copied += res;
		if (!(dst->flags & FUSE_BUF_FD_RETRY))
			break;

		src_off += res;
		dst_off += res;
		len -= res;
	}

	return copied;
}

<削除予定>
以下ソース解析結果を示す。なお、ソース解析では生成AIをつかっているため、間違いが含まれる可能性がある。

カーネル→FUSEデーモン

カーネルはfuse_dev_write 関数より/dev/fuseへの書き込み操作を通じてFUSEデーモンに要求を出す。

1. 要求の構築: カーネルは、ファイルシステム操作に関連するデータを含む fuse_req 構造体を構築する。以下は、fuse_req 構造体を初期化し、要求を設定する一般的なコードの例。

struct fuse_req *req;
req = fuse_request_alloc();
if (!req)
    return -ENOMEM;

req->in.h.opcode = FUSE_READ;  // 要求の種類を設定
req->in.h.nodeid = inode->i_ino;  // inode番号を設定
req->in.numargs = 1;  // 引数の数を設定

/* リクエストにデータを設定 */
req->in.args[0].size = size;
req->in.args[0].value = buffer;

/* FUSEデーモンに要求を送信 */
fuse_request_send(fc, req);

2. データの準備: fuse_req には、操作の種類、対象のinode、必要なデータなどを含める。

3. fuse_reqのキューイング_fuse_simple_request関数を通じてfuse_reqをキューイングする。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fuse_lowlevel.h>

void send_read_request(fuse_req_t req, fuse_ino_t inode, size_t size, off_t offset, struct fuse_file_info *fi) {
    struct fuse_bufvec buf = FUSE_BUFVEC_INIT(size);

    // バッファを確保
    buf.buf[0].mem = malloc(size);
    if (buf.buf[0].mem == NULL) {
        fuse_reply_err(req, ENOMEM);
        return;
    }

    // リードリクエストのパラメータを設定
    buf.buf[0].size = size;
    buf.buf[0].flags = FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK;
    buf.buf[0].fd = fi->fh;
    buf.buf[0].pos = offset;

    // カーネルにリードリクエストを送信
    fuse_reply_data(req, &buf, FUSE_BUF_COPY_FLAGS_SPLICE_MOVE);

    // バッファのメモリを解放
    free(buf.buf[0].mem);
}

// この関数はFUSEのreadコールバックとして登録される
void fuse_read_callback(fuse_req_t req, fuse_ino_t ino, size_t size, off_t offset, struct fuse_file_info *fi) {
    printf("Read request received: inode=%lu, size=%zu, offset=%ld\n", (unsigned long)ino, size, offset);
    send_read_request(req, ino, size, offset, fi);
}

FUSEデーモン→カーネル

ユーザスペースのFUSEデーモンは、以下の方法でカーネルにリクエストを出す。

1. デバイスファイルの操作: FUSEデーモンは、/dev/fuseという特殊なデバイスファイルを介してカーネルと通信する。

2. リクエストの書き込み: FUSEデーモンは、readシステムコールを/dev/fuseに対して発行する。カーネルからfuse_reqがない場合、ブロックされる。fuse_reqは、操作の種類(例えば、ファイルの読み取り、書き込み、属性の取得など)を示すオペコードと、必要なパラメータを含むデータ構造で構成される。

カーネルの処理: カーネル側のFUSEモジュールは、/dev/fuseデバイスからリクエストを読み取り、それを解析して適切なファイルシステム操作を行う。この処理は、FUSEカーネルモジュール内で行われ、必要に応じてユーザスペースのデーモンに追加情報を要求することがある。

FUSEのキューの実装は、入力キュー(struct fuse_iqueue) と 処理キュー(struct fuse_pqueue) を通じて行われる。キューへの要求の追加や取り出しは、queue_request_and_unlock や fuse_dev_wake_and_unlock などの関数を通じて行われる。

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);
}

queue_request_and_unlockはstruct fuse_req のリストに要求を追加し(list_add_tail)、その後 wake_pending_and_unlock を呼び出して、キューに新しい要求が追加されたことを待機中のスレッドに通知する。

static void fuse_dev_wake_and_unlock(struct fuse_iqueue *fiq)
__releases(fiq->lock)
{
	wake_up(&fiq->waitq);
	kill_fasync(&fiq->fasync, SIGIO, POLL_IN);
	spin_unlock(&fiq->lock);
}

fuse_dev_do_read 関数は、FUSE (Filesystem in Userspace) システムにおいて、カーネルがユーザースペースの FUSE デーモンから送信された要求を読み取る。

fuse_dev_do_read 関数は、FUSEデバイスからの読み取り要求を処理するための関数であり、以下のステップで構成されている。

1. バッファサイズの検証: 読み取りバッファのサイズが最小要件を満たしているか確認し、不足している場合は -EINVAL を返す。

2. リクエストの待機と取得: リクエストが利用可能になるまで待機し、利用可能なリクエストをキューから取得する。非ブロッキングモードが有効な場合は、リクエストがなければ -EAGAIN を返す。

3. リクエストの種類に応じた処理:割り込みリクエストがある場合、fuse_read_interrupt を呼び出す。忘れられたリクエスト(forget)がある場合、fuse_read_forget を呼び出す。通常のリクエストの場合、リクエストデータをユーザースペースにコピーする。

4. データのコピー: fuse_copy_one 関数を使用して、リクエストヘッダーと引数のデータをユーザースペースにコピーする。

5. エラーハンドリング: コピー中にエラーが発生した場合は、適切なクリーンアップを行い、エラーコードを返す。

6. リクエストの完了: リクエストが正常に処理された場合、そのサイズを返して読み取り操作を完了する。

static ssize_t fuse_dev_do_read(struct fuse_dev *fud, struct file *file,
                                struct fuse_copy_state *cs, size_t nbytes) {
    if (nbytes < MIN_REQUIRED_SIZE) {
        return -EINVAL;
    }

    wait_for_request();

    struct fuse_req *req = get_next_request();
    if (!req) {
        return -EAGAIN;
    }

    int err = copy_request_to_user(req, cs);
    if (err) {
        handle_error(err);
        return err;
    }

    complete_request(req);
    return req->size;
}

4. レスポンスの受信: 操作が完了すると、カーネルは結果を/dev/fuseデバイスを通じてFUSEデーモンに返す。このレスポンスには、操作の成否、エラーコード、および必要なデータ(ファイルの内容、属性情報など)が含まれることがあります。

5. デーモンの処理: FUSEデーモンは、カーネルからのレスポンスを受け取り、それに基づいてユーザスペースのアプリケーションに対して適切な応答を行います。例えば、ファイルの読み取りリクエストに対しては、読み取ったデータをアプリケーションに返すなどです。このプロセスにより、ユーザスペースのデーモンはカーネルのファイルシステムインターフェースを利用して、独自のファイルシステムロジックを実装することができます。

トレース

アプリからのRead要求を受けとりbackgroundキューにエンキュー@カーネル

root@ubuntu3:~# bpftrace -e 'kprobe:fuse_simple_background { printf("%s", kstack()) }'
(..)
        fuse_simple_background+1
        read_pages+149
        page_cache_ra_unbounded+353
        do_page_cache_ra+61
        ondemand_readahead+311
        page_cache_async_ra+166
        filemap_get_pages+544
        filemap_read+190
        generic_file_read_iter+229
        fuse_file_read_iter+158
        new_sync_read+272
        vfs_read+258
        ksys_read+103
        __x64_sys_read+26
        do_syscall_64+92
        entry_SYSCALL_64_after_hwframe+97

カーネルWrite

root@ubuntu3:/sys/kernel/debug/tracing# bpftrace -e 'kprobe:fuse_perform_write { printf("%s", kstack()) }'
Attaching 1 probe...

        fuse_perform_write+1
        fuse_file_write_iter+99
        new_sync_write+279
        vfs_write+393
        ksys_write+103
        __x64_sys_write+26
        do_syscall_64+92
        entry_SYSCALL_64_after_hwframe+97

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です