参考文献
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 — 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;
}
- Filesystem Daemon は libfuse を通して
/dev/fuse
を read しブロックされた状態で操作リクエストを待つ (fuse_session_receive_buf_int) - 操作リクエストがあると read の内容としてリクエストの情報が読まれ、fuse_lowlevel.cでfuse_ll_opsに定義した関数を呼び出す (fuse_session_process_buf_int)。例えば、リクエスト種別がREADの場合、do_readが、WRITEの場合はdo_writeが呼ばれる。
- 操作の結果を
/dev/fuse
に write で書き込む
内部的には 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_iter→fuse_perform_write→fuse_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