テストステ論

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

(nim-fuse) 調査: FUSEの仕組み(動作編)

nim-fuseを作るためにNimとFUSEの調査をしている. 初期化は置いといて, まずは本質たる動作について調べる.

ドキュメントを読む

fuse: FUSE API documentation

以下のシーケンス図的なものを読むと, /dev/fuseの中にカーネルの構造体があり, ここにリクエストをつなぎ, コピーすることでユーザランドに委譲するという設計になってるように思う.

  1. デーモンさんは寝てる (fc->waitq. fcはfuse connectionの略)
  2. sys_unlinkからfuse_unlinkが呼ばれると, 使ってないリクエストを取得して, こいつをfc->pendingに繋ぐ. デーモンさんを叩き起こす. sys_unlinkさんは寝る.
  3. デーモンさんはfc->pendingからリクエストを刈り取って, こいつを(たぶん)ユーザランドのバッファにコピーする. リクエストをfc->processingに繋ぐ(処理中なう)
  4. perform unlinkの部分でユーザランドのfile operationsが為す.
  5. デーモンさんは, 処理が終わったので/dev/fuseにライトする. カーネルとのやりとりは/dev/fuseへのread/writeを使って, wait/completeを実現しているような感じだと思う. リクエストはfc->processingから取り除かれる. sys_unlinkさんを叩き起こす.
  6. あとはそのままrmしたユーザまで上げる.
 |  "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()  

カーネルコードを眺めてみる.

カーネルコードもざっくり眺めておくと理解によい.

fuse_copy_do

ユーザランドとのデータコピーはここでしょうか? cs->write(コードを見ると, /dev/fuseへのrwの逆だと思う. つまり, ユーザランドにコピーする方をwriteとしている)によってその方向を決めているのだと思う.

/* Do as much copy to/from userspace buffer as we can */
static int fuse_copy_do(struct fuse_copy_state *cs, void **val, unsigned *size)
{
        unsigned ncpy = min(*size, cs->len);
        if (val) {
                void *pgaddr = kmap_atomic(cs->pg);
                void *buf = pgaddr + cs->offset;

                if (cs->write)
                        memcpy(buf, *val, ncpy);
                else
                        memcpy(*val, buf, ncpy);

                kunmap_atomic(pgaddr);
                *val += ncpy;
        }
        *size -= ncpy;
        cs->len -= ncpy;
        cs->offset += ncpy;
        return ncpy;
}

呼び出し元はこいつ. 全コピーっぽい感じがする.

/* Copy a single argument in the request to/from userspace buffer */
static int fuse_copy_one(struct fuse_copy_state *cs, void *val, unsigned size)
{
        while (size) {
                if (!cs->len) {
                        int err = fuse_copy_fill(cs);
                        if (err)
                                return err;
                }
                fuse_copy_do(cs, &val, &size);
        }
        return 0;
}

fuse_dev_do_read

コメントを読むと, fc->pendingからはぎとって ユーザランドにコピーしてうんぬんと書いてある. request_endが呼ばれる時は エラー時であるということがなんとなく分かる.

/*
 * Read a single request into the userspace filesystem's buffer.  This
 * function waits until a request is available, then removes it from
 * the pending list and copies request data to userspace buffer.  If
 * no reply is needed (FORGET) or request has been aborted or there
 * was an error during the copying then it's finished by calling
 * request_end().  Otherwise add it to the processing list, and set
 * the 'sent' flag.
 */
static ssize_t fuse_dev_do_read(struct fuse_conn *fc, struct file *file,
                                struct fuse_copy_state *cs, size_t nbytes)
{

呼び出し元はこいつ. デーモンから呼び出される(そしてデーモンはリクエスト待ちで寝る). /dev/fuse自体は, fuse_connを持ってるだけの構造であると分かる. fuse_connというのが, キューとかを色々持っている構造であり, ユーザランドカーネルとのデータ受け渡しの本質である.

static ssize_t fuse_dev_read(struct kiocb *iocb, const struct iovec *iov,
                              unsigned long nr_segs, loff_t pos)
{
        struct fuse_copy_state cs;
        struct file *file = iocb->ki_filp;
        struct fuse_conn *fc = fuse_get_conn(file);
        if (!fc)
                return -EPERM;

        fuse_copy_init(&cs, fc, 1, iov, nr_segs);

        return fuse_dev_do_read(fc, file, &cs, iov_length(iov, nr_segs));
}

まとめ

カーネルユーザランドとのやりとりについては大体分かった. あとはデーモンがどうやって動作しているか, 初期化はどうなってるのか(nim-fuseを作るに当たってはここがもっとも重要)を調べる.