テストステ論

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

device-mapperの仕組み (4) endioの仕組み

前回, device-mapperによって作成された仮想デバイスが, queueの処理関数であるrequest_fnについて, dm_requestという関数を設定しており, ここで, split -> clone bioをtype#mapで処理という流れになってることをコードレベルで説明しました. 今日は, I/O処理の後半部分, endioについて説明します.

endioの基本的な実装は参照カウントです. 分割した分だけカウントをincして, clone bioが処理されたらdecして, 0になったら元bioのendioをするというのが基本的な処理です. ただし, 分割の前に番兵的にincしています(番兵というかガードというか).

  • inc
  • 分割ごとにinc -> clone bioのendioでdec (dec_pendingという関数)
  • dec (dec_pending)

という実装です. この番兵は必要です. clone bioのendioは非同期なので, もしこの番兵がいないと, 一瞬ですり抜けてしまって, clone bioについてendioが終わらないうちに元bioをendioしてしまうことになりかねません*1.

clone bioのendioコールバック関数はclone_endioと言います. mapの時と同じようなコードです.

static void clone_endio(struct bio *bio, int error)
{
        int r = 0;
        struct dm_target_io *tio = bio->bi_private;
        struct dm_io *io = tio->io;
        struct mapped_device *md = tio->io->md;
        dm_endio_fn endio = tio->ti->type->end_io;

        if (!bio_flagged(bio, BIO_UPTODATE) && !error)
                error = -EIO;

        if (endio) {
                r = endio(tio->ti, bio, error);
                if (r < 0 || r == DM_ENDIO_REQUEUE)
                        /*
                         * error and requeue request are handled
                         * in dec_pending().
                         */
                        error = r;
                else if (r == DM_ENDIO_INCOMPLETE)
                        /* The target will handle the io */
                        return;
                else if (r) {
                        DMWARN("unimplemented target endio return value: %d", r);
                        BUG();
                }
        }

        free_tio(md, tio);
        dec_pending(io, error);
}

独自のtarget#end_ioを呼ぶことが出来ます. 私の経験では, このendioでは,

  • endioまで何らかの構造を持っていって, endioで解放する.
  • 参照カウントを落とす.
  • 処理をqueueする.

と言ったことを行ったことがあります. いずれにしろ, endioですから, sleepは出来ません*2.

dm-lcのendio関数であるlc_end_ioは以下のようになっています. map_context->ptrというポインタに何かを紐付けておけば, endioコンテキストまで運んでくれます. struct lc_endio_contextのようなオブジェクトを生成して紐付ける方が柔軟ではありますが, dm-lcではそこまでする必要がないため, シンプルに留めています. ある特定のケースでのみ, nr_inflight_iosをendioでdecしなければなりません.

static int lc_end_io(struct dm_target *ti, struct bio *bio, int error, union map_info *map_context)
{
        if(! map_context->ptr){
                return 0;
        }

        struct segment_header *seg = map_context->ptr;
        atomic_dec(&seg->nr_inflight_ios);

        return 0;
}

元bioの参照カウントを落とすのはdec_pendingです. 何らかの理由でrequeueする場合もありますが, io_countが0に落ちた時には元bioについてbio_endioが呼ばれるのがふつうのケースです. REQ_FLUSHの場合は, フラグを剥がしてからデータ部分を再度queueするという実装によってREQ_FLUSHをサポートしているということは以前に述べました*3.

/*
 * Decrements the number of outstanding ios that a bio has been
 * cloned into, completing the original io if necc.
 */
static void dec_pending(struct dm_io *io, int error)
{
        unsigned long flags;
        int io_error;
        struct bio *bio;
        struct mapped_device *md = io->md;

        /* Push-back supersedes any I/O errors */
        if (unlikely(error)) {
                spin_lock_irqsave(&io->endio_lock, flags);
                if (!(io->error > 0 && __noflush_suspending(md)))
                        io->error = error;
                spin_unlock_irqrestore(&io->endio_lock, flags);
        }

        if (atomic_dec_and_test(&io->io_count)) {
                if (io->error == DM_ENDIO_REQUEUE) {
                        /*
                         * Target requested pushing back the I/O.
                         */
                        spin_lock_irqsave(&md->deferred_lock, flags);
                        if (__noflush_suspending(md))
                                bio_list_add_head(&md->deferred, io->bio);
                        else
                                /* noflush suspend was interrupted. */
                                io->error = -EIO;
                        spin_unlock_irqrestore(&md->deferred_lock, flags);
                }

                io_error = io->error;
                bio = io->bio;
                end_io_acct(io);
                free_io(md, io);

                if (io_error == DM_ENDIO_REQUEUE)
                        return;

                if ((bio->bi_rw & REQ_FLUSH) && bio->bi_size) {
                        /*
                         * Preflush done for flush with data, reissue
                         * without REQ_FLUSH.
                         */
                        bio->bi_rw &= ~REQ_FLUSH;
                        queue_io(md, bio);
                } else {
                        /* done with normal IO or empty flush */
                        trace_block_bio_complete(md->queue, bio, io_error);
                        bio_endio(bio, io_error);
                }
        }
}

以上です. endioは簡単ですね. 今まで散々話してしまったので, コードを見るまでもない状態だったと思いますが, 対称性を重視してコードを追いました. 今までのブログを読めば, device-mapperがどのようにI/Oを処理しているかということが大まかには分かると思います. 私のブログを足がかりにして, 是非コードを読むことをオススメします.

device-mapperのコードは, Linuxカーネルの中では新しく, 従って綺麗です. 設計も, オブジェクト指向の概念が採り入れられており, 理解しやすいと思います. device-mapperは, 私が会社に入って始めて対面したカーネルコードでした. 私はオブジェクト指向については実践的な意味ではかなり習熟しています. device-mapperから入れたことで, 周辺の知識も無理なく身につけていくことが出来たと思います. カーネルと対面したばかりの時の私のようなスキルを持っているあなたには, カーネルと向き合うチャンスがあります. カーネルのコードが読めるということは, とても価値のあることです. 是非この機会に, カーネルコードを読んでみてはいかがでしょうか.

*1:というか大体の場合なるでしょう. ディスクからのendioなんて通常はmsオーダかかりますから. dm-lcはnsオーダですけどね. キリッ

*2:例えば, vfreeは出来ません. vfreeはsleepします

*3:ちなみに私, この実装好きです