テストステ論

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

(nim-fuse report) INTERRUPTリクエスト

nim-fuseの設計は, rust-fuseのものを元にしている. これらは, lowlevel APIを実装している. c-fuseはさらに, lowlevel APIをブリッジした容易なAPIであるfuse operationsをサポートしている. ふつう, fuseという場合はこちらの方を意味する. lowlevel APIの存在は, ふつう語られない.

これをhighlevel APIと呼ぶことにする. highlevel APIの長所は, 実装が簡単なことである. 特に, INTERRUPTリクエストの扱いを隠蔽してくれる. rust-fuseのTodoには以下のように書いてある. 私は, high performanceなファイルシステムを作るのであればlowlevel APIで実装すべきだと思うが, 多くの便利系ファイルシステムはhighlevel APIで記述した方が良いと思うし, 実際sshfsなどはそうなってるに違いないと思ってる. nim-fuseも, 将来的にはhighlevel APIをサポートする計画があるし, 早晩やった方がlowlevel APIのテストにもなって良いと思う.

An additional more high level API would be nice. It should provide pathnames instead inode numbers and automatically handle concurrency and interruption (like the FUSE C library's high level API).

fuseのドキュメントのinterruptの章を読んで調査する.

https://www.kernel.org/doc/Documentation/filesystems/fuse.txt

Interrupting filesystem operations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If a process issuing a FUSE filesystem request is interrupted, the following will happen:

1) If the request is not yet sent to userspace AND the signal is fatal (SIGKILL or unhandled fatal signal), then the request is dequeued and returns immediately.

2) If the request is not yet sent to userspace AND the signal is not fatal, then an 'interrupted' flag is set for the request. When the request has been successfully transferred to userspace and this flag is set, an INTERRUPT request is queued.

3) If the request is already sent to userspace, then an INTERRUPT request is queued.

INTERRUPT requests take precedence over other requests, so the userspace filesystem will receive queued INTERRUPTs before any others.

The userspace filesystem may ignore the INTERRUPT requests entirely, or may honor them by sending a reply to the original request, with the error set to EINTR.

It is also possible that there's a race between processing the original request and its INTERRUPT request. There are two possibilities:

1) The INTERRUPT request is processed before the original request is processed

2) The INTERRUPT request is processed after the original request has been answered

If the filesystem cannot find the original request, it should wait for some timeout and/or a number of new requests to arrive, after which it should reply to the INTERRUPT request with an EAGAIN error. In case 1) the INTERRUPT request will be requeued. In case 2) the INTERRUPT reply will be ignored.

ここでinterruptとは, signalのことである. syscallを受けたカーネルがリクエストを作り, それを処理するわけであるが, この時にsignalに割り込まれた時にどうするかが書いてある. 割り込まれた結果, INTERRUPTリクエストがキューされて, それをFUSEが処理することになるのだが, これについては無視でも良いとしてある.

INTERRUPTリクエストは以下のようなケースでキューされる.

  1. [A] リクエストがFUSEに送られていなくて, signalがfatalである -> カーネルはそのリクエストを破棄する. つまりINTERRUPTリクエストはキューされない.
  2. [B] リクエストがFUSEに送られていなくて, singalがfatalではない -> とりあえずリクエストにinterruptフラグをつける. そのままFUSEに送って, FUSEに届いた時にINTERRUPTリクエストもキューする. (おそらく, FUSEに渡るリクエストとしては, interruptフラグはなくなっている. カーネル内ではフラグがあるが, FUSEに渡す時に消滅するという仕組み. これが問題となる)
  3. [C] もうFUSEに届いてしまってる場合は, 単にINTERRUPTリクエストをキューする.

FUSE側の対応としては, 以下の2通りがあるとしている.

  1. 全無視
  2. 割り込まれた元リクエストに対して, EINTRでack

元リクエストとINTERRUPTリクエストの順序は通常の場合, INTERRUPTリクエストの方が先である(例えばBのケースではこう確定するものと考えられる). しかし, Cの場合は, 元リクエストを処理し始めてる可能性もある. すでにackに入ってしまってる可能性すらある.

リクエストはシリアライズされており, INTERRUPTリクエストを受け取った時にはそれに対応する元リクエストはわからないから(uniqueで判定するっぽい), いくらかリクエストを待って, その後にEAGAINをackする. なぜこのような順序問題を守る必要があるかというと, INTERRUPTリクエストのackは, 元リクエストのackのあとにしなければいけないからだと思う(だからrequeueするとか書いてある).

と, 色々なケースがあって鬱陶しいのだが, 結局, Cのケースがあるので, どうやっても正しくはならないだろうと思う. つまり, 上に書いてある順序がどうとかこの時にこうせよというのは, 全部本質的ではない. なので, 全無視ということもOKだし, 出来る限りがんばりを見せるというのもOKとなる. しかし, interruptされたリクエスト(そのinterruptはfatalではない)をどう扱うかは難しい問題だ. 私は, INTERRUPTリクエストは何ごともなかったかのように全部無視するのが良いと思う.

c-fuseは, 何やらINTERRUPTリクエストをキューしてほげほげしてるようである. たぶん, fuse_interrupt_inのuniqueは元リクエストのuniqueだであり, 先ほど書いたように, 元リクエストが見つかるまでことあるごとにリストを全探索するということをしているように見える.

// たぶん, キューされてるnot-INTERRUPTリクエストの中から, interruptされたものを探してinterruptマークをつける処理. つまり, 元リクエストだけを見てもinterruptされたものかどうかは判定出来ないということ. 特に, Cのケースではマークするタイミングすらない. なので, マークしないという決定にして, マークする操作はFUSE側にまかせているわけだが, これが線形探索を必要とするはめになっている.
tatic int find_interrupted(struct fuse_ll *f, struct fuse_req *req)
{
        struct fuse_req *curr;

        for (curr = f->list.next; curr != &f->list; curr = curr->next) {
                if (curr->unique == req->u.i.unique) {
                        curr->ctr++;
                        pthread_mutex_unlock(&f->lock);

                        /* Ugh, ugly locking */
                        pthread_mutex_lock(&curr->lock);
                        pthread_mutex_lock(&f->lock);
                        curr->interrupted = 1;
                        pthread_mutex_unlock(&f->lock);
                        if (curr->u.ni.func)
                                curr->u.ni.func(curr, curr->u.ni.data);
                        pthread_mutex_unlock(&curr->lock);

                        pthread_mutex_lock(&f->lock);
                        curr->ctr--;
                        if (!curr->ctr)
                                destroy_req(curr);

                        return 1;
                }
        }
        for (curr = f->interrupts.next; curr != &f->interrupts;
             curr = curr->next) {
                if (curr->u.i.unique == req->u.i.unique)
                        return 1;
        }
        return 0;
}

static void do_interrupt(fuse_req_t req, fuse_ino_t nodeid, const void *inarg)
{
        struct fuse_interrupt_in *arg = (struct fuse_interrupt_in *) inarg;
        struct fuse_ll *f = req->f;

        (void) nodeid;
        if (f->debug)
                fprintf(stderr, "INTERRUPT: %llu\n",
                        (unsigned long long) arg->unique);

        req->u.i.unique = arg->unique;

        pthread_mutex_lock(&f->lock);
        if (find_interrupted(f, req))
                destroy_req(req);
        else
                list_add_req(req, &f->interrupts);
        pthread_mutex_unlock(&f->lock);
}

fuse_ll_processの中のメインパスはこうなってる. これはたぶん, 通常のリクエストもキューして, のちのリスト探索に備えるというものである. 非常に効率が悪いし設計的にもuglyだと思う(ロッキングがuglyだというコメントは, 本質的ではない. そもそもの設計が悪いからロッキングに悪さがにじみ出ただけの話だ)

                if (in->opcode != FUSE_INTERRUPT) {
                        struct fuse_req *intr;
                        pthread_mutex_lock(&f->lock);
                        intr = check_interrupt(f, req); // たぶん対応するINTERRUPTリクエストを探してる
                        list_add_req(req, &f->list); // とりあえずキューする
                        pthread_mutex_unlock(&f->lock);
                        if (intr)
                                fuse_reply_err(intr, EAGAIN); // EAGAINを返す
                }
                fuse_ll_ops[in->opcode].func(req, in->nodeid, inarg);

直感的には, 元リクエストが早晩終わってしまった場合は, INTERRUPTリクエストが永遠にack出来ないタイミングもあると思ったが, FUSE側である点を通過した以降にinterruptが入った場合にはCにも該当しないという実装になっているのだろうと推測する.

勘だが, それはfuse_finish_interruptであると思う. あるいは, 同時期に割り込まれたままのリクエストがたかだか1つであることが保証出来るならばリストが空かどうかで判定ということも可能であると思う. あるいは, fuse_finish_interruptの中でfinishedフラグを立てて, 安全なタイミングで回収だろうか?これならば, 仮に何らかの理由で処理が終わったあとにINTERRUPTリクエストをキューすることになったとしても, 元リクエストを見つけることが出来る(ここで見つけられないと, 永遠にack出来ないということがあり得るので, 常に見つかるようにするというアイデア).

static void fuse_lib_getattr(fuse_req_t req, fuse_ino_t ino,
                             struct fuse_file_info *fi)
{
        struct fuse *f = req_fuse_prepare(req);
        struct stat buf;
        char *path;
        int err;

        (void) fi;
        memset(&buf, 0, sizeof(buf));

        err = -ENOENT;
        pthread_rwlock_rdlock(&f->tree_lock);
        path = get_path(f, ino);
        if (path != NULL) {
                struct fuse_intr_data d;
                fuse_prepare_interrupt(f, req, &d);
                err = fuse_fs_getattr(f->fs, path, &buf);
                fuse_finish_interrupt(f, req, &d);
                free(path);
        }