LeavaTailの日記

LeavaTailの日記

Linuxエンジニアを目指した技術者の備忘録

Linuxカーネルのファイルアクセスの処理を追いかける (13) block: blk_mq

関連記事

概要

QEMUの vexpress-a9 (arm) で Linux 5.15を起動させながら、ファイル書き込みのカーネル処理を確認していく。

本章では、blk_mq_submit_bio関数を確認した。

はじめに

ユーザプロセスはファイルシステムという機構によって記憶装置上のデータをファイルという形式で書き込み・読み込みすることができる。
本調査では、ユーザプロセスがファイルに書き込み要求を実行したときにLinuxカーネルではどのような処理が実行されるかを読み解いていく。

調査対象や環境などはPart 1: 環境セットアップを参照。

調査対象 (シーケンス図)

本記事では、writebackカーネルスレッドがblk_mq_submit_bio関数を呼び出すところから、blk_mq_submit_bio関数を呼ぶところまでを確認する。

blk-mqの概要

Multi-Queue Block (blk-mq) はブロックデバイスに IOリクエストを複数個のキューで管理することで高速なストレージデバイスでの並列処理によりボトルネックを解消するカーネルの機構の一つである。

blk-mqには、Software Staging queues と Hardware dispatch queues の2つの種類キューを持つ。

https://hyunyoung2.github.io/img/Image/SSD-Solid_State_Drives/2016-09-14-Multi_Queue/Mutil_queue3.png

https://hyunyoung2.github.io/2016/09/14/Multi_Queue

Software Staging queues と Hardware dispatch queue

Software staging queues (struct blk_mq_ctx) は複数個のキューがあり、隣接したセクターをマージしたり、Hardware dispatch queuesにキューイングする前にrequestを並び替える。

Hardware dispatch queues (struct blk_mq_hw_ctx) はデバイス側のキューと同数が生成され、デバイスドライバがrequestを投げる。

blk-mq のキューの全体像

この時、CPUがIOリクエストを特定できるよう Tagging がされる。

sbitmap

blk-mq では、Tagging に sbitmap と呼ばれるデータ構造を用いて管理する。

通常の bitmap では、管理した特定のデータを 1bit 単位に分割して、 0 or 1 を管理する。
この時、管理するデータが大きくなると、排他制御によるパフォーマンスの劣化が問題となっていた。

sbitmapでは、ビットマップを細分化して、0 or 1 を管理する。
これにより、排他制御によるパフォーマンスの劣化問題を緩和させている。

bitmapとsbitmapの比較

sbitmapは、Linux Kernelの sbitmap構造体によって管理される。
sbitmap構造体には、sbitmap_wordを管理するためのメンバ (depthmap_nrなど) が存在している。

この構造体のメンバmapが実際に 0 or 1 を管理するbitmap (sbitmap_word)となっている。

また、sbitmap構造体は sbitmap_queueによる Wait Queue を用いて管理される。

sbitmap_queue構造体

writepages関数から呼ばれるblk_mq_submit_bio関数

writepages関数から呼ばれるblk_mq_submit_bio関数実行前の状態

blk_mq_submit_bio関数は、ブロックデバイス (Multi-Queue)にrequestを作成する。

blk_mq_submit_bio関数の定義は下記の通りとなっている。

// 2177:
blk_qc_t blk_mq_submit_bio(struct bio *bio)
{
    struct request_queue *q = bio->bi_bdev->bd_disk->queue;
    const int is_sync = op_is_sync(bio->bi_opf);
    const int is_flush_fua = op_is_flush(bio->bi_opf);
    struct blk_mq_alloc_data data = {
        .q      = q,
    };
    struct request *rq;
    struct blk_plug *plug;
    struct request *same_queue_rq = NULL;
    unsigned int nr_segs;
    blk_qc_t cookie;
    blk_status_t ret;
    bool hipri;

    blk_queue_bounce(q, &bio);
    __blk_queue_split(&bio, &nr_segs);

    if (!bio_integrity_prep(bio))
        goto queue_exit;

    if (!is_flush_fua && !blk_queue_nomerges(q) &&
        blk_attempt_plug_merge(q, bio, nr_segs, &same_queue_rq))
        goto queue_exit;

    if (blk_mq_sched_bio_merge(q, bio, nr_segs))
        goto queue_exit;

    rq_qos_throttle(q, bio);

    hipri = bio->bi_opf & REQ_HIPRI;

    data.cmd_flags = bio->bi_opf;
    rq = __blk_mq_alloc_request(&data);
    if (unlikely(!rq)) {
        rq_qos_cleanup(q, bio);
        if (bio->bi_opf & REQ_NOWAIT)
            bio_wouldblock_error(bio);
        goto queue_exit;
    }

    trace_block_getrq(bio);

    rq_qos_track(q, rq, bio);

    cookie = request_to_qc_t(data.hctx, rq);

    blk_mq_bio_to_request(rq, bio, nr_segs);

    ret = blk_crypto_init_request(rq);
    if (ret != BLK_STS_OK) {
        bio->bi_status = ret;
        bio_endio(bio);
        blk_mq_free_request(rq);
        return BLK_QC_T_NONE;
    }

    plug = blk_mq_plug(q, bio);
    if (unlikely(is_flush_fua)) {
        /* Bypass scheduler for flush requests */
        blk_insert_flush(rq);
        blk_mq_run_hw_queue(data.hctx, true);
    } else if (plug && (q->nr_hw_queues == 1 ||
           blk_mq_is_sbitmap_shared(rq->mq_hctx->flags) ||
           q->mq_ops->commit_rqs || !blk_queue_nonrot(q))) {
        /*
        * Use plugging if we have a ->commit_rqs() hook as well, as
        * we know the driver uses bd->last in a smart fashion.
        *
        * Use normal plugging if this disk is slow HDD, as sequential
        * IO may benefit a lot from plug merging.
        */
        unsigned int request_count = plug->rq_count;
        struct request *last = NULL;

        if (!request_count)
            trace_block_plug(q);
        else
            last = list_entry_rq(plug->mq_list.prev);

        if (request_count >= blk_plug_max_rq_count(plug) || (last &&
            blk_rq_bytes(last) >= BLK_PLUG_FLUSH_SIZE)) {
            blk_flush_plug_list(plug, false);
            trace_block_plug(q);
        }

        blk_add_rq_to_plug(plug, rq);
    } else if (q->elevator) {
        /* Insert the request at the IO scheduler queue */
        blk_mq_sched_insert_request(rq, false, true, true);
    } else if (plug && !blk_queue_nomerges(q)) {
        /*
        * We do limited plugging. If the bio can be merged, do that.
        * Otherwise the existing request in the plug list will be
        * issued. So the plug list will have one request at most
        * The plug list might get flushed before this. If that happens,
        * the plug list is empty, and same_queue_rq is invalid.
        */
        if (list_empty(&plug->mq_list))
            same_queue_rq = NULL;
        if (same_queue_rq) {
            list_del_init(&same_queue_rq->queuelist);
            plug->rq_count--;
        }
        blk_add_rq_to_plug(plug, rq);
        trace_block_plug(q);

        if (same_queue_rq) {
            data.hctx = same_queue_rq->mq_hctx;
            trace_block_unplug(q, 1, true);
            blk_mq_try_issue_directly(data.hctx, same_queue_rq,
                    &cookie);
        }
    } else if ((q->nr_hw_queues > 1 && is_sync) ||
            !data.hctx->dispatch_busy) {
        /*
        * There is no scheduler and we can try to send directly
        * to the hardware.
        */
        blk_mq_try_issue_directly(data.hctx, rq, &cookie);
    } else {
        /* Default case. */
        blk_mq_sched_insert_request(rq, false, true, true);
    }

    if (!hipri)
        return BLK_QC_T_NONE;
    return cookie;
queue_exit:
    blk_queue_exit(q);
    return BLK_QC_T_NONE;
}
type name Description
request_queue *q
blk_mq_req_flags_t flags
unsigned int shallow_depth
unsigned int cmd_flags
struct blk_mq_ctx *ctx Software staging queues
struct blk_mq_hq_ctx *hctx Hardware dispatch queues

bounce buffersの作成

blk_queue_bounce関数は、bounce buffersを作成する関数となっている。

バイスブロックとデバイスのやり取りは、bioに設定されているページとDMAで行っています。もしそのページがハイメモリ等の、デバイスとDMAでやり取りできないページだと、元のbioを複製した物に、DMAとやり取りするページを割り当てる事でDMA転送を行います。このbioをバウンスバッファーと言うそうです。新たに作成したbioには、bi_privateメンバーに、元のbioが設定されており、読み込みなら、新たに設定したブロックIO終了コールバック関数で、DMA転送されたデータを、元のbioのページに転送する事になります。

https://wiki.bit-hive.com/north/pg/バウンスバッファー

// 324:
static inline void blk_queue_bounce(struct request_queue *q, struct bio **bio)
{
    if (unlikely(blk_queue_may_bounce(q) && bio_has_data(*bio)))
        __blk_queue_bounce(q, bio);  
}
// 317:
static inline bool blk_queue_may_bounce(struct request_queue *q)
{
    return IS_ENABLED(CONFIG_BOUNCE) &&
        q->limits.bounce == BLK_BOUNCE_HIGH &&
        max_low_pfn >= max_pfn;
}

bounce buffersを作成する条件としては、blk_queue_may_bounce関数で判定することができる。

  • カーネルbounce buffersを有効にしている
  • バイスのキューのbounce buffersを使用できる
  • lowmemの終了PFNがシステム最後のページのPFN

ただし、ここではq->limits.bounce = BLK_BOUNCE_NONE (bounceしない)ようとなっているので、bounce buffersは作成しない。

bioの分割

__blk_queue_split関数はbioを分割し、後続のbioを発行する。

// 305:
void __blk_queue_split(struct bio **bio, unsigned int *nr_segs)
{
    struct request_queue *q = (*bio)->bi_bdev->bd_disk->queue;
    struct bio *split = NULL;

    switch (bio_op(*bio)) {
    case REQ_OP_DISCARD:
    case REQ_OP_SECURE_ERASE:
        split = blk_bio_discard_split(q, *bio, &q->bio_split, nr_segs);
        break;
    case REQ_OP_WRITE_ZEROES:
        split = blk_bio_write_zeroes_split(q, *bio, &q->bio_split,
                nr_segs);
        break;
    case REQ_OP_WRITE_SAME:
        split = blk_bio_write_same_split(q, *bio, &q->bio_split,
                nr_segs);
        break;
    default:
        /*
        * All drivers must accept single-segments bios that are <=
        * PAGE_SIZE.  This is a quick and dirty check that relies on
        * the fact that bi_io_vec[0] is always valid if a bio has data.
        * The check might lead to occasional false negatives when bios
        * are cloned, but compared to the performance impact of cloned
        * bios themselves the loop below doesn't matter anyway.
        */
        if (!q->limits.chunk_sectors &&
            (*bio)->bi_vcnt == 1 &&
            ((*bio)->bi_io_vec[0].bv_len +
             (*bio)->bi_io_vec[0].bv_offset) <= PAGE_SIZE) {
            *nr_segs = 1;
            break;
        }
        split = blk_bio_segment_split(q, *bio, &q->bio_split, nr_segs);
        break;
    }

    if (split) {
        /* there isn't chance to merge the splitted bio */
        split->bi_opf |= REQ_NOMERGE;

        bio_chain(split, *bio);
        trace_block_split(split, (*bio)->bi_iter.bi_sector);
        submit_bio_noacct(*bio);
        *bio = split;

        blk_throtl_charge_bio_split(*bio);
    }
}

特殊なbio (REQ_OP_DISCARD, REQ_OP_SECURE_ERASE, REQ_OP_WRITE_ZEROES, REQ_OP_WRITE_SAME)でない限り、 分割可能か(簡易)チェックが入る。

  • RAIDやzonedによってchunk_sectorsが設定されていない場合
  • 配列bio_vecに格納されているエントリ数が1つのみの場合
  • データ開始位置のページ内オフセット + データ長がページサイズ内に収まる場合

上記の条件をすべて満たす場合には、分割不可能と判断してbioの分割は実施しない。

ブロックレイヤのデータ整合性

bio_integrity_prep関数は、ブロックレイヤにおけるデータ整合性の機能に必要な準備をする関数となっている。

lwn.net

この関数は、KconfigのCONFIG_BLK_DEV_INTEGRITYによって呼び出し先の関数が異なる。

この環境では CONFIG_BLK_DEV_INTEGRITY=nとなっているため trueを返すだけの関数となっている。

// 732:
static inline bool bio_integrity_prep(struct bio *bio)
{
    return true;
}

リクエストのマージ確認

// 2199:
    if (!is_flush_fua && !blk_queue_nomerges(q) &&
        blk_attempt_plug_merge(q, bio, nr_segs, &same_queue_rq))
        goto queue_exit;

    if (blk_mq_sched_bio_merge(q, bio, nr_segs))
        goto queue_exit;
  • REQ_FUAREQ_PREFLUSHが設定されていない
  • バイスQUEUE_FLAG_NOMERGES

上記の条件をすべて満たす場合、かつblk_attempt_plug_merge関数がtrueとなった場合、queue_exitラベルでblk_mq_submit_bio関数を終了する。

blk_attempt_plug_merge関数は、plugged list内のBIOをマージする関数となっている。

// 1043:
bool blk_attempt_plug_merge(struct request_queue *q, struct bio *bio,
        unsigned int nr_segs, struct request **same_queue_rq)
{
    struct blk_plug *plug;
    struct request *rq;
    struct list_head *plug_list;

    plug = blk_mq_plug(q, bio);
    if (!plug)
        return false;

    plug_list = &plug->mq_list;

    list_for_each_entry_reverse(rq, plug_list, queuelist) {
        if (rq->q == q && same_queue_rq) {
            /*
            * Only blk-mq multiple hardware queues case checks the
            * rq in the same queue, there should be only one such
            * rq in a queue
            **/
            *same_queue_rq = rq;
        }

        if (rq->q != q)
            continue;

        if (blk_attempt_bio_merge(q, rq, bio, nr_segs, false) ==
            BIO_MERGE_OK)
            return true;
    }

    return false;
}

plugged list内のmq_listから、各requestに対してblk_attempt_bio_merge関数を実行する。
ただし、ここではmq_listは一つのみであるので falseを返すだけの関数となっている。

また、一方でblk_mq_sched_bio_merge関数はIO schedulerによる IOのマージを試みる関数となっている。

// 33:
static inline bool
blk_mq_sched_bio_merge(struct request_queue *q, struct bio *bio,
        unsigned int nr_segs)
{
    if (blk_queue_nomerges(q) || !bio_mergeable(bio))
        return false;

    return __blk_mq_sched_bio_merge(q, bio, nr_segs);
}
  • バイスQUEUE_FLAG_NOMERGES
  • bioがREQ_NOMERGE_FLAGS

上記の条件をいずれも満たさない場合、blk_mq_sched_bio_merge関数を実行する。

__blk_mq_sched_bio_merge関数は、IOスケジューラによるbioのマージを実施する関数となっている。

// 366:
bool __blk_mq_sched_bio_merge(struct request_queue *q, struct bio *bio,
        unsigned int nr_segs)
{
    struct elevator_queue *e = q->elevator;
    struct blk_mq_ctx *ctx;
    struct blk_mq_hw_ctx *hctx;
    bool ret = false;
    enum hctx_type type;

    if (e && e->type->ops.bio_merge)
        return e->type->ops.bio_merge(q, bio, nr_segs);

    ctx = blk_mq_get_ctx(q);
    hctx = blk_mq_map_queue(q, bio->bi_opf, ctx);
    type = hctx->type;
    if (!(hctx->flags & BLK_MQ_F_SHOULD_MERGE) ||
        list_empty_careful(&ctx->rq_lists[type]))
        return false;

    /* default per sw-queue merge */
    spin_lock(&ctx->lock);
    /*
    * Reverse check our software queue for entries that we could
    * potentially merge with. Currently includes a hand-wavy stop
    * count of 8, to not spend too much time checking for merges.
    */
    if (blk_bio_list_merge(q, &ctx->rq_lists[type], bio, nr_segs)) {
        ctx->rq_merged++;
        ret = true;
    }

    spin_unlock(&ctx->lock);

    return ret;
}

IO Schedulerはマージ処理に対応している場合は固有のbio_merge処理を呼び出し、 そうでばければblk-mq汎用のマージ処理を試みる。

ここではmq_deadlineが設定されており、かつbio_mergeとしてdd_bio_merge処理が定義されているので、それを呼び出す。

MQ-deadline IO schedulerの詳細はここでは割愛する。

ここでは、e->type->ops.bio_mergeではfalseが返される。

requestの作成

__blk_mq_alloc_request関数は、requestを作成する関数となっている。

// 355:
static struct request *__blk_mq_alloc_request(struct blk_mq_alloc_data *data)
{
    struct request_queue *q = data->q;
    struct elevator_queue *e = q->elevator;
    u64 alloc_time_ns = 0;
    unsigned int tag;

    /* alloc_time includes depth and tag waits */
    if (blk_queue_rq_alloc_time(q))
        alloc_time_ns = ktime_get_ns();

    if (data->cmd_flags & REQ_NOWAIT)
        data->flags |= BLK_MQ_REQ_NOWAIT;

    if (e) {
        /*
        * Flush/passthrough requests are special and go directly to the
        * dispatch list. Don't include reserved tags in the
        * limiting, as it isn't useful.
        */
        if (!op_is_flush(data->cmd_flags) &&
            !blk_op_is_passthrough(data->cmd_flags) &&
            e->type->ops.limit_depth &&
            !(data->flags & BLK_MQ_REQ_RESERVED))
            e->type->ops.limit_depth(data->cmd_flags, data);
    }

retry:
    data->ctx = blk_mq_get_ctx(q);
    data->hctx = blk_mq_map_queue(q, data->cmd_flags, data->ctx);
    if (!e)
        blk_mq_tag_busy(data->hctx);

    /*
    * Waiting allocations only fail because of an inactive hctx.  In that
    * case just retry the hctx assignment and tag allocation as CPU hotplug
    * should have migrated us to an online CPU by now.
    */
    tag = blk_mq_get_tag(data);
    if (tag == BLK_MQ_NO_TAG) {
        if (data->flags & BLK_MQ_REQ_NOWAIT)
            return NULL;

        /*
        * Give up the CPU and sleep for a random short time to ensure
        * that thread using a realtime scheduling class are migrated
        * off the CPU, and thus off the hctx that is going away.
        */
        msleep(3);
        goto retry;
    }
    return blk_mq_rq_ctx_init(data, tag, alloc_time_ns);
}

最初の処理にあるblk_queue_rq_alloc_timeは、Kconfigによって処理が異なる関数となっている。

// 620:
#ifdef CONFIG_BLK_RQ_ALLOC_TIME
#define blk_queue_rq_alloc_time(q)  \
   test_bit(QUEUE_FLAG_RQ_ALLOC_TIME, &(q)->queue_flags)
#else
#define blk_queue_rq_alloc_time(q)  false
#endif

CONFIG_BLK_RQ_ALLOC_TIME=yの場合には、このrequestにbioが追加された最初の時間が記録されるようになる。
ただし、ここではCONFIG_BLK_RQ_ALLOC_TIME=nとなっているため、falseを返すだけの関数となる。

また、IO schedulerが登録されている場合には、

  • bioがREQ_FUAまたはREQ_PREFLUSHではない場合
  • bioがREQ_OP_DRV_INまたはREQ_OP_DRV_OUTではない場合
  • IO scheduler が limit_depthをサポートしている場合
  • allocate from reserved poolではない場合

上記のすべてを満たす場合、IO schedulerのlimit_depthを実行し、Hardware Queueの数をヒントとして持って置く。

キューの取得

その後、blk_mq_get_ctx関数で現在のCPU用のSoftware Queueを、blk_mq_map_queue関数でHardware Queueを取得する。

// 146:
static inline struct blk_mq_ctx *blk_mq_get_ctx(struct request_queue *q)
{
    return __blk_mq_get_ctx(q, raw_smp_processor_id());
}
// 134:
static inline struct blk_mq_ctx *__blk_mq_get_ctx(struct request_queue *q,
                       unsigned int cpu)
{
    return per_cpu_ptr(q->queue_ctx, cpu);
}

一方で、blk_mq_map_queue関数は次のような定義となっている。

// 105:
static inline struct blk_mq_hw_ctx *blk_mq_map_queue(struct request_queue *q,
                             unsigned int flags,
                             struct blk_mq_ctx *ctx)
{
    enum hctx_type type = HCTX_TYPE_DEFAULT;

    /*
    * The caller ensure that if REQ_HIPRI, poll must be enabled.
    */
    if (flags & REQ_HIPRI)
        type = HCTX_TYPE_POLL;
    else if ((flags & REQ_OP_MASK) == REQ_OP_READ)
        type = HCTX_TYPE_READ;
    
    return ctx->hctxs[type];
}

Hardware queueには4種類あり、通常の書き込みであれば HCTX_TYPE_DEFAULTが選択される。

// 198:
/**
 * enum hctx_type - Type of hardware queue
 * @HCTX_TYPE_DEFAULT: All I/O not otherwise accounted for.
 * @HCTX_TYPE_READ:    Just for READ I/O.
 * @HCTX_TYPE_POLL:    Polled I/O of any kind.
 * @HCTX_MAX_TYPES:    Number of types of hctx.
 */
enum hctx_type {
    HCTX_TYPE_DEFAULT,
    HCTX_TYPE_READ,
    HCTX_TYPE_POLL,

    HCTX_MAX_TYPES,
};
tagの取得

キューからタグを取得するためには、blk_mq_get_tag関数を呼び出す。

// 90:
unsigned int blk_mq_get_tag(struct blk_mq_alloc_data *data)
{
    struct blk_mq_tags *tags = blk_mq_tags_from_data(data);
    struct sbitmap_queue *bt;
    struct sbq_wait_state *ws;
    DEFINE_SBQ_WAIT(wait);
    unsigned int tag_offset;
    int tag;

    if (data->flags & BLK_MQ_REQ_RESERVED) {
        if (unlikely(!tags->nr_reserved_tags)) {
            WARN_ON_ONCE(1);
            return BLK_MQ_NO_TAG;
        }
        bt = tags->breserved_tags;
        tag_offset = 0;
    } else {
        bt = tags->bitmap_tags;
        tag_offset = tags->nr_reserved_tags;
    }

    tag = __blk_mq_get_tag(data, bt);
    if (tag != BLK_MQ_NO_TAG)
        goto found_tag;

    if (data->flags & BLK_MQ_REQ_NOWAIT)
        return BLK_MQ_NO_TAG;

    ws = bt_wait_ptr(bt, data->hctx);
    do {
        struct sbitmap_queue *bt_prev;

        /*
        * We're out of tags on this hardware queue, kick any
        * pending IO submits before going to sleep waiting for
        * some to complete.
        */
        blk_mq_run_hw_queue(data->hctx, false);

        /*
        * Retry tag allocation after running the hardware queue,
        * as running the queue may also have found completions.
        */
        tag = __blk_mq_get_tag(data, bt);
        if (tag != BLK_MQ_NO_TAG)
            break;

        sbitmap_prepare_to_wait(bt, ws, &wait, TASK_UNINTERRUPTIBLE);

        tag = __blk_mq_get_tag(data, bt);
        if (tag != BLK_MQ_NO_TAG)
            break;

        bt_prev = bt;
        io_schedule();

        sbitmap_finish_wait(bt, ws, &wait);

        data->ctx = blk_mq_get_ctx(data->q);
        data->hctx = blk_mq_map_queue(data->q, data->cmd_flags,
                        data->ctx);
        tags = blk_mq_tags_from_data(data);
        if (data->flags & BLK_MQ_REQ_RESERVED)
            bt = tags->breserved_tags;
        else
            bt = tags->bitmap_tags;

        /*
        * If destination hw queue is changed, fake wake up on
        * previous queue for compensating the wake up miss, so
        * other allocations on previous queue won't be starved.
        */
        if (bt != bt_prev)
            sbitmap_queue_wake_up(bt_prev);

        ws = bt_wait_ptr(bt, data->hctx);
    } while (1);

    sbitmap_finish_wait(bt, ws, &wait);

found_tag:
    /*
    * Give up this allocation if the hctx is inactive.  The caller will
    * retry on an active hctx.
    */
    if (unlikely(test_bit(BLK_MQ_S_INACTIVE, &data->hctx->state))) {
        blk_mq_put_tag(tags, data->ctx, tag + tag_offset);
        return BLK_MQ_NO_TAG;
    }
    return tag + tag_offset;
}

blk_mq_get_tag関数は、waitの取り扱いに考慮しながら__blk_mq_get_tag関数を呼び出すことでタグを取得することができる。

// 168:
static inline struct blk_mq_tags *blk_mq_tags_from_data(struct blk_mq_alloc_data *data)
{
    if (data->q->elevator)
        return data->hctx->sched_tags;

    return data->hctx->tags;
}

tagsのポインタは、IO schedulerが登録されている場合には sched_tags を、そうでなければ tags をそのまま返す。
sbitmapへのポインタは、"reserved pool"から取得するようなフラグが立っている場合*1nr_reserved_tagsを、そうでなければ、bitmap_tags となる。

これらの情報を基に __blk_mq_ge_tag関数によって、タグを取得する。

// 77:
static int __blk_mq_get_tag(struct blk_mq_alloc_data *data,
                struct sbitmap_queue *bt)
{
    if (!data->q->elevator && !(data->flags & BLK_MQ_REQ_RESERVED) &&
            !hctx_may_queue(data->hctx, bt))
        return BLK_MQ_NO_TAG;

    if (data->shallow_depth)
        return __sbitmap_queue_get_shallow(bt, data->shallow_depth);
    else
        return __sbitmap_queue_get(bt);
}

タグが取得できた場合には、found_tagラベルにジャンプして終了ルーチンに入り、そうでなければ wait queueを用いてタグが取得できるまで待つことになる。 (BLK_MQ_REQ_NOWAITを除く)

リクエストの初期化
// 284:
static struct request *blk_mq_rq_ctx_init(struct blk_mq_alloc_data *data,
        unsigned int tag, u64 alloc_time_ns)
{
    struct blk_mq_tags *tags = blk_mq_tags_from_data(data);
    struct request *rq = tags->static_rqs[tag];

    if (data->q->elevator) {
        rq->tag = BLK_MQ_NO_TAG;
        rq->internal_tag = tag;
    } else {
        rq->tag = tag;
        rq->internal_tag = BLK_MQ_NO_TAG;
    }

    /* csd/requeue_work/fifo_time is initialized before use */
    rq->q = data->q;
    rq->mq_ctx = data->ctx;
    rq->mq_hctx = data->hctx;
    rq->rq_flags = 0;
    rq->cmd_flags = data->cmd_flags;
    if (data->flags & BLK_MQ_REQ_PM)
        rq->rq_flags |= RQF_PM;
    if (blk_queue_io_stat(data->q))
        rq->rq_flags |= RQF_IO_STAT;
    INIT_LIST_HEAD(&rq->queuelist);
    INIT_HLIST_NODE(&rq->hash);
    RB_CLEAR_NODE(&rq->rb_node);
    rq->rq_disk = NULL;
    rq->part = NULL;
#ifdef CONFIG_BLK_RQ_ALLOC_TIME
    rq->alloc_time_ns = alloc_time_ns;
#endif
    if (blk_mq_need_time_stamp(rq))
        rq->start_time_ns = ktime_get_ns();
    else
        rq->start_time_ns = 0;
    rq->io_start_time_ns = 0;
    rq->stats_sectors = 0;
    rq->nr_phys_segments = 0;
#if defined(CONFIG_BLK_DEV_INTEGRITY)
    rq->nr_integrity_segments = 0;
#endif
    blk_crypto_rq_set_defaults(rq);
    /* tag was already set */
    WRITE_ONCE(rq->deadline, 0);

    rq->timeout = 0;

    rq->end_io = NULL;
    rq->end_io_data = NULL;

    data->ctx->rq_dispatched[op_is_sync(data->cmd_flags)]++;
    refcount_set(&rq->ref, 1);

    if (!op_is_flush(data->cmd_flags)) {
        struct elevator_queue *e = data->q->elevator;

        rq->elv.icq = NULL;
        if (e && e->type->ops.prepare_request) {
            if (e->type->icq_cache)
                blk_mq_sched_assign_ioc(rq);

            e->type->ops.prepare_request(rq);
            rq->rq_flags |= RQF_ELVPRIV;
        }
    }

    data->hctx->queued++;
    return rq;
}

blk_mq_rq_ctx_init関数は、タグからリクエストを初期化する。

struct request の初期化

また、IOスケジューラが登録されている場合に、このタイミングでprepare_request処理を実施する。

リクエストとbioの関連付け

初期化したリクエストは blk_mq_bio_to_request関数によって bioと関連付けされる。

// 1952:
static void blk_mq_bio_to_request(struct request *rq, struct bio *bio,
        unsigned int nr_segs)
{
    int err;

    if (bio->bi_opf & REQ_RAHEAD)
        rq->cmd_flags |= REQ_FAILFAST_MASK;

    rq->__sector = bio->bi_iter.bi_sector;
    rq->write_hint = bio->bi_write_hint;
    blk_rq_bio_prep(rq, bio, nr_segs);

    /* This can't fail, since GFP_NOIO includes __GFP_DIRECT_RECLAIM. */
    err = blk_crypto_rq_bio_prep(rq, bio, GFP_NOIO);
    WARN_ON_ONCE(err);

    blk_account_io_start(rq);
}
// 624:
static inline void blk_rq_bio_prep(struct request *rq, struct bio *bio,
        unsigned int nr_segs)
{
    rq->nr_phys_segments = nr_segs;
    rq->__data_len = bio->bi_iter.bi_size;
    rq->bio = rq->biotail = bio;
    rq->ioprio = bio_prio(bio);

    if (bio->bi_bdev)
        rq->rq_disk = bio->bi_bdev->bd_disk;
}

これにより、rqbioのデータ構造が関連付けられる。

blk_mq_bio_to_request関数を実行した結果

リクエストの追加

rqの初期化が完了した後は、IOリクエストの種類によって処理が異なる。

// 2240:
    } else if (plug && (q->nr_hw_queues == 1 ||
           blk_mq_is_sbitmap_shared(rq->mq_hctx->flags) ||
           q->mq_ops->commit_rqs || !blk_queue_nonrot(q))) {

今回のケースでは、nr_hw_queue == 1であり、 shared bitmapであるため、この条件分岐にマッチする。

// 2243:
        /*
        * Use plugging if we have a ->commit_rqs() hook as well, as
        * we know the driver uses bd->last in a smart fashion.
        *
        * Use normal plugging if this disk is slow HDD, as sequential
        * IO may benefit a lot from plug merging.
        */
        unsigned int request_count = plug->rq_count;
        struct request *last = NULL;

        if (!request_count)
            trace_block_plug(q);
        else
            last = list_entry_rq(plug->mq_list.prev);

        if (request_count >= blk_plug_max_rq_count(plug) || (last &&
            blk_rq_bytes(last) >= BLK_PLUG_FLUSH_SIZE)) {
            blk_flush_plug_list(plug, false);
            trace_block_plug(q);
        }

        blk_add_rq_to_plug(plug, rq);

この場合、blk_plugにリクエストを追加する。 (plugがmaxの場合は、事前に flush する必要がある)

blk_plugへのリクエストの追加は blk_add_rq_to_plug関数によって実施される。

// 2136:
static void blk_add_rq_to_plug(struct blk_plug *plug, struct request *rq)
{
    list_add_tail(&rq->queuelist, &plug->mq_list);
    plug->rq_count++;
    if (!plug->multiple_queues && !list_is_singular(&plug->mq_list)) {
        struct request *tmp;

        tmp = list_first_entry(&plug->mq_list, struct request,
                        queuelist);
        if (tmp->q != rq->q)
            plug->multiple_queues = true;
    }
}

multiple_queueを使用している場合には multiple_queuesフラグを立てる。
ただし、今回の環境では存在していないので、リストplug->mq_listに 要素rq_queuelistを追加するだけとなる。

write_inode関数から呼ばれるblk_mq_submit_bio関数

writepages関数から呼ばれた場合とシーケンスが同じになるため、省略する。

おわりに

本記事では、ブロックレイヤのblk_mq_submit_bio関数を確認した。

変更履歴

  • 2022/11/26: 記事公開

参考

*1:v5.15では、Micron P320 SSD や NVMeドライバ のみ対応している

Phoronix Test Suite で Diskベンチマークを実施する

概要

dockerコンテナでPhoronix Test Suite のDiskベンチマークを実施した。

本記事のベンチマーク結果については、OpenBenchmarking.org にアップロードした。

openbenchmarking.org

openbenchmarking.org

はじめに

Phoronix Test Suite は Linux含め様々なOSで利用できるベンチマークプラットフォームである。

これによって、テストのインストールから実行・レポートまでを完全に自動化された方法でテストすることができる。
また、その特徴から Extensible Architecture (拡張可能容易性) に優れており、XMLファイルやbashスクリプトで構成されるアーキテクチャから簡単にテストを追加することができる。

Phoronix Test Suite では、600以上のテストプロファイルと、200以上のテストスイートがデフォルトで用意されている。
これらのテストスイートは、"Processor", "Graphics", "Disk", "Memory"などで分類されている。

ここでは、Diskベンチマーク ("sqlite", "fs-mark", "compilebench", "ior", "iozone" "dbench", "postmark", "fio") を 実行し、手順や拡張方法などを確認する。

実験環境

本記事で使用した開発用PC (Host PC)の構成は次の通りとなっている。

環境 概要
CPU AMD Ryzen 3 3300X
RAM DDR4-2666 16GB ×2
Host OS Ubuntu Desktop 22.04.1
kernel v5.15.0-47-generic
Storage SL-MG5
File-Syste f2fs
Mount Option errors=remount-ro relatime rw
Disk Scheduler MQ-DEADLINE
Disk Details BlockSize: 4096

また、開発用PCにはDockerがインストールされており、ログインユーザはroot権限なしでdockerを実行できるものとする。

Phoronix Test Suite のインストール

Phoronix Test Suiteでは、必要なパッケージがインストールされたDockerコンテナを提供している。

hub.docker.com

本記事では、このイメージを利用してDiskベンチマークを実施する。

$ docker pull phoronix/pts/

Phoronix Test Suite が動作することを確認する。

$ docker run -it phoronix/pts

Updated OpenBenchmarking.org Repository Index
pts: 495 Distinct Tests, 2101 Test Versions, 56 Suites
Available Changes From 13 February To 3 October
Updated Test:   pts/ai-benchmark       v1.0.2  AI Benchmark Alpha
Updated Test:   pts/aircrack-ng        v1.3.0  Aircrack-ng
Updated Test:   pts/aom-av1            v3.5.0  AOM AV1
Updated Test:   pts/apache             v2.0.1  Apache HTTP Server
Updated Test:   pts/astcenc            v1.4.0  ASTC Encoder
Updated Test:   pts/avifenc            v1.2.0  libavif avifenc
Updated Test:   pts/blender            v3.3.1  Blender
Updated Test:   pts/blosc              v1.2.0  C-Blosc
Updated Test:   pts/brl-cad            v1.3.0  BRL-CAD
Updated Test:   pts/build-erlang       v1.2.0  Timed Erlang/OTP Compilation
Updated Test:   pts/build-linux-kernel v1.14.0 Timed Linux Kernel Compilation
Updated Test:   pts/build-mplayer      v1.5.0  Timed MPlayer Compilation
Updated Test:   pts/build-nodejs       v1.2.0  Timed Node.js Compilation
Updated Test:   pts/build-php          v1.6.0  Timed PHP Compilation
New Test:       pts/build-python       v1.0.0  Timed CPython Compilation
Updated Test:   pts/build-wasmer       v1.2.0  Timed Wasmer Compilation
Updated Test:   pts/chia-vdf           v1.1.0  Chia Blockchain VDF
New Test:       pts/clickhouse         v1.1.0  ClickHouse
Updated Test:   pts/compress-7zip      v1.10.0 7-Zip Compression
Updated Test:   pts/couchdb            v1.2.0  Apache CouchDB
Updated Test:   pts/csgo               v1.7.1  Counter-Strike: Global Offensive
Updated Test:   pts/dav1d              v1.12.0 dav1d
Updated Test:   pts/ddnet              v1.3.0  DDraceNetwork
New Test:       pts/dragonflydb        v1.0.0  Dragonflydb
Updated Test:   pts/encode-flac        v1.8.0  FLAC Audio Encoding
New Test:       pts/etcd               v1.0.0  etcd
Updated Test:   pts/etcpak             v1.1.0  Etcpak
Updated Test:   pts/ethr               v1.2.0  Ethr
New Test:       pts/fast-cli           v1.0.0  fast-cli
Updated Test:   pts/glibc-bench        v1.7.2  Glibc Benchmarks
Updated Test:   pts/graphics-magick    v2.1.0  GraphicsMagick
Updated Test:   pts/gravitymark        v1.7.0  GravityMark
Updated Test:   pts/gromacs            v1.7.0  GROMACS
Updated Test:   pts/influxdb           v1.0.1  InfluxDB
Updated Test:   pts/java-jmh           v1.0.1  Java JMH
Updated Test:   pts/lammps             v1.4.0  LAMMPS Molecular Dynamics Simulator
New Test:       pts/memcached          v1.0.0  Memcached
Updated Test:   pts/memtier-benchmark  v1.4.0  memtier_benchmark
Updated Test:   pts/mnn                v2.1.0  Mobile Neural Network
Updated Test:   pts/mysqlslap          v1.3.0  MariaDB
Updated Test:   pts/natron             v1.1.0  Natron
Updated Test:   pts/ncnn               v1.4.0  NCNN
Updated Test:   pts/nettle             v1.1.0  Nettle
Updated Test:   pts/nginx              v2.0.1  nginx
Updated Test:   pts/node-web-tooling   v1.0.1  Node.js V8 Web Tooling Benchmark
Updated Test:   pts/onednn             v2.7.0  oneDNN
Updated Test:   pts/onnx               v1.5.0  ONNX Runtime
Updated Test:   pts/opencv             v1.2.0  OpenCV
Updated Test:   pts/openfoam           v1.2.0  OpenFOAM
Updated Test:   pts/openvino           v1.1.0  OpenVINO
Updated Test:   pts/ospray             v2.10.0 OSPRay
New Test:       pts/ospray-studio      v1.1.0  OSPRay Studio
Updated Test:   pts/paraview           v1.3.0  ParaView
Updated Test:   pts/perf-bench         v1.0.4  perf-bench
Updated Test:   pts/portal2            v1.1.1  Portal 2
Updated Test:   pts/primesieve         v1.9.0  Primesieve
Updated Test:   pts/qmcpack            v1.5.0  QMCPACK
Updated Test:   pts/redis              v1.4.0  Redis
Updated Test:   pts/renaissance        v1.3.0  Renaissance
Updated Test:   pts/rocksdb            v1.3.0  Facebook RocksDB
Updated Test:   pts/simdjson           v2.0.1  simdjson
Updated Test:   pts/smhasher           v1.1.0  SMHasher
New Test:       pts/spark              v1.0.0  Apache Spark
New Test:       pts/specviewperf2020   v1.0.0  SPECViewPerf 2020
New Test:       pts/speedtest-cli      v1.0.0  speedtest-cli
Updated Test:   pts/srsran             v1.2.0  srsRAN
Updated Test:   pts/stockfish          v1.4.0  Stockfish
Updated Test:   pts/stress-ng          v1.5.1  Stress-NG
Updated Test:   pts/svt-av1            v2.6.0  SVT-AV1
Updated Test:   pts/svt-hevc           v1.2.1  SVT-HEVC
Updated Test:   pts/svt-vp9            v1.3.1  SVT-VP9
Updated Test:   pts/tensorflow-lite    v1.1.0  TensorFlow Lite
Updated Test:   pts/tf2                v1.2.4  Team Fortress 2
New Test:       pts/tww3               v1.0.1  Total War: WARHAMMER III
Updated Test:   pts/unpack-linux       v1.2.0  Unpacking The Linux Kernel
Updated Test:   pts/unvanquished       v1.7.0  Unvanquished
Updated Test:   pts/v-ray              v1.4.0  Chaos Group V-RAY
Updated Test:   pts/vkmark             v1.3.1  VKMark
Updated Test:   pts/webp               v1.2.0  WebP Image Encode
Updated Test:   pts/webp2              v1.2.0  WebP2 Image Encode
Updated Test:   pts/x264               v2.7.0  x264
Updated Test:   pts/xonotic            v1.6.0  Xonotic
Updated Test:   pts/y-cruncher         v1.2.0  Y-Cruncher
Updated Test:   pts/yquake2            v1.3.0  yquake2
Updated Suite:  pts/compilation        v1.2.8  Timed Code Compilation
Updated Suite:  pts/database           v1.3.6  Database Test Suite
New Suite:      pts/internet-speed     v1.0.0  Internet Speed
Updated Suite:  pts/oneapi             v1.3.3  Intel oneAPI
Updated Suite:  pts/raytracing         v1.0.2  Raytracing
Updated Suite:  pts/steam              v1.0.7  Steam
Updated OpenBenchmarking.org Repository Index
system: 41 Distinct Tests, 125 Test Versions
Available Changes From 13 February To 3 October
Updated Test:  system/blender            v1.2.1  Blender
Updated Test:  system/inkscape           v1.0.1  Inkscape
Updated Test:  system/selenium           v1.0.31 Selenium
New Test:      system/timed-battery-test v1.0.0  Timed Battery Test
Updated OpenBenchmarking.org Repository Index
git: 8 Distinct Tests, 11 Test Versions
Available Changes From 13 February To 3 October
Updated Test:  git/dav1d v1.1.0 dav1d

Phoronix Test Suite v10.8.3
An outdated version of the Phoronix Test Suite is installed.
The version in use is 10.8.3 (10830), but the latest is pts-core 10840.
Visit https://www.phoronix-test-suite.com/ to update this software.


Interactive Shell

Generating Shell Cache...
Refreshing OpenBenchmarking.org Repository Cache...

  PROCESSOR:              AMD Ryzen 3 3300X 4-Core @ 3.80GHz
    Core Count:           4
    Thread Count:         8
    Extensions:           SSE 4.2 + AVX2 + AVX + RDRAND + FSGSBASE
    Cache Size:           16 MB
    Microcode:            0x8701013
    Core Family:          Zen 2
    Scaling Driver:       acpi-cpufreq schedutil (Boost: Enabled)

  GRAPHICS:

  MOTHERBOARD:            ASRock B450M Steel Legend
    BIOS Version:         P2.90

  MEMORY:                 32GB

  DISK:                   2000GB Western Digital WD20EZRZ-22Z
                      + 250GB Samsung SSD 860
                      + 500GB Western Digital WDS500G2B0B
                      + 512GB Storage
    File-System:          overlayfs
    Disk Scheduler:       MQ-DEADLINE

  OPERATING SYSTEM:       Ubuntu 20.04.4 LTS
    Kernel:               5.15.0-48-generic (x86_64)
    System Layer:         Docker
    Security:             itlb_multihit: Not affected
                          + l1tf: Not affected
                          + mds: Not affected
                          + meltdown: Not affected
                          + mmio_stale_data: Not affected
                          + retbleed: Mitigation of untrained return thunk; SMT enabled with STIBP protection
                          + spec_store_bypass: Mitigation of SSB disabled via prctl and seccomp
                          + spectre_v1: Mitigation of usercopy/swapgs barriers and __user pointer sanitization
                          + spectre_v2: Mitigation of Retpolines IBPB: conditional STIBP: always-on RSB filling
                          + srbds: Not affected
                          + tsx_async_abort: Not affected


     CPU Temperature: 32.25 C   CPU Usage (Summary): 0.00  %       GPU Temperature: 40.00 C          Memory Usage: 736   MB         System Uptime 2822  M

Phoronix Test Suite command to run or help for all possible options, commands for a quick overview of options, interactive for a guided experience, system-info to view system hardware/software information, exit to exit. For new users, benchmark is the simplest and most important sub-command. Tab auto-completion support available.

# phoronix-test-suite

このイメージでは、デフォルトで /phoronix-test-suite/phoronix-test-suite shellが起動するようになっており、CLIインターフェースが提供されている。

ベンチマークテストのセットアップ

コンテナ内でブロックデバイスに直接アクセスするため--privilegedオプションを、環境をセットアップしたいので/bin/bashで起動する。

$ docker run --privileged -it phoronix/pts /bin/bash
root@f3739e0f6d23:/# 

コンテナ内のパッケージキャッシュは古いので、手動で更新する。

root@f3739e0f6d23:/# apt update

測定対象のストレージ (/dev/sdd1) を /mntにマウントしておく。

root@f3739e0f6d23:/# mount -t f2fs /dev/sdd1 /mnt/

測定対象のストレージで計測するように、Phoronix Test Suiteの設定ファイルを更新する。 ((システム全体のファイルは /etc/phoronix-test-suite.xml に格納され、ユーザ設定は${HOME}/.phoronix-test-suiteで管理される。 ))

root@f3739e0f6d23:/# /phoronix-test-suite/phoronix-test-suite user-config-set EnvironmentDirectory=/mnt/.

EnvironmentDirectoryは、テストがインストールされるディレクトリとなっており、デフォルトでは~/.phoronix-test-suite/installed-tests/となっている。

テスト実行

/bin/bashから、CLIインターフェースを起動させるためには/phoronix-test-suite/phoronix-test-suite shellを実行する必要がある。

root@f3739e0f6d23:/# /phoronix-test-suite/phoronix-test-suite shell


Phoronix Test Suite v10.8.3
Interactive Shell

Generating Shell Cache...
Refreshing OpenBenchmarking.org Repository Cache...

  PROCESSOR:              AMD Ryzen 3 3300X 4-Core @ 3.80GHz
    Core Count:           4
    Thread Count:         8
    Extensions:           SSE 4.2 + AVX2 + AVX + RDRAND + FSGSBASE
    Cache Size:           16 MB
    Microcode:            0x8701013
    Core Family:          Zen 2
    Scaling Driver:       acpi-cpufreq schedutil (Boost: Enabled)

  GRAPHICS:

  MOTHERBOARD:            ASRock B450M Steel Legend
    BIOS Version:         P2.90

  MEMORY:                 32GB

  DISK:                   2000GB Western Digital WD20EZRZ-22Z
                      + 250GB Samsung SSD 860
                      + 500GB Western Digital WDS500G2B0B
                      + 512GB Storage
    File-System:          f2fs

    Mount Options:        acl active_logs=6 alloc_mode=default background_gc=on checkpoint_merge discard_unit=block extent_cache flush_merge fsync_mode=posix inline_data inline_dentry inline_xattr lazytime mode=adaptive no_heap nodiscard relatime rw user_xattr
    Disk Scheduler:       MQ-DEADLINE

    Disk Details:         Block Size: 4096


  OPERATING SYSTEM:       Ubuntu 20.04.4 LTS
    Kernel:               5.15.0-48-generic (x86_64)
    Compiler:             GCC 9.4.0
    System Layer:         Docker
    Security:             itlb_multihit: Not affected
                          + l1tf: Not affected
                          + mds: Not affected
                          + meltdown: Not affected
                          + mmio_stale_data: Not affected
                          + retbleed: Mitigation of untrained return thunk; SMT enabled with STIBP protection
                          + spec_store_bypass: Mitigation of SSB disabled via prctl and seccomp
                          + spectre_v1: Mitigation of usercopy/swapgs barriers and __user pointer sanitization
                          + spectre_v2: Mitigation of Retpolines IBPB: conditional STIBP: always-on RSB filling
                          + srbds: Not affected
                          + tsx_async_abort: Not affected


     CPU Temperature: 42.25 C   CPU Usage (Summary): 0.25  %       GPU Temperature: 40.00 C          Memory Usage: 723   MB         System Uptime 2857  M

Phoronix Test Suite command to run or help for all possible options, commands for a quick overview of options, interactive for a guided experience, system-info to view system hardware/software information, exit to exit. For new users, benchmark is the simplest and most important     sub-command. Tab auto-completion support available.

# phoronix-test-suite

ここで、File-System:が対象ストレージのフォーマットされたファイルシステム (今回はf2fsでフォーマットしている) になっていることを確認しておく。

CLIインターフェース上でbenchmarkコマンドを実行することで、「依存パッケージのインストール」から「テストの実行」までを実行してくれる。

benchmark diskを実行することで、Diskベンチマークを実行することができる。

# phoronix-test-suite benchmark disk     
<-- snipped -->


SQLite 3.30.1:
    pts/sqlite-2.1.0 [Threads / Copies: 8]
    Test 2 of 42
    Estimated Trial Run Count:    3
    Estimated Test Run-Time:      3 Minutes
    Estimated Time To Completion: 7 Hours, 13 Minutes [04:57 JST]
        Running Pre-Test Script @ 21:44:28
        Started Run 1 @ 21:44:28
        Running Interim Test Script @ 21:45:48
        Started Run 2 @ 21:45:50
        Running Interim Test Script @ 21:46:43
        Started Run 3 @ 21:46:45
        Running Interim Test Script @ 21:47:38
        Started Run 4 @ 21:47:40 *
        Running Interim Test Script @ 21:48:31
        Started Run 5 @ 21:48:33 *
        Running Interim Test Script @ 21:49:23
        Started Run 6 @ 21:49:25 *
        Running Interim Test Script @ 21:50:15
        Started Run 7 @ 21:50:17 *
        Running Interim Test Script @ 21:51:09
        Started Run 8 @ 21:51:11 *
        Running Interim Test Script @ 21:52:03
        Started Run 9 @ 21:52:05 *
        Running Interim Test Script @ 21:52:56
        Started Run 10 @ 21:52:58 *
        Running Interim Test Script @ 21:53:53
        Started Run 11 @ 21:53:55 *
        Running Interim Test Script @ 21:54:48
        Started Run 12 @ 21:54:50 *
        Running Interim Test Script @ 21:55:41
        Started Run 13 @ 21:55:43 *
        Running Interim Test Script @ 21:56:35
        Started Run 14 @ 21:56:37 *
        Running Interim Test Script @ 21:57:28
        Started Run 15 @ 21:57:30 *
        Running Post-Test Script @ 21:58:22

    Threads / Copies: 8:
        78.656
        51.155
        50.141
        49.025
        48.573
        47.527
        49.982
        49.911
        49.744
        52.275
        51.123
        49.219
        49.668
        49.019
        50.29

    Average: 51.754 Seconds
    Deviation: 14.55%
    Samples: 15

    Comparison of 2,536 OpenBenchmarking.org samples since 25 October 2019; median result: 33.69 Seconds. Box plot of samples:
    [                                                                                 *--------------------------*-------------------------------------------------############*##*#*#!*#*|]
                                                                                                                                              This Result (41st Percentile): 51.754 ^
                                                                      15GB SD16G: 701 ^          32GB SE32G: 513 ^                             6001GB Western Digital WD60EFAX-68S: 23.2 ^
                                                                                                                                                              500GB CT500P5SSD8: 32.04 ^
                                                                                                                                               4 x 400GB KXG50ZNV512G TOSHIBA: 65 ^
                                                                                                                                            1 TB APPLE HDD HTS541010A9E662: 87 ^    

<-- snipped -->

上記の実行例では、42個あるDiskベンチマークテストのうち2個目のpts/sqlite-2.1.0 (パラメータ: Threads / copies: 8) を切り出している。

ベンチマークの計測ごとに、OpenBenchmarking.orgのサンプルと共に統計情報も含めレポートされる。

Would you like to save these test results (Y/n):yとすることで、OpenBenchmarking.orgに結果をアップロードすることができる。

openbenchmarking.org

runコマンドでもテストの実行することができる。
このとき、テストにパラメータが指定可能であれば、対話的に決めることができる。

# phoronix-test-suite run pts/fio     


Flexible IO Tester 3.29:
    pts/fio-1.15.0
    Disk Test Configuration
        1: Random Read
        2: Random Write
        3: Sequential Read
        4: Sequential Write
        5: Test All Options
        ** Multiple items can be selected, delimit by a comma. **
        Type: 2,4


        1: IO_uring
        2: POSIX AIO
        3: Sync
        4: Linux AIO
        5: Test All Options
        ** Multiple items can be selected, delimit by a comma. **
        Engine: 3,4


        1: Yes
        2: No
        3: Test All Options
        ** Multiple items can be selected, delimit by a comma. **
        Buffered: 1


        1: No
        2: Yes
        3: Test All Options
        ** Multiple items can be selected, delimit by a comma. **
        Direct: 1


        1:  4KB
        2:  8KB
        3:  16KB
        4:  32KB
        5:  64KB
        6:  128KB
        7:  256KB
        8:  512KB
        9:  1MB
        10: 2MB
        11: 4MB
        12: 8MB
        13: Test All Options
        ** Multiple items can be selected, delimit by a comma. **
        Block Size: 9


        1: Default Test Directory
        2: /mnt
        3: Test All Options
        ** Multiple items can be selected, delimit by a comma. **
        Disk Target: 2

おわりに

本稿では、Phoronix Test SuiteによりDiskベンチマークを実行する手順を確認した。

変更履歴

  • 2022/10/4: 記事公開

参考文献

Linuxカーネルのファイルアクセスの処理を追いかける (12) block: submit_bio

関連記事

概要

QEMUの vexpress-a9 (arm) で Linux 5.15を起動させながら、ファイル書き込みのカーネル処理を確認していく。

本章では、submit_bio関数を確認した。

はじめに

ユーザプロセスはファイルシステムという機構によって記憶装置上のデータをファイルという形式で書き込み・読み込みすることができる。
本調査では、ユーザプロセスがファイルに書き込み要求を実行したときにLinuxカーネルではどのような処理が実行されるかを読み解いていく。

調査対象や環境などはPart 1: 環境セットアップを参照。

調査対象 (シーケンス図)

本記事では、writebackカーネルスレッドがsubmit_bio関数を呼び出すところから、blk_mq_submit_bio関数を呼ぶところまでを確認する。

BIOの概要

Linuxでは、ブロックデバイスにIOのやり取りをするために BIO と呼ばれるデータ構造を用いる。
ファイルシステムからファイルへの書き込みをすることで、writepages関数とwrite_inode関数が呼ばれ、それぞれの関数で次のようなbioが生成する。

(実データの書き込みとinodeの書き込みはそれぞれ別のwriteback kthreadで生成される)

これまでに生成させたBIO

また、bio構造体には、どのような操作するかどうかをbi_opfで管理している。
Linux v5.15では次のような操作群が定義されている。

Operation Value Description
REQ_OP_READ 0 read sectors from the device
REQ_OP_WRITE 1 write sectors to the device
REQ_OP_FLUSH 2 flush the volatile write cache
REQ_OP_DISCARD 3 discard sectors
REQ_OP_SECURE_ERASE 5 securely erase sectors
REQ_OP_WRITE_SAME 7 write the same sector many times
REQ_OP_WRITE_ZEROES 9 write the zero filled sector many times
REQ_OP_ZONE_OPEN 10 Open a zone
REQ_OP_ZONE_CLOSE 11 Close a zone
REQ_OP_ZONE_FINISH 12 Transition a zone to full
REQ_OP_ZONE_APPEND 13 write data at the current zone write pointer
REQ_OP_ZONE_RESET 15 reset a zone write pointer
REQ_OP_ZONE_RESET_ALL 17 reset all the zone present on the device
REQ_OP_DRV_IN 34 Driver private requests
REQ_OP_DRV_OUT 35 Driver private requests

今回のケースでは、ファイルへの追記書き込みのみとなっているので、bi_opfには1 (REQ_OP_WRITE)が設定されている。

また、writepages関数とwrite_inode関数でそれぞれsubmit_bio関数が呼び出されるため、双方の処理を順番に追っていく。

writepages関数から呼ばれるsubmit_bio関数

writepages関数から呼ばれるsubmit_bio関数前の状態

submit_bio関数

submit_bio関数は、bi_opfのフラグよりI/Oスケジューラにリクエストとして追加する。
submit_bio関数は下記の通りとなっている。

// 1057:
blk_qc_t submit_bio(struct bio *bio)
{
    if (blkcg_punt_bio_submit(bio))
        return BLK_QC_T_NONE;

    /*
    * If it's a regular read/write or a barrier with data attached,
    * go through the normal accounting stuff before submission.
    */
    if (bio_has_data(bio)) {
        unsigned int count;

        if (unlikely(bio_op(bio) == REQ_OP_WRITE_SAME))
            count = queue_logical_block_size(
                    bio->bi_bdev->bd_disk->queue) >> 9;
        else
            count = bio_sectors(bio);

        if (op_is_write(bio_op(bio))) {
            count_vm_events(PGPGOUT, count);
        } else {
            task_io_account_read(bio->bi_iter.bi_size);
            count_vm_events(PGPGIN, count);
        }
    }

    /*
    * If we're reading data that is part of the userspace workingset, count
    * submission time as memory stall.  When the device is congested, or
    * the submitting cgroup IO-throttled, submission can be a significant
    * part of overall IO time.
    */
    if (unlikely(bio_op(bio) == REQ_OP_READ &&
        bio_flagged(bio, BIO_WORKINGSET))) {
        unsigned long pflags;
        blk_qc_t ret;

        psi_memstall_enter(&pflags);
        ret = submit_bio_noacct(bio);
        psi_memstall_leave(&pflags);

        return ret;
    }

    return submit_bio_noacct(bio);
}

submit_bio関数では、blkcg_punt_bio_submit関数でcgroup処理に加えてsubmit_bio_noacct関数を呼び出す。

本環境では、CONFIG_BLK_CGROUP=nなので、blkcg_punt_bio_submit関数はfalseを返すインライン関数となっている。

  • REQ_OP_WRITE: vm_event_states.event[PGPGOUT]countだけ増加させる。
  • REQ_OP_WRITE_SAME: countを調整したうえで、REQ_OP_WRITEと同様に実施する。
  • REQ_OP_READ: プロセス毎のIO統計情報/proc/<pid>/ioread_bytesの増加、vm_event_states.event[PGPGIN]countだけ増加させる。

bio_has_data関数では、submit予定のbioにデータがあることを確認する。

// 61:
static inline bool bio_has_data(struct bio *bio)
{
    if (bio &&
        bio->bi_iter.bi_size &&
        bio_op(bio) != REQ_OP_DISCARD &&
        bio_op(bio) != REQ_OP_SECURE_ERASE &&
        bio_op(bio) != REQ_OP_WRITE_ZEROES)
        return true;

    return false;
}

基本的には、bioのサイズでデータの有無を確認することができるが、DISCARDSECURE_ERASEといった操作はデータを必要としないのでfalseを返す。

直後に、REQ_OP_WRITE_SAMEとの比較する処理があるが、Linuxでは0埋めにしか使われておらずLinux 5.18で削除されている。

listman.redhat.com

そのため、今回はelse文で実行されるbio_sectorsを確認する。

// 49:
#define bio_sectors(bio)    bvec_iter_sectors((bio)->bi_iter)

bvec_iter_sectorsbio構造体のbi_size (ブロックのサイズ)から 512で割った値(セクタ数)を返す。

// 46:
#define bvec_iter_sectors(iter) ((iter).bi_size >> 9)

また、op_is_write関数でwrite操作であるかどうかを判定し、

// 444:
static inline bool op_is_write(unsigned int op)
{
    return (op & 1);
}

writeの場合はPGPGOUT、readの場合はPGPGINの値を更新する。

writeの操作の場合では、submit_bio_noacc関数を呼び出す。

submit_bio_noacct関数

submit_bio_noacct関数は、__submit_bio_noacct関数のラッパ関数となっている。

submit_bio_noacct関数の定義は次のようになっている。

// 1025:
blk_qc_t submit_bio_noacct(struct bio *bio)
{
    /*
    * We only want one ->submit_bio to be active at a time, else stack
    * usage with stacked devices could be a problem.  Use current->bio_list
    * to collect a list of requests submited by a ->submit_bio method while
    * it is active, and then process them after it returned.
    */
    if (current->bio_list) {
        bio_list_add(&current->bio_list[0], bio);
        return BLK_QC_T_NONE;
    }

    if (!bio->bi_bdev->bd_disk->fops->submit_bio)
        return __submit_bio_noacct_mq(bio);
    return __submit_bio_noacct(bio);
}
  • MD(Multi-Device)/DM(Device Mapper)のようなstacked block deviceの場合は、bio_list_add関数によって現在のプロセスが持つbio_listbioを追加する。
  • block deviceがblk-mq対応の場合、__submit_bio_noacct_mq関数を呼び出す。
  • 上記に該当しない場合、__submit_bio_noacct関数を呼び出す。

今回の環境はシンプルな構成(MDやDMなどを使用しない) であり、blk-mq対応ドライバ(下記のblock_device_operationsを参照)となっている。

// 823:
static const struct block_device_operations mmc_bdops = {
    .open           = mmc_blk_open,
    .release        = mmc_blk_release,
    .getgeo         = mmc_blk_getgeo,
    .owner          = THIS_MODULE,
    .ioctl          = mmc_blk_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl       = mmc_blk_compat_ioctl,
#endif
    .alternative_gpt_sector = mmc_blk_alternative_gpt_sector,
};

__submit_bio_noacct_mq関数

__submit_bio_noacct_mq関数は、ブロックデバイスに対する要求 (request) の作成・依頼する bio_mq_submit_bio関数のラッパ関数となっている。

ここで、それぞれのデータ構造の関係性を図示してみる。

request_queueとbioの関係性

bio構造体とrequest_queue構造体は、1パーティションを管理するgendisk構造体によって紐づいている。

さらに、stacked block deviceの対応として、この処理の間はcurrnet->bio_listに処理中のbioを入れておく。

__submit_bio_noacct_mq関数の定義は次の通りとなっている。

// 1001:
static blk_qc_t __submit_bio_noacct_mq(struct bio *bio)
{
    struct bio_list bio_list[2] = { };
    blk_qc_t ret;

    current->bio_list = bio_list;

    do {
        ret = __submit_bio(bio);
    } while ((bio = bio_list_pop(&bio_list[0])));

    current->bio_list = NULL;
    return ret;
}

__submit_bio_noacct_mq関数は、bio_listにある各bioに対して__submit_bio関数を実行する。

__submit_bio関数

__submit_bio関数は、発行するbioを準備したうえでdiskに対応するsubmit_bio操作を実行する。

// 915:
static blk_qc_t __submit_bio(struct bio *bio)
{
    struct gendisk *disk = bio->bi_bdev->bd_disk;
    blk_qc_t ret = BLK_QC_T_NONE;

    if (unlikely(bio_queue_enter(bio) != 0))
        return BLK_QC_T_NONE;

    if (!submit_bio_checks(bio) || !blk_crypto_bio_prep(&bio))
        goto queue_exit;
    if (disk->fops->submit_bio) {
        ret = disk->fops->submit_bio(bio);
        goto queue_exit;
    }
    return blk_mq_submit_bio(bio);

queue_exit:
    blk_queue_exit(disk->queue);
    return ret;
}

__submit_bio関数は、リクエストをブロックデバイスに投げる前に、次のような処理を実行する。

  • リクエストキューのリファレンスカウントを増やす
  • ブロックデバイスが対応している操作かどうか確認する
  • ブロックレイヤでインライン暗号化を使用する

まず初めに、リファレンスカウントを増やすbio_queue_enter関数について確認する。

リファレンスカウントを増やす

bio_queue_enter関数の定義は次のようになっている。

// 471:
static inline int bio_queue_enter(struct bio *bio)
{
    struct gendisk *disk = bio->bi_bdev->bd_disk;
    struct request_queue *q = disk->queue;

    while (!blk_try_enter_queue(q, false)) {
        if (bio->bi_opf & REQ_NOWAIT) {
            if (test_bit(GD_DEAD, &disk->state))
                goto dead;
            bio_wouldblock_error(bio);
            return -EBUSY;
        }

        /*
        * read pair of barrier in blk_freeze_queue_start(), we need to
        * order reading __PERCPU_REF_DEAD flag of .q_usage_counter and
        * reading .mq_freeze_depth or queue dying flag, otherwise the
        * following wait may never return if the two reads are
        * reordered.
        */
        smp_rmb();
        wait_event(q->mq_freeze_wq,
               (!q->mq_freeze_depth &&
                blk_pm_resume_queue(false, q)) ||
               test_bit(GD_DEAD, &disk->state));
        if (test_bit(GD_DEAD, &disk->state))
            goto dead;
    }

    return 0;
dead:
    bio_io_error(bio);
    return -ENODEV;
}

bio_queue_enter関数では、blk_try_enter_queue関数でキューが利用可能であるまでwaitする。
ただし、REQ_NOWAITの場合や複数回ループしてしまった場合には-ENODEVを返す。

blk_try_entry_queue関数の定義は以下の通りとなっている。

// 415:
static bool blk_try_enter_queue(struct request_queue *q, bool pm)
{
    rcu_read_lock();
    if (!percpu_ref_tryget_live(&q->q_usage_counter))
        goto fail;

    /*
    * The code that increments the pm_only counter must ensure that the
    * counter is globally visible before the queue is unfrozen.
    */
    if (blk_queue_pm_only(q) &&
        (!pm || queue_rpm_status(q) == RPM_SUSPENDED))
        goto fail_put;

    rcu_read_unlock();
    return true;

fail_put:
    percpu_ref_put(&q->q_usage_counter);
fail:
    rcu_read_unlock();
    return false;
}
// 284:
static inline bool percpu_ref_tryget_live(struct percpu_ref *ref)
{
    unsigned long __percpu *percpu_count;
    bool ret = false;

    rcu_read_lock();

    if (__ref_is_percpu(ref, &percpu_count)) {
        this_cpu_inc(*percpu_count);
        ret = true;
    } else if (!(ref->percpu_count_ptr & __PERCPU_REF_DEAD)) {
        ret = atomic_long_inc_not_zero(&ref->data->count);
    }

    rcu_read_unlock();

    return ret;
}

詳細は省くが、blk_try_entry_queue関数は、そのキューが使用可能かどうかを確認し、使用可能であればq_usage_counterをインクリメントする。
q_usage_counter0(使用不可能)である場合、whileループを抜ける。

ブロックデバイスが対応しているかどうか確認する

この時、submit_bio_checks関数は、発行するbioが妥当性を確認する。(例えば、「ストレージデバイスが対応していない操作を実行する」など)

submit_bio_checks関数の定義は次の通りとなっている。

// 797:
static noinline_for_stack bool submit_bio_checks(struct bio *bio)
{
    struct block_device *bdev = bio->bi_bdev;
    struct request_queue *q = bdev->bd_disk->queue;
    blk_status_t status = BLK_STS_IOERR;
    struct blk_plug *plug;

    might_sleep();

    plug = blk_mq_plug(q, bio);
    if (plug && plug->nowait)
        bio->bi_opf |= REQ_NOWAIT;

    /*
    * For a REQ_NOWAIT based request, return -EOPNOTSUPP
    * if queue does not support NOWAIT.
    */
    if ((bio->bi_opf & REQ_NOWAIT) && !blk_queue_nowait(q))
        goto not_supported;

    if (should_fail_bio(bio))
        goto end_io;
    if (unlikely(bio_check_ro(bio)))
        goto end_io;
    if (!bio_flagged(bio, BIO_REMAPPED)) {
        if (unlikely(bio_check_eod(bio)))
            goto end_io;
        if (bdev->bd_partno && unlikely(blk_partition_remap(bio)))
            goto end_io;
    }

    /*
    * Filter flush bio's early so that bio based drivers without flush
    * support don't have to worry about them.
    */
    if (op_is_flush(bio->bi_opf) &&
        !test_bit(QUEUE_FLAG_WC, &q->queue_flags)) {
        bio->bi_opf &= ~(REQ_PREFLUSH | REQ_FUA);
        if (!bio_sectors(bio)) {
            status = BLK_STS_OK;
            goto end_io;
        }
    }

    if (!test_bit(QUEUE_FLAG_POLL, &q->queue_flags))
        bio_clear_hipri(bio);

    switch (bio_op(bio)) {
    case REQ_OP_DISCARD:
        if (!blk_queue_discard(q))
            goto not_supported;
        break;
    case REQ_OP_SECURE_ERASE:
        if (!blk_queue_secure_erase(q))
            goto not_supported;
        break;
    case REQ_OP_WRITE_SAME:
        if (!q->limits.max_write_same_sectors)
            goto not_supported;
        break;
    case REQ_OP_ZONE_APPEND:
        status = blk_check_zone_append(q, bio);
        if (status != BLK_STS_OK)
            goto end_io;
        break;
    case REQ_OP_ZONE_RESET:
    case REQ_OP_ZONE_OPEN:
    case REQ_OP_ZONE_CLOSE:
    case REQ_OP_ZONE_FINISH:
        if (!blk_queue_is_zoned(q))
            goto not_supported;
        break;
    case REQ_OP_ZONE_RESET_ALL:
        if (!blk_queue_is_zoned(q) || !blk_queue_zone_resetall(q))
            goto not_supported;
        break;
    case REQ_OP_WRITE_ZEROES:
        if (!q->limits.max_write_zeroes_sectors)
            goto not_supported;
        break;
    default:
        break;
    }

    /*
    * Various block parts want %current->io_context, so allocate it up
    * front rather than dealing with lots of pain to allocate it only
    * where needed. This may fail and the block layer knows how to live
    * with it.
    */
    if (unlikely(!current->io_context))
        create_task_io_context(current, GFP_ATOMIC, q->node);

    if (blk_throtl_bio(bio)) {
        blkcg_bio_issue_init(bio);
        return false;
    }

    blk_cgroup_bio_start(bio);
    blkcg_bio_issue_init(bio);

    if (!bio_flagged(bio, BIO_TRACE_COMPLETION)) {
        trace_block_bio_queue(bio);
        /* Now that enqueuing has been traced, we need to trace
        * completion as well.
        */
        bio_set_flag(bio, BIO_TRACE_COMPLETION);
    }
    return true;

not_supported:
    status = BLK_STS_NOTSUPP;
end_io:
    bio->bi_status = status;
    bio_endio(bio);
    return false;
}

submit_bio_checks関数では以下のような状態を確認する。

ブロックレイヤのインライン暗号化

ブロックレイヤのインライン暗号化は、Linux v5.8から導入されたカーネルの機能の一つとなっている。

docs.kernel.org

// 176:
config BLK_INLINE_ENCRYPTION
    bool "Enable inline encryption support in block layer"
    help
      Build the blk-crypto subsystem. Enabling this lets the
      block layer handle encryption, so users can take
      advantage of inline encryption hardware if present.

CONFIG_BLK_INLINE_ENCRYPTION=yの場合には、インライン暗号化の準備するために__blk_crypto_bio_prep関数を呼び出す。

// 122:
bool __blk_crypto_bio_prep(struct bio **bio_ptr);
static inline bool blk_crypto_bio_prep(struct bio **bio_ptr)
{
    if (bio_has_crypt_ctx(*bio_ptr))
        return __blk_crypto_bio_prep(bio_ptr);
    return true;
}

今回の場合では、CONFIG_BLK_INLINE_ENCRYPTION=nであるため、__blk_crypto_bio_prep関数は常にfalseを返す。

write_inode関数から呼ばれるsubmit_bio関数

write_inode関数から呼ばれるsubmit_bio関数前の状態

submit_bio関数

writepages関数から呼ばれた場合と同じのため割愛する。

submit_bio_noacct関数

writepages関数から呼ばれた場合と同じのため割愛する。

__submit_bio_noacct_mq関数

request_queueとbioの関係性 (write_inode)

writepages関数から呼ばれた場合と同じのため割愛する。

__submit_bio関数

writepages関数から呼ばれた場合と同じのため割愛する。

おわりに

本記事では、ブロックレイヤのsubmit_bio関数を確認した。

変更履歴

  • 2022/09/25: 記事公開

参考

KUnitフレームワークによるLinuxカーネルのテスト実施方法

概要

本記事では、kunit_toolを使用して、既存のKUnitテストをQEMU(armアーキテクチャ)で実行する手順について確認する。

はじめに

KUnitは、Linuxカーネル用のテストフレームワークである。
Linuxカーネル v5.5から導入されたスクリプトtools/testing/kunit.pyを実行することで、テスト実行・結果解析することができる。
このスクリプトを利用することで、User Mode Linux (UML) やQEMUでテストを実行することができる。

KUnitでは、内部ライブラリ (include/linux/list.hなど) といったユーザ空間からテストすることができる。 また、アサーションの作成やsetup/clean-upなどのテストを簡単に書くためのライブラリを提供している。

実験環境

本記事で使用した開発用PC (Host PC)の構成は次の通りとなっている。

環境 概要
CPU AMD Ryzen 3 3300X
RAM DDR4-2666 16GB ×2
Host OS Ubuntu Desktop 22.04
Target kernel v5.15.37
QEMU 6.2.0 (Debian 1:6.2-+dfsg-2ubuntu6)

準備

LinuxのソースツリーがDirtyの場合、次のエラーが発生する。あらかじめmake mrproperを実施しておくこと。

leava@kbuild:~/workspace/linux$ ./tools/testing/kunit/kunit.py run --arch=arm --cross_compile=arm-linux-gnueabihf- --jobs=12 --qemu_config=./tools/testing/kunit/qemu_configs/arm.py 
[18:35:11] Configuring KUnit Kernel ...
Generating .config ...
Populating config with:
$ make ARCH=arm olddefconfig CROSS_COMPILE=arm-linux-gnueabihf- O=.kunit
ERROR:root:make[1]: Entering directory '/home/leava/workspace/linux/.kunit'
***
*** The source tree is not clean, please run 'make ARCH=arm mrproper'
*** in /home/leava/workspace/linux
***
make[1]: *** [/home/leava/workspace/linux/Makefile:570: outputmakefile] Error 1
make[1]: Leaving directory '/home/leava/workspace/linux/.kunit'
make: *** [Makefile:219: __sub-make] Error 2

また、KUnitを実行するためには、次のカーネルコンフィグが必要となる。 

CONFIG_KUNIT=y
CONFIG_MSDOS_FS=y
CONFIG_FAT_KUNIT_TEST=y

KUnitでは、Linuxカーネルのコンフィグファイル.configとは別のファイル.kunit/.kunitconfigでもカーネルコンフィグを管理することができる。

コンフィグファイルの生成

kunit_toolのconfigを指定することでKUnitに必要な生成される。

leava@kbuild:~/workspace/linux$ ./tools/testing/kunit/kunit.py config
[22:41:14] Configuring KUnit Kernel ...
Generating .config ...
Populating config with:
$ make ARCH=um olddefconfig O=.kunit
[22:41:16] Elapsed time: 2.409s

leava@kbuild:~/workspace/linux$ ls -lA .kunit/
total 48
-rw-r--r-- 1 leava root 22976 May  8 22:41 .config
-rw-r--r-- 1 leava root    68 May  8 22:41 .config.old
-rw-r--r-- 1 leava root    39 May  8 22:41 .gitignore
-rw-r--r-- 1 leava root    68 May  8 22:41 .kunitconfig
-rw-r--r-- 1 leava root    73 May  8 22:41 Makefile
drwxr-xr-x 4 leava root  4096 May  8 22:41 include
drwxr-xr-x 4 leava root  4096 May  8 22:41 scripts
lrwxrwxrwx 1 leava root     2 May  8 22:41 source -> ..

leava@kbuild:~/workspace/linux$ cat .kunit/.kunitconfig 
CONFIG_KUNIT=y
CONFIG_KUNIT_EXAMPLE_TEST=y
CONFIG_KUNIT_ALL_TESTS=y

カーネルのビルド

kunit_toolのbuildを指定することでKUnitで実行するカーネルをビルドする。 このとき、--arch--cross_compileオプションを指定することでクロスコンパイルすることができる。

leava@kbuild:~/workspace/linux$ ./tools/testing/kunit/kunit.py build --arch=arm --cross_compile=arm-linux
[23:09:22] Building KUnit Kernel ...
Populating config with:
$ make ARCH=arm olddefconfig CROSS_COMPILE=arm-linux-gnueabihf- O=.kunit
Building with:
$ make ARCH=arm --jobs=12 CROSS_COMPILE=arm-linux-gnueabihf- O=.kunit
[23:09:57] Elapsed time: 35.098s

leava@kbuild:~/workspace/linux$ ls -la .kunit/
total 19400
drwxr-xr-x 19 leava root    4096 May  8 23:09 .
drwxrwxr-x 26 leava leava    4096 May  8 22:41 ..
-rw-r--r--  1 leava root   37237 May  8 23:09 .config
-rw-r--r--  1 leava root   22976 May  8 22:41 .config.old
-rw-r--r--  1 leava root      39 May  8 22:41 .gitignore
-rw-r--r--  1 leava root      68 May  8 22:41 .kunitconfig
-rw-r--r--  1 leava root     633 May  8 23:09 .missing-syscalls.d
-rw-r--r--  1 leava root  448911 May  8 23:09 .tmp_System.map
-rwxr-xr-x  1 leava root 4125012 May  8 23:09 .tmp_vmlinux.kallsyms1
-rw-r--r--  1 leava root  959465 May  8 23:09 .tmp_vmlinux.kallsyms1.S
-rw-r--r--  1 leava root  165852 May  8 23:09 .tmp_vmlinux.kallsyms1.o
-rwxr-xr-x  1 leava root 4256384 May  8 23:09 .tmp_vmlinux.kallsyms2
-rw-r--r--  1 leava root  959465 May  8 23:09 .tmp_vmlinux.kallsyms2.S
-rw-r--r--  1 leava root  165852 May  8 23:09 .tmp_vmlinux.kallsyms2.o
-rw-r--r--  1 leava root       2 May  8 23:09 .version
-rw-r--r--  1 leava root    1172 May  8 23:09 .vmlinux.cmd
-rw-r--r--  1 leava root      73 May  8 23:09 Makefile
-rw-r--r--  1 leava root  448911 May  8 23:09 System.map
drwxr-xr-x  5 leava root    4096 May  8 23:09 arch
drwxr-xr-x  3 leava root    4096 May  8 23:09 block
drwxr-xr-x  2 leava root    4096 May  8 23:09 certs
drwxr-xr-x  2 leava root    4096 May  8 23:09 crypto
drwxr-xr-x 44 leava root    4096 May  8 23:09 drivers
drwxr-xr-x 11 leava root    4096 May  8 23:09 fs
drwxr-xr-x  4 leava root    4096 May  8 22:41 include
drwxr-xr-x  2 leava root    4096 May  8 23:09 init
drwxr-xr-x  2 leava root    4096 May  8 23:09 ipc
drwxr-xr-x 12 leava root    4096 May  8 23:09 kernel
drwxr-xr-x  5 leava root   12288 May  8 23:09 lib
drwxr-xr-x  2 leava root    4096 May  8 23:09 mm
-rw-r--r--  1 leava root    1378 May  8 23:09 modules.builtin
-rw-r--r--  1 leava root    8994 May  8 23:09 modules.builtin.modinfo
drwxr-xr-x  6 leava root    4096 May  8 23:09 scripts
drwxr-xr-x  2 leava root    4096 May  8 23:09 security
drwxr-xr-x  2 leava root    4096 May  8 23:09 sound
lrwxrwxrwx  1 leava root       2 May  8 23:09 source -> ..
drwxr-xr-x  2 leava root    4096 May  8 23:09 usr
drwxr-xr-x  3 leava root    4096 May  8 23:09 virt
-rwxr-xr-x  1 leava root 4256384 May  8 23:09 vmlinux
-rw-r--r--  1 leava root 4678424 May  8 23:09 vmlinux.o
-rw-r--r--  1 leava root       0 May  8 23:09 vmlinux.symvers    

テスト実行

kunit_toolのrunを指定することでKUnitで実行するカーネルをビルドする。 このとき、--arch--cross_compileオプションを指定することで起動するカーネルアーキテクチャを指定することができる。

jobsオプションを指定することで、makeコマンド実行を並列処理させることができる。

また、デフォルトはUMLでの起動となっているが、QEMUで起動する場合には--qemu_configオプションを指定する。 パラメータとして指定するファイルには、QEMUで実行する場合に必要となるパラメータ (qemuのパラメータや追加のカーネルコンフィグなど)を指定することができる。

leava@kbuild:~/workspace/linux$ ./tools/testing/kunit/kunit.py run \        
                                --arch=arm \
                                --cross_compile=arm-linux-gnueabihf- \
                                --jobs=12 \
                                --timeout=300 \
                                --qemu_config=./tools/testing/kunit/qemu_configs/arm.py

上記のコマンドを実行すると、次のような結果が得られる。

テスト実施時の様子

おわりに

本稿では、既存のKUnitのテストをQEMU (armアーキテクチャ) で実行する手順を確認した。

また、KUnitは公式ドキュメントが充実しており、ドキュメントを一読するだけでも簡単なテストを作成することができると思われる。 そのため、興味のある人はカーネルドキュメントを参考に一度KUnitを利用してみることをお勧めする。

変更履歴

  • 2022/5/9: 記事公開

参考文献

Linuxカーネルのファイルアクセスの処理を追いかける (11) ext2: write_inode

関連記事

概要

QEMUの vexpress-a9 (arm) で Linux 5.15を起動させながら、ファイル書き込みのカーネル処理を確認していく。

本章では、ext2_write_inode関数を確認した。

はじめに

ユーザプロセスはファイルシステムという機構によって記憶装置上のデータをファイルという形式で書き込み・読み込みすることができる。
本調査では、ユーザプロセスがファイルに書き込み要求を実行したときにLinuxカーネルではどのような処理が実行されるかを読み解いていく。

調査対象や環境などはPart 1: 環境セットアップを参照。

調査対象 (シーケンス図)

本記事では、writebackカーネルスレッドがwrite_inodeを呼び出すところを確認する。

write_inodeの概要

kerner thread のライトバック処理によって、Dirtyのinodeに対してwritepages関数とwrite_inode関数を呼び出す。

基本的には、writepages関数はファイルの実データの書き込み、write_inode関数はファイルのメタデータの書き込みをする。
ext2ファイルシステムの場合では、ext2_writepages関数とext2_write_inode関数が定義されている。

ext2_writepages関数の定義は次の通りとなっている。

// 1639:
int ext2_write_inode(struct inode *inode, struct writeback_control *wbc)
{
    return __ext2_write_inode(inode, wbc->sync_mode == WB_SYNC_ALL);
}

ext2_write_inode関数は、__ext2_write_inode関数を呼び出す。

// 1635:
static int __ext2_write_inode(struct inode *inode, int do_sync)
{
    struct ext2_inode_info *ei = EXT2_I(inode);
    struct super_block *sb = inode->i_sb;
    ino_t ino = inode->i_ino;
    uid_t uid = i_uid_read(inode);
    gid_t gid = i_gid_read(inode);
    struct buffer_head * bh;
    struct ext2_inode * raw_inode = ext2_get_inode(sb, ino, &bh);
    int n;
    int err = 0;

    if (IS_ERR(raw_inode))
        return -EIO;

    /* For fields not not tracking in the in-memory inode,
    * initialise them to zero for new inodes. */
    if (ei->i_state & EXT2_STATE_NEW)
        memset(raw_inode, 0, EXT2_SB(sb)->s_inode_size);

    raw_inode->i_mode = cpu_to_le16(inode->i_mode);
    if (!(test_opt(sb, NO_UID32))) {
        raw_inode->i_uid_low = cpu_to_le16(low_16_bits(uid));
        raw_inode->i_gid_low = cpu_to_le16(low_16_bits(gid));
/*
 * Fix up interoperability with old kernels. Otherwise, old inodes get
 * re-used with the upper 16 bits of the uid/gid intact
 */
        if (!ei->i_dtime) {
            raw_inode->i_uid_high = cpu_to_le16(high_16_bits(uid));
            raw_inode->i_gid_high = cpu_to_le16(high_16_bits(gid));
        } else {
            raw_inode->i_uid_high = 0;
            raw_inode->i_gid_high = 0;
        }
    } else {
        raw_inode->i_uid_low = cpu_to_le16(fs_high2lowuid(uid));
        raw_inode->i_gid_low = cpu_to_le16(fs_high2lowgid(gid));
        raw_inode->i_uid_high = 0;
        raw_inode->i_gid_high = 0;
    }
    raw_inode->i_links_count = cpu_to_le16(inode->i_nlink);
    raw_inode->i_size = cpu_to_le32(inode->i_size);
    raw_inode->i_atime = cpu_to_le32(inode->i_atime.tv_sec);
    raw_inode->i_ctime = cpu_to_le32(inode->i_ctime.tv_sec);
    raw_inode->i_mtime = cpu_to_le32(inode->i_mtime.tv_sec);

    raw_inode->i_blocks = cpu_to_le32(inode->i_blocks);
    raw_inode->i_dtime = cpu_to_le32(ei->i_dtime);
    raw_inode->i_flags = cpu_to_le32(ei->i_flags);
    raw_inode->i_faddr = cpu_to_le32(ei->i_faddr);
    raw_inode->i_frag = ei->i_frag_no;
    raw_inode->i_fsize = ei->i_frag_size;
    raw_inode->i_file_acl = cpu_to_le32(ei->i_file_acl);
    if (!S_ISREG(inode->i_mode))
        raw_inode->i_dir_acl = cpu_to_le32(ei->i_dir_acl);
    else {
        raw_inode->i_size_high = cpu_to_le32(inode->i_size >> 32);
        if (inode->i_size > 0x7fffffffULL) {
            if (!EXT2_HAS_RO_COMPAT_FEATURE(sb,
                    EXT2_FEATURE_RO_COMPAT_LARGE_FILE) ||
                EXT2_SB(sb)->s_es->s_rev_level ==
                    cpu_to_le32(EXT2_GOOD_OLD_REV)) {
                   /* If this is the first large file
               * created, add a flag to the superblock.
               */
                spin_lock(&EXT2_SB(sb)->s_lock);
                ext2_update_dynamic_rev(sb);
                EXT2_SET_RO_COMPAT_FEATURE(sb,
                    EXT2_FEATURE_RO_COMPAT_LARGE_FILE);
                spin_unlock(&EXT2_SB(sb)->s_lock);
                ext2_sync_super(sb, EXT2_SB(sb)->s_es, 1);
            }
        }
    }
    
    raw_inode->i_generation = cpu_to_le32(inode->i_generation);
    if (S_ISCHR(inode->i_mode) || S_ISBLK(inode->i_mode)) {
        if (old_valid_dev(inode->i_rdev)) {
            raw_inode->i_block[0] =
                cpu_to_le32(old_encode_dev(inode->i_rdev));
            raw_inode->i_block[1] = 0;
        } else {
            raw_inode->i_block[0] = 0;
            raw_inode->i_block[1] =
                cpu_to_le32(new_encode_dev(inode->i_rdev));
            raw_inode->i_block[2] = 0;
        }
    } else for (n = 0; n < EXT2_N_BLOCKS; n++)
        raw_inode->i_block[n] = ei->i_data[n];
    mark_buffer_dirty(bh);
    if (do_sync) {
        sync_dirty_buffer(bh);
        if (buffer_req(bh) && !buffer_uptodate(bh)) {
            printk ("IO error syncing ext2 inode [%s:%08lx]\n",
                sb->s_id, (unsigned long) ino);
            err = -EIO;
        }
    }
    ei->i_state &= ~EXT2_STATE_NEW;
    brelse (bh);
    return err;
}

ext2ファイルシステムにおいてinodeは、struct ext2_inodeデータ構造によって管理している。
struct ext2_inodeは、inode型のメンバvfs_inodeがあり、VFSレイヤから渡されたデータはここに格納されている。

EXT2_Iマクロとinode構造体

__ext2_write_inode関数は、書き込み対象のinodeをstruct ex2_inodeデータ構造を介して書き込みを実施する。
そのため、struct ext2_inode構造体はストレージ上におけるinodeの構造と一致している。

struct ext2_inode構造体は次のメンバを持つデータ構造である。

メンバ 概要
__le16 i_mode ファイルのモード
__le16 i_uid ユーザID
__le32 i_size; ファイルサイズ(バイト)
__le32 i_atime 最終ファイルアクセス時間
__le32 i_ctime 最終ファイル作成時間
__le32 i_mtime 最終ファイル修正時間
__le32 i_dtime ファイル削除時間
__le16 i_gid グループID
__le16 i_links_count ハードリンク数
__le32 i_blocks データブロック数
__le32 i_flags ファイルのフラグ
__le32 osd1 OS依存情報1
__le32 i_block[EXT2_N_BLOCKS] データブロックの番号
__le32 i_generation NFS用のファイルバージョン
__le32 i_file_acl ファイル用ACL
__le32 i_dir_acl ディレクトリ用ACI
__le32 i_faddr フラグメントのアドレス
__le32 osd2 OS依存情報2

ext2ファイルシステムでは、ext2_get_inode関数でstruct ext2_inodeデータ構造を取得する。

// 1328:
static struct ext2_inode *ext2_get_inode(struct super_block *sb, ino_t ino,
                    struct buffer_head **p)
{
    struct buffer_head * bh;
    unsigned long block_group;
    unsigned long block;
    unsigned long offset;
    struct ext2_group_desc * gdp;

    *p = NULL;
    if ((ino != EXT2_ROOT_INO && ino < EXT2_FIRST_INO(sb)) ||
        ino > le32_to_cpu(EXT2_SB(sb)->s_es->s_inodes_count))
        goto Einval;

    block_group = (ino - 1) / EXT2_INODES_PER_GROUP(sb);
    gdp = ext2_get_group_desc(sb, block_group, NULL);
    if (!gdp)
        goto Egdp;
    /*
    * Figure out the offset within the block group inode table
    */
    offset = ((ino - 1) % EXT2_INODES_PER_GROUP(sb)) * EXT2_INODE_SIZE(sb);
    block = le32_to_cpu(gdp->bg_inode_table) +
        (offset >> EXT2_BLOCK_SIZE_BITS(sb));
    if (!(bh = sb_bread(sb, block)))
        goto Eio;

    *p = bh;
    offset &= (EXT2_BLOCK_SIZE(sb) - 1);
    return (struct ext2_inode *) (bh->b_data + offset);

Einval:
    ext2_error(sb, "ext2_get_inode", "bad inode number: %lu",
           (unsigned long) ino);
    return ERR_PTR(-EINVAL);
Eio:
    ext2_error(sb, "ext2_get_inode",
           "unable to read inode block - inode=%lu, block=%lu",
           (unsigned long) ino, block);
Egdp:
    return ERR_PTR(-EIO);
}

ブロックグループディスクリプタの探索

ext2_get_group_desc関数では、渡されたinode番号inoを基にそのinodeが格納されているグループディスクリプタ block_group を探索する。
ext2ファイルシステムでは、super blockとblock group descriptorはBlock Group #0に保存され、他のBlock Groupには複製が保存される。

ext2ファイルシステムとブロックグループディスクリプタ

グループディスクリプタ struct ext2_group_descデータ構造は次のメンバを持つ。

メンバ 概要
__le32 bg_block_bitmap Data Block Bitmapのブロック番号
__le32 bg_inode_bitmap inode Bitmapのブロック番号
__le32 bg_inode_table 最初のinode tableがあるブロックのブロック番号
__le16 bg_free_blocks_count ブロックグループ内の空きブロック数
__le16 bg_free_inodes_count ブロックグループ内の空きinode数
__le16 bg_used_dirs_count ブロックグループ内のディレクトリ数
__le16 bg_pad 4バイト境界のアライン
__le32 bg_reserved[3] 予約領域

inode tableの探索

グループディスクリプタにあるinode tableの先頭ブロック番号bg_inode_tableとinode番号から、対象のブロックを検索する。

ext2ファイルシステムとiノードテーブル

得られたブロック番号から、sb_bread関数によってメモリ上にロードする。(sb_bread関数については下記参照)

qiita.com

その結果、次のようなデータ構造となる。

sb_bread関数を実施した後のバッファ

bufferの書き出し

__ext2_write_inode関数は、ext2_get_inode関数によってストレージからロードしたext2_inodeデータ構造体をinodeを基に更新し、ストレージに書き出しする。
その後、更新したinodeが含まれているbhに対して、mark_buffer_dirty関数(と必要に応じてsync_dirty_buffer関数)を呼び出す。

mark_buffer_dirty関数では、buffer_headpageにDirtyフラグをセットとDirtyリストに追加する。

mark_buffer_dirty関数の実行結果

また、inodeのexpireを契機としている場合は、do_syncには0が設定されるため、sync_dirty_buffer関数は呼ばれない。

sb_bread関数では、def_blk_aops操作群が設定される。

// 377:
const struct address_space_operations def_blk_aops = {
    .set_page_dirty = __set_page_dirty_buffers,
    .readpage   = blkdev_readpage,
    .readahead  = blkdev_readahead,
    .writepage  = blkdev_writepage,
    .write_begin    = blkdev_write_begin,
    .write_end  = blkdev_write_end,
    .writepages = blkdev_writepages,
    .direct_IO  = blkdev_direct_IO,
    .migratepage    = buffer_migrate_page_norefs,
    .is_dirty_writeback = buffer_check_dirty_writeback,
};

writebackのシーケンスに関しては、ext2と同様なので省略する。

// 371:
static int blkdev_writepages(struct address_space *mapping,
                 struct writeback_control *wbc)
{
    return generic_writepages(mapping, wbc);
}

blkdev_writepages関数は、generic_writepages関数を呼び出す。

// 2335:
int generic_writepages(struct address_space *mapping,
               struct writeback_control *wbc)
{
    struct blk_plug plug;
    int ret;

    /* deal with chardevs and other special file */
    if (!mapping->a_ops->writepage)
        return 0;

    blk_start_plug(&plug);
    ret = write_cache_pages(mapping, wbc, __writepage, mapping);
    blk_finish_plug(&plug);
    return ret;
}

write_cache_pages関数を呼ぶ流れについても、ext2_writepages関数と同様であるので省略する。

おわりに

本記事では、ext2ファイルシステムwrite_inode操作 (ext2_write_inode)を解説した。
今回の環境でext2_write_inode関数を実行することで、次のwriteback kthreadの起床タイミング (同条件) で 次のようなbio構造体が生成された。

ext2_write_inode関数の結果

変更履歴

  • 2022/05/06: 記事公開
  • 2022/06/13: 動作後のイメージ図の追加
  • 2022/09/19: カーネルバージョンを5.15に変更

kcovによるカーネルのソースコードカバレッジの分析

概要

kcovは、カーネルソースコードカバレッジを測定するツールである。
本記事では、kcovに関係する次の二つの内容が含まれている。

はじめに

ソフトウェアのコードカバレッジを計測することは、プログラムの品質を確認するための要素の一つになる。 Linuxカーネルのような規模が大きいプログラムとなると、簡単に定量的な結果を残せるコードカバレッジは有用であると考えられる。

kcovは、Linux v4.6から導入されたファジングのカーネルコードカバレッジ機能である。 同様のツールとしてgcovもあるが、カーネルソースコードカバレッジを計測する場合には「コードブロックが膨大である」「他プロセス(スレッド)が常に動作している」という点で、kcovが優位性がある。

そこで、本記事ではkcovのドキュメントに沿ってソースコードカバレッジを計測してみる。 また、内部でkcovを利用するファジングツールsyzkallerの実行結果も確認してみる。

実験環境

本記事で使用した開発用PC (Host PC)の構成は次の通りとなっている。

環境 概要
CPU AMD Ryzen 3 3300X
RAM DDR4-2666 16GB ×2
OS Ubuntu Desktop 20.04.04
kernel 5.13.0-40-generic
QEMU 4.2.1 (Debian 1:4.2-3ubuntu6.21)

使用方法

kcovのインターフェースがdebugfsを経由したIOCTLによるものとなっている。

次のカーネルコンフィグにより、debugfs直下にkcovがインターフェースとして生成される。

CONFIG_KCOV=y

kcovは、次のIOCTLの操作を提供している。

API group value len 概要
KCOV_INIT_TRACE 'c' 1 unsigned long kcovの初期セットアップ
KCOV_ENABLE 'c' 100 現在のプロセスから発行されたsyscallに対するカバレッジの取得を開始する
KCOV_DISABLE 'c' 101 カバレッジの所得を終了する
KCOV_REMOTE_ENABLE 'c' 102 カーネルコードの任意の部分に対するカバレッジを取得を開始する

kcovによるコードカバレッジの確認

Host OS(x86_64)上に、QEMUによるGuest OS(armhf)にkcovを実施する環境を構築する。

kcovの実施に利用した環境

このとき、Guest OSは次のコマンドにより生成した。

qemu-system-arm 
    -M vexpress-a9 \
    -smp 1 \
    -m 1024 \
    -kernel ${KIMAGE_DIR}/zImage \
    -dtb ${KIMAGE_DIR}/dts/vexpress-v2p-ca9.dtb \
    -drive file=${SD_IMAGE},if=sd,format=raw \
    -append "rootwait root=/dev/nfs console=ttyAMA0 ip=on rw" \
    -net nic,model=lan9118 -net user \
    -nographic

また、NFSroot先は Ubuntu Base 20.04.4 LTS (armhf) を基に、Host PC上に格納してあり、binutilsパッケージを追加している。

測定プログラムは公式ドキュメントにあるものをそのまま利用する。

www.kernel.org

測定プログラムcoverage.cはread(-1, NULL, 0);(46行目)を実行したときのカバレッジを測定する。

// 1:
#include <stdio.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/types.h>

#define KCOV_INIT_TRACE                     _IOR('c', 1, unsigned long)
#define KCOV_ENABLE                 _IO('c', 100)
#define KCOV_DISABLE                        _IO('c', 101)
#define COVER_SIZE                  (64<<10)

#define KCOV_TRACE_PC  0
#define KCOV_TRACE_CMP 1

int main(int argc, char **argv)
{
    int fd;
    unsigned long *cover, n, i;

    /* A single fd descriptor allows coverage collection on a single
     * thread.
     */
    fd = open("/sys/kernel/debug/kcov", O_RDWR);
    if (fd == -1)
            perror("open"), exit(1);
    /* Setup trace mode and trace size. */
    if (ioctl(fd, KCOV_INIT_TRACE, COVER_SIZE))
            perror("ioctl"), exit(1);
    /* Mmap buffer shared between kernel- and user-space. */
    cover = (unsigned long*)mmap(NULL, COVER_SIZE * sizeof(unsigned long),
                                 PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if ((void*)cover == MAP_FAILED)
            perror("mmap"), exit(1);
    /* Enable coverage collection on the current thread. */
    if (ioctl(fd, KCOV_ENABLE, KCOV_TRACE_PC))
            perror("ioctl"), exit(1);
    /* Reset coverage from the tail of the ioctl() call. */
    __atomic_store_n(&cover[0], 0, __ATOMIC_RELAXED);
    /* That's the target syscal call. */
    read(-1, NULL, 0);
    /* Read number of PCs collected. */
    n = __atomic_load_n(&cover[0], __ATOMIC_RELAXED);
    for (i = 0; i < n; i++)
            printf("0x%lx\n", cover[i + 1]);
    /* Disable coverage collection for the current thread. After this call
     * coverage can be enabled for a different thread.
     */
    if (ioctl(fd, KCOV_DISABLE, 0))
            perror("ioctl"), exit(1);
    /* Free resources. */
    if (munmap(cover, COVER_SIZE * sizeof(unsigned long)))
            perror("munmap"), exit(1);
    if (close(fd))
            perror("close"), exit(1);
    return 0;
}

その後、測定プログラムをクロスコンパイルしたバイナリcoverageと、Guest OSのカーネルバイナリvmlinuxをGuest OSのrootfsにコピーする。

測定プログラムcoverageではアドレスで出力されるため、addr2lineにパイプすることで解読できるような形で出力させる。

# ./coverage | addr2line -e vmlinux
/home/leava/linux/fs/read_write.c:644
/home/leava/linux/./include/linux/file.h:75
/home/leava/linux/fs/file.c:915
/home/leava/linux/fs/file.c:901
/home/leava/linux/./include/linux/fdtable.h:85
/home/leava/linux/fs/file.c:912
/home/leava/linux/fs/file.c:936
/home/leava/linux/fs/read_write.c:640

出力結果のファイル名と行数は、測定対象のシステムコールを実行したときに実行された行番号となる。

ただし、新しめのLInuxカーネルでは、デフォルトでGCCの最適化オプション(-O2)が指定されているため、コードカバレッジの状態と実際のソースコードが異なることがある。

syzkallerによるコードカバレッジの確認

kcovはあくまでカーネルの一機能であり、ユーザが使うツールとしては利便性の面ではあまり優れていない。 そのため、kcov単体で使うのではなく、kcovを内部で利用しているツールを使うことが好ましい。

syzkallerはGoogleが開発したカーネルのファジングツールの一つとなっている。

github.com

syzkaller自体はGo言語で記述されたプログラムである。 内部でkcovでなどカーネルの機能を利用しているため、ファジングした結果のコードカバレッジも取得することができる。

Host OS(x86_64)上で、QEMUによるGuest OS(arm64)にksyzkallerを実施する。

syzkaller実行環境

公式ドキュメントにあるSetup: Linux host, QEMU vm, arm64 kernelに沿って実施する。

項目 概要 補足
rootfs buildroot 2022.02.1 qemu_aarch64_virt_defconfig をベースとする
kernel Linux v5.15 defconfigをベースとする
toolchain AArch64 FNU/Linux 2021.07 cross compiler
syzkaller dc9e52595336dbe32f9a20f5da9f09cb8172cd21

Go実行環境のインストール

公式サイトの手順通りにHost OSにGo実行環境をインストールする。

go.dev

leava@ubuntu:/work/$ wget https://go.dev/dl/go1.18.1.linux-amd64.tar.gz
leava@ubuntu:/work/$ tar -C /usr/local -xzf go1.18.1.linux-amd64.tar.gz

Goの実行に必要なディレクトリにPATHを通す

leava@ubuntu:/work/$ export PATH=$PATH:/usr/local/go/bin

Goの実行環境が正しくインストールされているか確認する

leava@ubuntu:/work/$ go version
leava@ubuntu:/work/$ go version go1.18.1 linux/amd64

buildrootによるrootfsの生成

buildrootのソースコードを取得する

leava@ubuntu:/work/$ wget https://buildroot.uclibc.org/downloads/buildroot-2022.02.1.tar.gz
leava@ubuntu:/work/$ tar xf buildroot-2022.02.1.tar.gz
leava@ubuntu:/work/$ cd buildroot-2022.02.1

デフォルトのコンフィグを生成する

leava@ubuntu:/work/buildroot-2022.02.1/$ make qemu_aarch64_virt_defconfig

syzkallerに必要なコンフィグを修正する

Target options
    Target Architecture - Aarch64 (little endian)
Toolchain
    Toolchain type (External toolchain)
System Configuration
    [*] Enable root login with password
    (password) Root password
    [*] Run a getty (login prompt) after boot
        (ttyAMA0) TTY port
Target packages
    [*]   Show packages that are also provided by busybox
    Networking applications
        [*] dhcpcd
        [*] iproute2
        [*] openssh
Filesystem images
    [*] ext2/3/4 root filesystem
        ext2/3/4 variant (ext3)
    (60M) exact size
    [*] tar the root filesystem

buildrootでビルドする

leava@ubuntu:/work/buildroot-2022.02.1/$ make

成果物を確認する

leava@ubuntu:/work/buildroot-2022.02.1/$ ls -l output/images
total 50668
-rw-r--r-- 1 blue root 10883584 May  3 23:41 Image
-rw-r--r-- 1 blue root 62914560 May  4 09:12 rootfs.ext2
lrwxrwxrwx 1 blue root       11 May  3 23:41 rootfs.ext3 -> rootfs.ext2
-rw-r--r-- 1 blue root 16640000 May  3 23:41 rootfs.tar
-rwxr-xr-x 1 blue root      486 May  4 09:12 start-qemu.sh

Linux Kernelのビルド

Linuxソースコードを取得する

leava@ubuntu:/work/$ wget https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.15.tar.gz
leava@ubuntu:/work/$ tar xf linux-5.15.tar.gz
leava@ubuntu:/work/$ cd linux-5.15

デフォルトのコンフィグを生成する

leava@ubuntu:/work/linux-5.15/$ ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make defconfig

syzkallerに必要なコンフィグを修正する

CONFIG_DEBUG_INFO=y
CONFIG_DEBUG_INFO_REDUCED=n
CONFIG_KCOV=y
CONFIG_KCOV_INSTRUMENT_ALL=y
CONFIG_KASAN=y
CONFIG_CMDLINE="console=ttyAMA0"
CONFIG_FAULT_INJECTION=y

Linuxをビルドする

leava@ubuntu:/work/linux-5.15/$ ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-make -j$(nproc)

成果物を確認する

leava@ubuntu:/work/linux-5.15/$ ls -la arch/arm64/boot/Image
-rw-r--r-- 1 blue root 82354688 May  4 15:38 arch/arm64/boot/Image

rootfsのカスタマイズ

QEMUで作成したカーネルを起動させる (root:passwordでログインが可能)

leava@ubuntu:/work/$ qemu-system-aarch64 \
  -machine virt \
  -cpu cortex-a57 \
  -nographic -smp 1 \
  -hda /path/to/rootfs.ext3 \
  -kernel /path/to/arch/arm64/boot/Image \
  -append "console=ttyAMA0 root=/dev/vda oops=panic panic_on_warn=1 panic=-1 ftrace_dump_on_oops=orig_cpu debug earlyprintk=serial slub_debug=UZ" \
  -m 2048 \
  -net user,hostfwd=tcp::10023-:22 -net nic    

initスクリプトに次の処理を追加する (/etc/init.d/S50sshd)

ifconfig eth0 up
dhcpcd
mount -t debugfs none /sys/kernel/debug
chmod 777 /sys/kernel/debug/kcov

sshdに次の設定を追加する (/etc/ssh/sshd_config)

PermitRootLogin yes
PubkeyAuthentication yes
PasswordAuthentication yes    

sshdの設定更新のために、Guest OSを再起動する。

Guest OSのために、Host OS上で公開鍵ペアを作成し、Guest OSに送る。

leava@ubuntu:/work/$ ssh-keygen
leava@ubuntu:/work/$ ssh-copy-id -i id_rsa.pub root@localhost -p 10023

Host OSからGuest OSに対して、sshできることを確認できたらGuest OSの電源を落とす。

syzkallerのビルド

ツールチェインの取得

leava@ubuntu:/work/$ wget https://developer.arm.com/-/media/Files/downloads/gnu-a/10.3-2021.07/binrel/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu.tar.xz
leava@ubuntu:/work/$ tar xf gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu.tar.xz

syzkallerのソースコードを取得する

leava@ubuntu:/work/$ git clone https://github.com/google/syzkaller.git
leava@ubuntu:/work/syzkaller/$ git clone https://github.com/google/syzkaller.git

syzkallerをビルドする

leava@ubuntu:/work/syzkaller/$ CC=/work/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-linux-gnu-g++
leava@ubuntu:/work/syzkaller/$ make TARGETARCH=arm64

syzkallerを実行する

syzkallerの設定ファイル (my.cfg) を用意する。

// 1:
{
        "name": "QEMU-aarch64",
        "target": "linux/arm64",
        "http": ":56700",
        "workdir": "/work/syzkaller/workdir",
        "kernel_obj": "/work/linux-5.15",
        "image": "/work/buildroot-2022.02.1/output/images/rootfs.ext3",
        "sshkey": "~/.ssh/id_rsa",
        "syzkaller": "/work/syzkaller",
        "procs": 1,
        "type": "qemu",
        "disable_syscalls": ["keyctl", "add_key", "request_key"],
        "suppressions": ["some known bug"],
        "vm": {
                "count": 1,
                "qemu": "qemu-system-aarch64",
                "cmdline": "console=ttyAMA0 root=/dev/vda",
                "kernel": "/work/linux-5.15/arch/arm64/boot/Image",
                "cpu": 2,
                "mem": 2048
        }
}

用意したコンフィグファイルを入力として、syz-managerを実行することで、ファジングテストが開始する。

leava@ubuntu:/work/syzkaller/$ sudo bin/syz-manager -config=my.cfg

テストが開始してからしばらく待った後に、ブラウザから http://127.0.0.1:56700/ にアクセスすると、次のようなWebページが表示される。

syzkallerのトップ画面

coverageにある数字をクリックすると次のようなWebページが表示される。

syzkallerによるcoverageの確認

github.com

おわりに

本記事では、kcovによるソースコードカバレッジの取得方法について確認した。
また、ファジングツールの一つsyzkallerで、ファジングテストの結果におけるカバレッジの取得方法についても確認した。

変更履歴

  • 2022/5/5: 記事公開

参考文献

Linuxカーネルのファイルアクセスの処理を追いかける (10) ext2: writepages

関連記事

概要

QEMUの vexpress-a9 (arm) で Linux 5.15を起動させながら、ファイル書き込みのカーネル処理を確認していく。

本章では、ext2_writepages関数を確認した。

はじめに

ユーザプロセスはファイルシステムという機構によって記憶装置上のデータをファイルという形式で書き込み・読み込みすることができる。
本調査では、ユーザプロセスがファイルに書き込み要求を実行したときにLinuxカーネルではどのような処理が実行されるかを読み解いていく。

調査対象や環境などはPart 1: 環境セットアップを参照。

調査対象 (シーケンス図)

本記事では、writebackカーネルスレッドがwritepagesを呼び出すところから、submit_bio関数を呼ぶところまでを確認する。

writepagesの概要

kerner thread のライトバック処理によって、Dirtyのinodeに対してwritepages関数とwrite_inode関数を呼び出す。

基本的には、writepages関数はファイルの実データの書き込み、write_inode関数はファイルのメタデータの書き込みをする。
ext2ファイルシステムの場合では、ext2_writepages関数とext2_write_inode関数が定義されている。

writepages関数の開始時のイメージ

ext2_writepages関数の定義は次の通りとなっている。

// 950:
static int
ext2_writepages(struct address_space *mapping, struct writeback_control *wbc)
{
    return mpage_writepages(mapping, wbc, ext2_get_block);
}

ext2_writepage関数は、mpage_writepage関数を呼び出す。(関数の引数にext2固有の関数ポインタを渡す)

mpage_writepages関数

// 693:
int
mpage_writepages(struct address_space *mapping,
        struct writeback_control *wbc, get_block_t get_block)
{
    struct blk_plug plug;
    int ret;

    blk_start_plug(&plug);

    if (!get_block)
        ret = generic_writepages(mapping, wbc);
    else {
        struct mpage_data mpd = {
            .bio = NULL,
            .last_block_in_bio = 0,
            .get_block = get_block,
            .use_writepage = 1,
        };

        ret = write_cache_pages(mapping, wbc, __mpage_writepage, &mpd);
        if (mpd.bio) {
            int op_flags = (wbc->sync_mode == WB_SYNC_ALL ?
                  REQ_SYNC : 0);
            mpage_bio_submit(REQ_OP_WRITE, op_flags, mpd.bio);
        }
    }
    blk_finish_plug(&plug);
    return ret;
}

ここで、writepages関数では、IOリクエストを管理するためのblk_plug構造体を取り扱う。
blk_start_plug関数で初期化 (または保留されているものを戻す) 、blk_finidh_plug関数で完了を表す。

blk_plug構造体のイメージ

ext2ファイルシステムでは、独自のget_block関数を渡すため、write_cache_pages関数とmpage_bio_submit関数が主な処理となっている。
write_cache_pages関数は、ext2_writepages関数から渡された関数に加えて、単体ページ書き込み関数__mpage_writepage関数とページ書き込み用メタデータmpage_data構造体を渡す。

ここで、今回の場合におけるwriteback_control構造体の値を再掲する。

変数名
nr_pages ノード内にあるDirtyページ数
sync_mode WB_SYNC_NONE
for_kupdate 1
for_background 0
range_cyclic 1

write_cache_pages関数の定義は次の通りとなっている。

write_cache_pages関数

// 2177:
int write_cache_pages(struct address_space *mapping,
              struct writeback_control *wbc, writepage_t writepage,
              void *data)
{
    int ret = 0;
    int done = 0;
    int error;
    struct pagevec pvec;
    int nr_pages;
    pgoff_t index;
    pgoff_t end;        /* Inclusive */
    pgoff_t done_index;
    int range_whole = 0;
    xa_mark_t tag;

    pagevec_init(&pvec);
    if (wbc->range_cyclic) {
        index = mapping->writeback_index; /* prev offset */
        end = -1;
    } else {
        index = wbc->range_start >> PAGE_SHIFT;
        end = wbc->range_end >> PAGE_SHIFT;
        if (wbc->range_start == 0 && wbc->range_end == LLONG_MAX)
            range_whole = 1;
    }
    if (wbc->sync_mode == WB_SYNC_ALL || wbc->tagged_writepages) {
        tag_pages_for_writeback(mapping, index, end);
        tag = PAGECACHE_TAG_TOWRITE;
    } else {
        tag = PAGECACHE_TAG_DIRTY;
    }
    done_index = index;
    while (!done && (index <= end)) {
        int i;

        nr_pages = pagevec_lookup_range_tag(&pvec, mapping, &index, end,
                tag);
        if (nr_pages == 0)
            break;

        for (i = 0; i < nr_pages; i++) {
            struct page *page = pvec.pages[i];

            done_index = page->index;

            lock_page(page);

            /*
            * Page truncated or invalidated. We can freely skip it
            * then, even for data integrity operations: the page
            * has disappeared concurrently, so there could be no
            * real expectation of this data integrity operation
            * even if there is now a new, dirty page at the same
            * pagecache address.
            */
            if (unlikely(page->mapping != mapping)) {
continue_unlock:
                unlock_page(page);
                continue;
            }

            if (!PageDirty(page)) {
                /* someone wrote it for us */
                goto continue_unlock;
            }

            if (PageWriteback(page)) {
                if (wbc->sync_mode != WB_SYNC_NONE)
                    wait_on_page_writeback(page);
                else
                    goto continue_unlock;
            }

            BUG_ON(PageWriteback(page));
            if (!clear_page_dirty_for_io(page))
                goto continue_unlock;

            trace_wbc_writepage(wbc, inode_to_bdi(mapping->host));
            error = (*writepage)(page, wbc, data);
            if (unlikely(error)) {
                /*
                * Handle errors according to the type of
                * writeback. There's no need to continue for
                * background writeback. Just push done_index
                * past this page so media errors won't choke
                * writeout for the entire file. For integrity
                * writeback, we must process the entire dirty
                * set regardless of errors because the fs may
                * still have state to clear for each page. In
                * that case we continue processing and return
                * the first error.
                */
                if (error == AOP_WRITEPAGE_ACTIVATE) {
                    unlock_page(page);
                    error = 0;
                } else if (wbc->sync_mode != WB_SYNC_ALL) {
                    ret = error;
                    done_index = page->index + 1;
                    done = 1;
                    break;
                }
                if (!ret)
                    ret = error;
            }

            /*
            * We stop writing back only if we are not doing
            * integrity sync. In case of integrity sync we have to
            * keep going until we have written all the pages
            * we tagged for writeback prior to entering this loop.
            */
            if (--wbc->nr_to_write <= 0 &&
                wbc->sync_mode == WB_SYNC_NONE) {
                done = 1;
                break;
            }
        }
        pagevec_release(&pvec);
        cond_resched();
    }

    /*
    * If we hit the last page and there is more work to be done: wrap
    * back the index back to the start of the file for the next
    * time we are called.
    */
    if (wbc->range_cyclic && !done)
        done_index = 0;
    if (wbc->range_cyclic || (range_whole && wbc->nr_to_write > 0))
        mapping->writeback_index = done_index;

    return ret;
}

write_cache_pages関数では、それぞれのページキャッシュに対して、次のような処理を実施していく。

  1. ページキャッシュをロックを取得する
  2. 引数のwritepage (__mpage_writepage関数)を呼び出す
  3. ページキャッシュのロックを解放する

そのために、pagevec_lookup_range_tag関数からPAGECACHE_TAG_DIRTY(tag)を検索していき、pvec(pagevec構造体)で管理する。
検索して得られたページがDirtyフラグが立っていない、Writebackフラグが立っている場合には、continu_unlockラベル (ロック解放して次のページを移る) にジャンプする。

__mpage_writepage関数

__mpage_writepage関数の定義は次の通りとなっている。

// 478:
static int __mpage_writepage(struct page *page, struct writeback_control *wbc,
              void *data)
{
    struct mpage_data *mpd = data;
    struct bio *bio = mpd->bio;
    struct address_space *mapping = page->mapping;
    struct inode *inode = page->mapping->host;
    const unsigned blkbits = inode->i_blkbits;
    unsigned long end_index;
    const unsigned blocks_per_page = PAGE_SIZE >> blkbits;
    sector_t last_block;
    sector_t block_in_file;
    sector_t blocks[MAX_BUF_PER_PAGE];
    unsigned page_block;
    unsigned first_unmapped = blocks_per_page;
    struct block_device *bdev = NULL;
    int boundary = 0;
    sector_t boundary_block = 0;
    struct block_device *boundary_bdev = NULL;
    int length;
    struct buffer_head map_bh;
    loff_t i_size = i_size_read(inode);
    int ret = 0;
    int op_flags = wbc_to_write_flags(wbc);

    if (page_has_buffers(page)) {
        struct buffer_head *head = page_buffers(page);
        struct buffer_head *bh = head;

        /* If they're all mapped and dirty, do it */
        page_block = 0;
        do {
            BUG_ON(buffer_locked(bh));
            if (!buffer_mapped(bh)) {
                /*
                * unmapped dirty buffers are created by
                * __set_page_dirty_buffers -> mmapped data
                */
                if (buffer_dirty(bh))
                    goto confused;
                if (first_unmapped == blocks_per_page)
                    first_unmapped = page_block;
                continue;
            }

            if (first_unmapped != blocks_per_page)
                goto confused;    /* hole -> non-hole */

            if (!buffer_dirty(bh) || !buffer_uptodate(bh))
                goto confused;
            if (page_block) {
                if (bh->b_blocknr != blocks[page_block-1] + 1)
                    goto confused;
            }
            blocks[page_block++] = bh->b_blocknr;
            boundary = buffer_boundary(bh);
            if (boundary) {
                boundary_block = bh->b_blocknr;
                boundary_bdev = bh->b_bdev;
            }
            bdev = bh->b_bdev;
        } while ((bh = bh->b_this_page) != head);

        if (first_unmapped)
            goto page_is_mapped;

        /*
        * Page has buffers, but they are all unmapped. The page was
        * created by pagein or read over a hole which was handled by
        * block_read_full_page().  If this address_space is also
        * using mpage_readahead then this can rarely happen.
        */
        goto confused;
    }

    /*
    * The page has no buffers: map it to disk
    */
    BUG_ON(!PageUptodate(page));
    block_in_file = (sector_t)page->index << (PAGE_SHIFT - blkbits);
    last_block = (i_size - 1) >> blkbits;
    map_bh.b_page = page;
    for (page_block = 0; page_block < blocks_per_page; ) {

        map_bh.b_state = 0;
        map_bh.b_size = 1 << blkbits;
        if (mpd->get_block(inode, block_in_file, &map_bh, 1))
            goto confused;
        if (buffer_new(&map_bh))
            clean_bdev_bh_alias(&map_bh);
        if (buffer_boundary(&map_bh)) {
            boundary_block = map_bh.b_blocknr;
            boundary_bdev = map_bh.b_bdev;
        }
        if (page_block) {
            if (map_bh.b_blocknr != blocks[page_block-1] + 1)
                goto confused;
        }
        blocks[page_block++] = map_bh.b_blocknr;
        boundary = buffer_boundary(&map_bh);
        bdev = map_bh.b_bdev;
        if (block_in_file == last_block)
            break;
        block_in_file++;
    }
    BUG_ON(page_block == 0);

    first_unmapped = page_block;

page_is_mapped:
    end_index = i_size >> PAGE_SHIFT;
    if (page->index >= end_index) {
        /*
        * The page straddles i_size.  It must be zeroed out on each
        * and every writepage invocation because it may be mmapped.
        * "A file is mapped in multiples of the page size.  For a file
        * that is not a multiple of the page size, the remaining memory
        * is zeroed when mapped, and writes to that region are not
        * written out to the file."
        */
        unsigned offset = i_size & (PAGE_SIZE - 1);

        if (page->index > end_index || !offset)
            goto confused;
        zero_user_segment(page, offset, PAGE_SIZE);
    }

    /*
    * This page will go to BIO.  Do we need to send this BIO off first?
    */
    if (bio && mpd->last_block_in_bio != blocks[0] - 1)
        bio = mpage_bio_submit(REQ_OP_WRITE, op_flags, bio);

alloc_new:
    if (bio == NULL) {
        if (first_unmapped == blocks_per_page) {
            if (!bdev_write_page(bdev, blocks[0] << (blkbits - 9),
                                page, wbc))
                goto out;
        }
        bio = mpage_alloc(bdev, blocks[0] << (blkbits - 9),
                BIO_MAX_VECS, GFP_NOFS|__GFP_HIGH);
        if (bio == NULL)
            goto confused;

        wbc_init_bio(wbc, bio);
        bio->bi_write_hint = inode->i_write_hint;
    }

    /*
    * Must try to add the page before marking the buffer clean or
    * the confused fail path above (OOM) will be very confused when
    * it finds all bh marked clean (i.e. it will not write anything)
    */
    wbc_account_cgroup_owner(wbc, page, PAGE_SIZE);
    length = first_unmapped << blkbits;
    if (bio_add_page(bio, page, length, 0) < length) {
        bio = mpage_bio_submit(REQ_OP_WRITE, op_flags, bio);
        goto alloc_new;
    }

    clean_buffers(page, first_unmapped);

    BUG_ON(PageWriteback(page));
    set_page_writeback(page);
    unlock_page(page);
    if (boundary || (first_unmapped != blocks_per_page)) {
        bio = mpage_bio_submit(REQ_OP_WRITE, op_flags, bio);
        if (boundary_block) {
            write_boundary_block(boundary_bdev,
                    boundary_block, 1 << blkbits);
        }
    } else {
        mpd->last_block_in_bio = blocks[blocks_per_page - 1];
    }
    goto out;

confused:
    if (bio)
        bio = mpage_bio_submit(REQ_OP_WRITE, op_flags, bio);

    if (mpd->use_writepage) {
        ret = mapping->a_ops->writepage(page, wbc);
    } else {
        ret = -EAGAIN;
        goto out;
    }
    /*
    * The caller has a ref on the inode, so *mapping is stable
    */
    mapping_set_error(mapping, ret);
out:
    mpd->bio = bio;
    return ret;
}

__mpage_writepage関数は、該当ページが連続しているかどうかを考慮しつつ、bioを準備する関数となっている。

連続している場合 (かつ、ページキャッシュ内のデータがすべて書き込み対象である) には、それに対応するbioを生成する。

書き込み連続している場合の__mpage_writepage関数

一方で、連続していない場合には、連続しているブロックのみのbioを発行し、残りのブロックのためのbioを生成する。

書き込み非連続している場合の__mpage_writepage関数

これらを踏まえて、ソースコード上の動作も確認する。

__mpage_writepage

__mpage_writepage関数は、それぞれの状態に応じてラベルにジャンプしたり条件分岐することで多数の状態に対応している。

page_is_mapped

ページキャッシュが記憶装置のブロックとマッピングされている場合の処理となる。

page_is_mappedラベルの概要図

  • ページキャッシュに載っている書き込み対象でない範囲のゼロ埋めを実施する。
  • 非連続となっているブロックのbioを発行する

ページキャッシュをゼロ埋めをする処理はzero_user_segment関数が担う。

// 222:
static inline void zero_user_segment(struct page *page,
    unsigned start, unsigned end)
{
    zero_user_segments(page, start, end, 0, 0);
}

zero_user_segment関数は、zero_user_segments関数のラッパ関数となっている。

// 201:
static inline void zero_user_segments(struct page *page,
        unsigned start1, unsigned end1,
        unsigned start2, unsigned end2)
{
    void *kaddr = kmap_atomic(page);
    unsigned int i;

    BUG_ON(end1 > page_size(page) || end2 > page_size(page));

    if (end1 > start1)
        memset(kaddr + start1, 0, end1 - start1);

    if (end2 > start2)
        memset(kaddr + start2, 0, end2 - start2);

    kunmap_atomic(kaddr);
    for (i = 0; i < compound_nr(page); i++)
        flush_dcache_page(page + i);
}

kunmap_ztomic関数やflush_dcache_page関数は、以前の記事 (write_iter) と同様である。

leavatail.hatenablog.com

alloc_new

mmcドライバではできないので詳細は割愛するが、bdev_write_page関数によってページキャッシュによる書き込みができるドライバであれば、実施して終了する。

// 357:
int bdev_write_page(struct block_device *bdev, sector_t sector,
            struct page *page, struct writeback_control *wbc)
{
    int result;
    const struct block_device_operations *ops = bdev->bd_disk->fops;

    if (!ops->rw_page || bdev_get_integrity(bdev))
        return -EOPNOTSUPP;
    result = blk_queue_enter(bdev->bd_disk->queue, 0);
    if (result)
        return result;

    set_page_writeback(page);
    result = ops->rw_page(bdev, sector + get_start_sect(bdev), page,
                  REQ_OP_WRITE);
    if (result) {
        end_page_writeback(page);
    } else {
        clean_page_buffers(page);
        unlock_page(page);
    }
    blk_queue_exit(bdev->bd_disk->queue);
    return result;
}

ちなみに、Linux v5.10現在で対応しているのは次の3種類のみとなっている。

  • zram
  • brd
  • nvdimm

そうではない場合には、mpage_alloc関数によってbioを生成し、書き込み対象をbiovecに追加する処理となる。

// 71:
static struct bio *
mpage_alloc(struct block_device *bdev,
        sector_t first_sector, int nr_vecs,
        gfp_t gfp_flags)
{
    struct bio *bio;

    /* Restrict the given (page cache) mask for slab allocations */
    gfp_flags &= GFP_KERNEL;
    bio = bio_alloc(gfp_flags, nr_vecs);

    if (bio == NULL && (current->flags & PF_MEMALLOC)) {
        while (!bio && (nr_vecs /= 2))
            bio = bio_alloc(gfp_flags, nr_vecs);
    }

    if (bio) {
        bio_set_dev(bio, bdev);
        bio->bi_iter.bi_sector = first_sector;
    }
    return bio;
}

mpage_alloc関数は、bioの生成するためにbio_alloc関数を呼び出す。

// 442:
static inline struct bio *bio_alloc(gfp_t gfp_mask, unsigned int nr_iovecs)
{
    return bio_alloc_bioset(gfp_mask, nr_iovecs, &fs_bio_set);
}

bioの生成には、bio_alloc_bioset関数と呼ばれるAPIを実行する。(詳細は割愛)

www.kernel.org

その後、新規に生成したbioに対して、IO対象となるページキャッシュを追加する。

bioがマージ可能かどうか

bioをマージできる条件として次のようなものがある。

  • ページキャッシュが同じ場合
  • 書き込み先が連続している場合
// 1004:
int bio_add_page(struct bio *bio, struct page *page,
         unsigned int len, unsigned int offset)
{
    bool same_page = false;

    if (!__bio_try_merge_page(bio, page, len, offset, &same_page)) {
        if (bio_full(bio, len))
            return 0;
        __bio_add_page(bio, page, len, offset);
    }
    return len;
}

これらの場合には、__bio_try_merge_page関数でbioのマージ (bv_lenbi_sizeの更新) を試みる。

confused

生成できているだけのbioを発行する。

mpage_bio_submit関数

mpage_bio_submit関数

// 61:
static struct bio *mpage_bio_submit(int op, int op_flags, struct bio *bio)
{
    bio->bi_end_io = mpage_end_io;
    bio_set_op_attrs(bio, op, op_flags);
    guard_bio_eod(bio);
    submit_bio(bio);
    return NULL;
}

bi_end_ioはI/Oが完了時に呼び出すハンドラとなっている。(詳細は別記事) bio_set_op_attrs関数は、bio構造体のbi_opfにフラグを設定する

// 438:
static inline void bio_set_op_attrs(struct bio *bio, unsigned op,
        unsigned op_flags)
{
    bio->bi_opf = op | op_flags;
}

今回のケースでは、op_flagswbc_to_write_flags関数によって更新される。

//97:
static inline int wbc_to_write_flags(struct writeback_control *wbc)
{
    int flags = 0;

    if (wbc->punt_to_cgroup)
        flags = REQ_CGROUP_PUNT;

    if (wbc->sync_mode == WB_SYNC_ALL)
        flags |= REQ_SYNC;
    else if (wbc->for_kupdate || wbc->for_background)
        flags |= REQ_BACKGROUND;

    return flags;
}
変数名 wb_check_start_all wb_check_old_data_flush wb_check_background_flush
sync_mode WB_SYNC_NONE WB_SYNC_NONE WB_SYNC_NONE
for_kupdate 0 1 0
for_background 0 0 1

wb_check_old_data_flush関数やwb_check_background_flush関数の場合には、REQ_BACKGROUNDが付与される。

// 600:
void guard_bio_eod(struct bio *bio)
{
    sector_t maxsector = bdev_nr_sectors(bio->bi_bdev);

    if (!maxsector)
        return;

    /*
    * If the *whole* IO is past the end of the device,
    * let it through, and the IO layer will turn it into
    * an EIO.
    */
    if (unlikely(bio->bi_iter.bi_sector >= maxsector))
        return;

    maxsector -= bio->bi_iter.bi_sector;
    if (likely((bio->bi_iter.bi_size >> 9) <= maxsector))
        return;

    bio_truncate(bio, maxsector << 9);
}

また、bioの書き込み範囲がブロックデバイスを超えている場合、guard_bio_eod関数により切り詰める。

こうして得られたbioをsubmit_bio関数を実行して、IOを発行する。

おわりに

本記事では、ext2ファイルシステムwritepages操作 (ext2_writepages)を解説した。
今回の環境でext2_writepages関数を実行することで、次のようなbio構造体が生成された。

ext2_writepages関数の結果

変更履歴

  • 2022/05/01: 記事公開
  • 2022/06/13: 動作後のイメージ図の追加
  • 2022/09/19: カーネルバージョンを5.15に変更

参考