テストステ論

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

device-mapperの仕組み (3) dm_requestからの流れ

前回, device-mapperによって作成された仮想デバイスが, 受け取ったbioをsplitして, それぞれの小さなbio(clone bio)について処理をしてから, 元bioのACK*1を返すというお話をしました. 今回は, 仮想デバイスがbioを受け取ってからtargetのmapに処理を渡すところまで, コードベースで説明します.

まず, 仮想デバイスのrequest_fnは, dm_requestです. Linuxのブロックサブシステムへの入り口はgeneric_make_requestという関数であり, そのラストでは以下のようなコードがあります. current(というマクロ)についてるbio_listからbioをpopしてrequest_fnに渡していくという処理です. queueに設定されたrequest_fnを呼ぶことが本質です.
仮想デバイスにもqueueがありますが, これは仮想的なものであり, 具体的にはbioを後の具体的な処理に変換する責務しか持ちません. このqueueのrequest_fnとして, dm_requestが設定されています.

        bio_list_init(&bio_list_on_stack);
        current->bio_list = &bio_list_on_stack;
        do {
                struct request_queue *q = bdev_get_queue(bio->bi_bdev);

                q->make_request_fn(q, bio);

                bio = bio_list_pop(current->bio_list);
        } while (bio);
        current->bio_list = NULL; /* deactivate */
}
EXPORT_SYMBOL(generic_make_request);

dm_requestは以下のように定義されています. request_basedというのは, 今回は関係ありません. 2.6.32から導入されたrequest-based device-mapperを利用するということですが, 実際はmultipathくらいでしか使われていません. linearやraidなどもすべて, bio-basedなdevice-mapperを使っています. その場合, _dm_requestに処理が転送されます.

static void dm_request(struct request_queue *q, struct bio *bio)
{
        struct mapped_device *md = q->queuedata;

        if (dm_request_based(md))
                blk_queue_bio(q, bio);
        else
                _dm_request(q, bio);
}

_dm_requestは, read semaphoreをdownしてから, __split_and_process_bio(md, bio)に処理を転送します.

__split_and_process_bioは以下のように定義されています. clone_infoというのは, これからbioをsplitしていくために使う構造です. if文の最初では, REQ_FLUSHフラグを引き剥がしています. これについては, dm-lcの記事で書きましたので, こちらを参考してください. 本質的なのは, elseの方です. clone_infoの構造を元手にして__clone_and_mapを実行します.

static void __split_and_process_bio(struct mapped_device *md, struct bio *bio)
{
        struct clone_info ci;
        int error = 0;

        ci.map = dm_get_live_table(md);
        if (unlikely(!ci.map)) {
                bio_io_error(bio);
                return;
        }

        ci.md = md;
        ci.io = alloc_io(md);
        ci.io->error = 0;
        atomic_set(&ci.io->io_count, 1);
        ci.io->bio = bio;
        ci.io->md = md;
        spin_lock_init(&ci.io->endio_lock);
        ci.sector = bio->bi_sector;
        ci.idx = bio->bi_idx;

        start_io_acct(ci.io);
        if (bio->bi_rw & REQ_FLUSH) {
                ci.bio = &ci.md->flush_bio;
                ci.sector_count = 0;
                error = __clone_and_map_empty_flush(&ci);
                /* dec_pending submits any data associated with flush */
        } else {
                ci.bio = bio;
                ci.sector_count = bio_sectors(bio);
                while (ci.sector_count && !error)
                        error = __clone_and_map(&ci);
        }

        /* drop the extra reference count */
        dec_pending(ci.io, error);

__clone_and_mapは, ti = dm_target_find_target(ci->map, ci->sector)を実行して, どのターゲットに処理を渡すかを計算します. そして, max = max_io_len(ci->sector, ti)によって, 「最長でどのくらい長いbioに分割出来るか」を計算します.

max_io_lenのコードは以下です. max_io_len(3.5まではsplit_ioという名前でした)が設定されている場合に限ってsplitをします. max_io_lenというのは基本的に, 2Nであることが要求されています. このコードで散見されるx & (y - 1)というのは, y以下の部分を抽出するということです. ようするにx % yと同じです. コメントに書いてあるように, targetの範囲内で, max_io_lenを最大としてどのくらいの範囲にbioを発行出来るかを計算しています.

static sector_t max_io_len(sector_t sector, struct dm_target *ti)
{
        sector_t len = max_io_len_target_boundary(sector, ti);
        sector_t offset, max_len;

        /*
         * Does the target need to split even further?
         */
        if (ti->max_io_len) {
                offset = dm_target_offset(ti, sector);
                if (unlikely(ti->max_io_len & (ti->max_io_len - 1)))
                        max_len = sector_div(offset, ti->max_io_len);
                else
                        max_len = offset & (ti->max_io_len - 1);
                max_len = ti->max_io_len - max_len;

                if (len > max_len)
                        len = max_len;
        }

        return len;
}

__clone_and_mapは, 小さなbioを一つ生成して, __map_bio(ti, bio)に渡します. ciが表現する元bioがすべて発行されるまで(ci.sector_countが0になるまで), 先ほど紹介した__split_and_process_bioに書いてあったように, 小さなbioをちぎって発行をループします.

                while (ci.sector_count && !error)
                        error = __clone_and_map(&ci);

__map_bioは以下のように定義されています. bioをti->type->mapに渡しています. このmapが, target_typeのメソッドであるmapです(例えばdm-lcならlc_map). mapの返り値は3つ定義されていますが, よく使うのはDM_MAPIO_SUBMITTED(0)とDM_MAPIO_REMAPPED(1)です. 後者は, 「bioのアドレスだけ変えたし, 自分の外で適当にgeneric_make_requestしてくれ」という意味です. コードでは, trace_bio_block_remap関数が呼ばれてからgeneric_make_requestが呼ばれています. 前者は, 「外で余計なことしないで」という意思表示です. このコードでは, どの条件分岐にも引っかからずに何もせずに抜けます(r=0なので).

static void __map_bio(struct dm_target *ti, struct dm_target_io *tio)
{
        int r;
        sector_t sector;
        struct mapped_device *md;
        struct bio *clone = &tio->clone;

        clone->bi_end_io = clone_endio;
        clone->bi_private = tio;

        /*
         * Map the clone. If r == 0 we don't need to do
         * anything, the target has assumed ownership of
         * this io.
         */
        atomic_inc(&tio->io->io_count); <<
        sector = clone->bi_sector;
        r = ti->type->map(ti, clone);
        if (r == DM_MAPIO_REMAPPED) {
                /* the bio has been remapped so dispatch it */

                trace_block_bio_remap(bdev_get_queue(clone->bi_bdev), clone,
                                      tio->io->bio->bi_bdev->bd_dev, sector);

                generic_make_request(clone);
        } else if (r < 0 || r == DM_MAPIO_REQUEUE) {
                /* error the io and bail out, or requeue it if needed */
                md = tio->io->md;
                dec_pending(tio->io, r);
                free_tio(md, tio);
        } else if (r) {
                DMWARN("unimplemented target map return value: %d", r);
                BUG();
        }
}

device-mapperは, bioのアドレスを変更するものではなかったの?と思ってる方もいるかも知れません. device-mapperの表現力はその程度ではありません. DM_MAPIO_SUBMITTEDを使うことによって, bioの遅延実行なども行うことが出来ます. 例えばdm-lcでは, 以下のような点で使っています.

  1. write barrierのためにRAMバッファの書き出しを遅延することがあります. この場合, DM_MAPIO_SUBMITTEDを発行してしまい, 「あとはおれの方で適当にやっておくから, 余計なことはするな」と外側に伝えます. 実際, バッファを書き出したあとにendioを呼んでいます.
  2. RAMバッファへの書き込みでimmediate completionをするため, バッファにmemcpyしたあとendioを呼び出してbioについてとりあえずACKを返したあと, DM_MAPIO_SUBMITTEDを返しています.

聡明な方は, ciからclone bioを作ってmapするまで次の__clone_and_mapに進まないので, 並行に処理出来る場合などは遅いんじゃないの思う方もいると思います. つまり, clone bioをちぎっては何かする(I/Oを実際に発行してACKまで待つ必要はない)を逐次的に繰り返すことになってるということです. targetとしては, bioが同時並行的に突入してくることも考慮すべきですから, そういう意味ではフレームワーク側でこのような逐次処理を入れるのはおかしいと言う見方も出来ますが, 「そういうことがしたいならmapの中でワークキューかなんか使ってスレッド切ってくれ」というポリシーなのだと思います. もっと外側でスレッドを切って並行処理した方が, clone bioを作る部分も並行処理出来ますが, ロックの制御などが難しくなるのが良くないということかも知れません. スレッドを切ることによるスケジューリングのオーバーヘッドもありますし, トレードオフといえばトレードオフです. 例えば, linearではこの設計が最善でしょう.

以上です. 今回は, device-mapperによって作成された仮想デバイスが, 最終的にtargetのmapメソッドに処理を渡すところまでを説明しました. 次回は, I/O処理の後半, endioについて説明します.

*1:acknowledgementの略です. ストレージの世界でも, ACKの返すという言葉を使います. ただし, ブロックの人はOKを返すという, より実装に近い言い方をする人もいます