テストステ論

高テス協会会長が, テストステロンに関する情報をお届けします.

(nim-fuse) 調査: FUSEのコード調査

ドキュメントを読む

  1. fuse_mainを呼ぶ
  2. fuse_mountはソケットの組を作る. FUSE_COMMFD_ENVにその片方を設定して, fusermountを実行する.
  3. fusermountは/dev/fuseをオープンして, fuse_mountにそのハンドル(fd)を送り返す.
  4. fuse_mountは, そのハンドルをfuse_mainに返す.
  5. 最後にfuse_loopを呼び出す. fuse_loopは/dev/fuseからファイルシステム呼び出しを読み出し, fuse_operationsを呼び出す. (before calling fuse_mainは意味不明)
  6. 結果は/dev/fuseに書き戻されて, system callに返される.
When your user mode program calls fuse_main() (lib/helper.c),
fuse_main() parses the arguments passed to your user mode program,
then calls fuse_mount() (lib/mount.c).
fuse_mount() creates a UNIX domain socket pair, then forks and execs
fusermount (util/fusermount.c) passing it one end of the socket in the
FUSE_COMMFD_ENV environment variable.
fusermount (util/fusermount.c) makes sure that the fuse module is
loaded. fusermount then open /dev/fuse and send the file handle over a
UNIX domain socket back to fuse_mount().
fuse_mount() returns the filehandle for /dev/fuse to fuse_main().
fuse_main() calls fuse_new() (lib/fuse.c) which allocates the struct
fuse datastructure that stores and maintains a cached image of the
filesystem data.
Lastly, fuse_main() calls either fuse_loop() (lib/fuse.c) or
fuse_loop_mt() (lib/fuse_mt.c) which both start to read the filesystem
system calls from the /dev/fuse, call the usermode functions
stored in struct fuse_operations datastructure before calling
fuse_main(). The results of those calls are then written back to the
/dev/fuse file where they can be forwarded back to the system
calls.

コードを読む

動作

fuseでは, /dev/fuseから受け取ったリクエストを, fuse_operationsに直接は渡さない. カーネルとの界面にlowlevelというレイヤを用意して, ここを経由することにしてる.

  1. カーネルからリクエストを受け取る. fuse_ll_opsにdispatchされる.
  2. fuse_lowlevel_operationsにforwardされる.
  3. ユーザ定義のfuse_operationsにforwardされる.
tatic struct {
        void (*func)(fuse_req_t, fuse_ino_t, const void *);
        const char *name;
} fuse_ll_ops[] = {
        [FUSE_LOOKUP]      = { do_lookup,      "LOOKUP"      },
        [FUSE_FORGET]      = { do_forget,      "FORGET"      },
        [FUSE_GETATTR]     = { do_getattr,     "GETATTR"     },
        [FUSE_SETATTR]     = { do_setattr,     "SETATTR"     },
        [FUSE_READLINK]    = { do_readlink,    "READLINK"    },
        [FUSE_SYMLINK]     = { do_symlink,     "SYMLINK"     },
        [FUSE_MKNOD]       = { do_mknod,       "MKNOD"       },
        [FUSE_MKDIR]       = { do_mkdir,       "MKDIR"       },
        [FUSE_UNLINK]      = { do_unlink,      "UNLINK"      },
        [FUSE_RMDIR]       = { do_rmdir,       "RMDIR"       },

lowlevel operationsは以下のような形である.

static struct fuse_lowlevel_ops fuse_path_ops = {
        .init = fuse_lib_init,
        .destroy = fuse_lib_destroy,
        .lookup = fuse_lib_lookup,
        .forget = fuse_lib_forget,
        .forget_multi = fuse_lib_forget_multi,

accessというプロトコルは以下のように処理されている.

static void fuse_lib_access(fuse_req_t req, fuse_ino_t ino, int mask)
{
        struct fuse *f = req_fuse_prepare(req);
        char *path;
        int err;

        err = get_path(f, ino, &path);
        if (!err) {
                struct fuse_intr_data d;

                fuse_prepare_interrupt(f, req, &d);
                err = fuse_fs_access(f->fs, path, mask);
                fuse_finish_interrupt(f, req, &d);
                free_path(f, ino, path);
        }
        reply_err(req, err);
}

これを以下のループの下で繰り返す. チャネルからバッファを受け取り, それをリクエストに翻訳して, process_buf以下で処理するという設計になっている. それぞれ, fuse_ll_receive_buf, fuse_ll_process_buf(ここでfuse_ll_opsが呼ばれる)が本質である. rust-fuseは, fuse_ll_opsの実装を自前で行う設計になっている(つまり, FUSE_xのopcodeを解析して各プロトコル処理にdispatchする)

int fuse_session_loop(struct fuse_session *se)
{
        int res = 0;
        struct fuse_chan *ch = fuse_session_next_chan(se, NULL);
        size_t bufsize = fuse_chan_bufsize(ch);
        char *buf = (char *) malloc(bufsize);
        if (!buf) {
                fprintf(stderr, "fuse: failed to allocate read buffer\n");
                return -1;
        }

        while (!fuse_session_exited(se)) {
                struct fuse_chan *tmpch = ch;
                struct fuse_buf fbuf = {
                        .mem = buf,
                        .size = bufsize,
                };

                res = fuse_session_receive_buf(se, &fbuf, &tmpch);

                if (res == -EINTR)
                        continue;
                if (res <= 0)
                        break;

                fuse_session_process_buf(se, &fbuf, tmpch);
        }

        free(buf);
        fuse_session_reset(se);
        return res < 0 ? -1 : 0;
}

初期化

ここまでで, どのように動作するかは分かった. 続いて初期化について調べる. fuse_mainから重要な関数を辿る.

fuse_main_common
- fuse_setup_common
  - ch = fuse_mount_common
    - fd = fuse_mount_compat25 // /dev/fuseのfdを取得する
    - fuse_kern_chan_new(fd) // チャネルで取得
  - fuse_new_common(ch)
    - fuse_fs_new
    - f->se = fuse_lowlevel_new_common(fuse_path_ops) // lowlevel opsを設定. セッションを取得
    - fuse_session_add_chan(f->se, ch) // セッションとチャネルを双方向でリンク
  - fuse_daemonize // 今のプロセスをデーモンにする
- fuse_loop

rust-fuseは, チャネル(fdのラッパー)を以下のように実装している. nim-fuseも同じ界面で実装すれば良さそうである.

impl Channel {
    /// Create a new communication channel to the kernel driver by mounting the
    /// given path. The kernel driver will delegate filesystem operations of
    /// the given path to the channel. If the channel is dropped, the path is
    /// unmounted.
    pub fn new (mountpoint: &Path, options: &[&[u8]]) -> Result<Channel, c_int> {
        real_path(mountpoint).and_then(|mountpoint| {
            with_fuse_args(options, |args| {
                let mnt = CString::from_slice(mountpoint.as_vec()).as_ptr();
                let fd = unsafe { fuse_mount_compat25(mnt, args) };
                if fd < 0 {
                    Err(os::errno() as c_int)
                } else {
                    Ok(Channel { mountpoint: mountpoint.clone(), fd: fd })
                }
            })
        })
    }

    /// Receives data up to the size of the given buffer (can block) and returns
    /// the size of the received data.
    pub fn receive (&self, buffer: &mut [u8]) -> Result<uint, c_int> {
        let rc = unsafe { ::libc::read(self.fd, buffer.as_ptr() as *mut c_void, buffer.len() as size_t) };
        if rc < 0 {
            Err(os::errno() as c_int)
        } else {
            Ok(rc as uint)
        }
    }

    /// Returns a sender object for this channel. The sender object can be
    /// used to send to the channel. Multiple sender objects can be used
    /// and they can safely be sent to other threads.
    pub fn sender (&self) -> ChannelSender {
        // Since write/writev syscalls are threadsafe, we can simply create
        // a sender by using the same fd and use it in other threads. Only
        // the channel closes the fd when dropped. If any sender is used after
        // dropping the channel, it'll return an EBADF error.
        ChannelSender { fd: self.fd }
    }
}

まとめ

fuseの初期化と動作について, コードレベルでの調査を行った.