FUSE

投稿者: | 2024年2月17日

参考文献

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

アーキ

The fuse kernel holds 5 different queues, with each queue providing a specific purpose:

  • 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)
  • 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()
|                                    |      >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]
|                                    |      [add req to fc->processing]
|                                    |    <fuse_dev_read()
|                                    |  <sys_read()
|                                    |
|                                    |  [perform unlink]
|                                    |
|                                    |  >sys_write()
|                                    |    >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()                     |

以下ソース解析結果を示す。なお、ソース解析では生成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_request_send 関数を通じて、これらの要求データをFUSEデバイスファイルに書き込む。この書き込み操作により、データがFUSEデーモンに渡される。

FUSEデーモン→カーネル

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

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

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

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

3. カーネルの処理: カーネル側の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

コメントを残す

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