読者です 読者をやめる 読者になる 読者になる

テストステ論

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

(writeboost report) suspendが期待すること

v2.2.2をリリースした. 前回のv2.2.1から2週間足らずとなったが, 出したい時に出す. それがおれのやり方だ.

v2.2.2では, 目下指摘されている(しかし再現不能な)read-cachingのバグを直す可能性が1%くらいはある修正を入れた.

v2.2.3ではバグフィックスのアイデアがない限りは改善系を入れたい.

don't merge when the asking region entirely exists in the caching device · Issue #118 · akiradeveloper/dm-writeboost · GitHub

not able to dmsetup remove wb device before all write data are successfully flushed · Issue #127 · akiradeveloper/dm-writeboost · GitHub

後者について考えよう. これは, SSDがぶっ壊れてwrite出来なくなった場合, dmsetup removeが永遠に終わりませんという問題なのだが, dmsetup remove時に呼ばれるpostsuspend hookが永遠に終わらないのが問題だと言ってる. コード(__dm_destory)を見てみる.

static void __dm_destroy(struct mapped_device *md, bool wait)
{
        struct dm_table *map;
        int srcu_idx;

        might_sleep();

        spin_lock(&_minor_lock);
        idr_replace(&_minor_idr, MINOR_ALLOCED, MINOR(disk_devt(dm_disk(md))));
        set_bit(DMF_FREEING, &md->flags);
        spin_unlock(&_minor_lock);

        if (dm_request_based(md) && md->kworker_task)
                flush_kthread_worker(&md->kworker);

        /*
         * Take suspend_lock so that presuspend and postsuspend methods
         * do not race with internal suspend.
         */
        mutex_lock(&md->suspend_lock);
        map = dm_get_live_table(md, &srcu_idx);
        if (!dm_suspended_md(md)) {
                dm_table_presuspend_targets(map);
                dm_table_postsuspend_targets(map);
        }
        /* dm_put_live_table must be before msleep, otherwise deadlock is possible */
        dm_put_live_table(md, srcu_idx);
        mutex_unlock(&md->suspend_lock);

        /*
         * Rare, but there may be I/O requests still going to complete,
         * for example.  Wait for all references to disappear.
         * No one should increment the reference count of the mapped_device,
         * after the mapped_device state becomes DMF_FREEING.
         */
        if (wait)
                while (atomic_read(&md->holders))
                        msleep(1);
        else if (atomic_read(&md->holders))
                DMWARN("%s: Forcibly removing mapped_device still in use! (%d users)",
                       dm_device_name(md), atomic_read(&md->holders));

        dm_sysfs_exit(md);
        dm_table_destroy(__unbind(md));
        free_dev(md);
}

ということはコメントにも書いてある.

/*
 * .postsuspend is called before .dtr.
 * We flush out all the transient data and make them persistent.
 */
static void writeboost_postsuspend(struct dm_target *ti)
{
    struct wb_device *wb = ti->private;
    flush_current_buffer(wb);
    blkdev_issue_flush(wb->cache_dev->bdev, GFP_NOIO, NULL);
}

今回はコメントの二行目がなぜそう考えたかを残す.

結論をいうと, このコードはやりすぎだと思う. 実際には, inflightなIOsがすべてackすればいいのだ. だから, 今のrambufferに対して飛んでいるIOsがすべてackするのを待てばいい. それは, 現在のrambufをSSDにflushすることではない. (もちろん, flushしたとしてもバグではない)

全IOがackするのを待つだけならば, rambufに書いて終わりというケースでSSDがnot involvedなケースもあると思うかも知れないが, 現実的にはそれはあまり見込めない. なぜかというと, suspendするくらいだったらその前にはREQ_FLUSHが飛んできているだろうから. だとすると, やはり現在のrambufをflushするところまではやらないといけない. だから, blkdev_issue_flushくらいは削除しても問題なさそうというレベルだ.

根拠を探しにいく. __dm_suspendでは, md(仮想デバイス)のwqをflushして, 何らかの完了を待っていることが分かる.

/*
 * If __dm_suspend returns 0, the device is completely quiescent
 * now. There is no request-processing activity. All new requests
 * are being added to md->deferred list.
 *
 * Caller must hold md->suspend_lock
 */
static int __dm_suspend(struct mapped_device *md, struct dm_table *map,
                        unsigned suspend_flags, int interruptible)
{
        // ....
        flush_workqueue(md->wq);

        /*
         * At this point no more requests are entering target request routines.
         * We call dm_wait_for_completion to wait for all existing requests
         * to finish.
         */
        r = dm_wait_for_completion(md, interruptible);

dm_wait_for_completionは, md_in_flightが成立するまで寝たり起きたりを繰り返す.

static int dm_wait_for_completion(struct mapped_device *md, int interruptible)
{
        int r = 0;
        DECLARE_WAITQUEUE(wait, current);

        add_wait_queue(&md->wait, &wait);

        while (1) {
                set_current_state(interruptible);

                if (!md_in_flight(md))
                        break;

                if (interruptible == TASK_INTERRUPTIBLE &&
                    signal_pending(current)) {
                        r = -EINTR;
                        break;
                }

                io_schedule();
        }
        set_current_state(TASK_RUNNING);

        remove_wait_queue(&md->wait, &wait);

        return r;
}

static int md_in_flight(struct mapped_device *md)
{
        return atomic_read(&md->pending[READ]) +
               atomic_read(&md->pending[WRITE]);
}

これがどこで増えたり減ったりしてるかというと,

static void start_io_acct(struct dm_io *io)
{
        struct mapped_device *md = io->md;
        struct bio *bio = io->bio;
        int cpu;
        int rw = bio_data_dir(bio);

        io->start_time = jiffies;

        cpu = part_stat_lock();
        part_round_stats(cpu, &dm_disk(md)->part0);
        part_stat_unlock();
        atomic_set(&dm_disk(md)->part0.in_flight[rw],
                atomic_inc_return(&md->pending[rw]));

        if (unlikely(dm_stats_used(&md->stats)))
                dm_stats_account_io(&md->stats, bio->bi_rw, bio->bi_iter.bi_sector,
                                    bio_sectors(bio), false, 0, &io->stats_aux);
}

と, end_io_acctだけである.

start_io_acct__split_and_process_bioで呼ばれる. これはコメントにもあるとおり, mdがbioを受け取った時に通るパスだ. 具体的には, targetが要求するサイズにbioを分割して発行する.

/*
 * Entry point to split a bio into clones and submit them to the targets.
 */
static void __split_and_process_bio(struct mapped_device *md,
                                    struct dm_table *map, struct bio *bio)
{
        struct clone_info ci;
        int error = 0;

        if (unlikely(!map)) {
                bio_io_error(bio);
                return;
        }

        ci.map = map;
        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_iter.bi_sector;

        start_io_acct(ci.io);

つまり, mdがリクエストを受け取った時にincされて, それが完了した時にdecする. だから, dm_wait_for_completionを通るためには, 「ackをすればいい」. 永続性については何も言及していないことがポイント. もし永続性について言及したいならば, REQ_FLUSHをflagしたbioをwqのtailに追加してからflushするという実装がとられるだろうから, フレームワークは永続化までは期待していない.