LeavaTailの日記

LeavaTailの日記

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

Linuxカーネルのファイルアクセスの処理を追いかける (15) mq-deadline: insert_request

関連記事

概要

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

本章では、mq-deadline I/Oスケジューラのリクエスト挿入処理(insert_requests)とマージ処理解放処理(dd_bio_merge)を確認した。

はじめに

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

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

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

mq-deadlineの関数群

dd_bio_merge

dd_bio_merge関数は、elevetor_typeにあるopsbio_mergeに設定され、blk_mq_update_nr_requests関数から呼び出される関数となっている。

bio_mergeでは、発行される bio が Elevator (または、I/O スケジューラ) にあるrequestと可能であればマージする。

mq-deadline I/O スケジューラで登録されている dd_bio_merge関数の定義は次の通りとなっている。

// 639:
static bool dd_bio_merge(struct request_queue *q, struct bio *bio,
        unsigned int nr_segs)
{
    struct deadline_data *dd = q->elevator->elevator_data;
    struct request *free = NULL;
    bool ret;

    spin_lock(&dd->lock);
    ret = blk_mq_sched_try_merge(q, bio, nr_segs, &free);
    spin_unlock(&dd->lock);

    if (free)
        blk_mq_free_request(free);

    return ret;
}

dd_bio_merge関数は、mq-deadline I/O スケジューラが保持するロックを確保し、ブロック層汎用のblk_mq_sched_try_merge関数を呼び出す。

このロックを保持している間が、次の処理と排他で動作することになる。

  • dd_dispatch_request
  • dd_bio_merge
  • dd_insert_requests
  • deadline_##name##_fifo_start
  • deadline_dispatch##prio##_start

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

// 1106:
bool blk_mq_sched_try_merge(struct request_queue *q, struct bio *bio,
        unsigned int nr_segs, struct request **merged_request)
{
    struct request *rq;

    switch (elv_merge(q, &rq, bio)) {
    case ELEVATOR_BACK_MERGE:
        if (!blk_mq_sched_allow_merge(q, rq, bio))
            return false;
        if (bio_attempt_back_merge(rq, bio, nr_segs) != BIO_MERGE_OK)
            return false;
        *merged_request = attempt_back_merge(q, rq);
        if (!*merged_request)
            elv_merged_request(q, rq, ELEVATOR_BACK_MERGE);
        return true;
    case ELEVATOR_FRONT_MERGE:
        if (!blk_mq_sched_allow_merge(q, rq, bio))
            return false;
        if (bio_attempt_front_merge(rq, bio, nr_segs) != BIO_MERGE_OK)
            return false;
        *merged_request = attempt_front_merge(q, rq);
        if (!*merged_request)
            elv_merged_request(q, rq, ELEVATOR_FRONT_MERGE);
        return true;
    case ELEVATOR_DISCARD_MERGE:
        return bio_attempt_discard_merge(q, rq, bio) == BIO_MERGE_OK;
    default:
        return false;
    }
}

blk_mq_sched_try_merge関数は、bioqにあるリクエストとマージを試みる関数となっている。
既存のリクエストとマージすることができた場合には、merged_requestにポインタが設定され、 trueを返す。

blk_mq_sched_try_merge関数では、まず elv_merge関数によって bioが既存の q にあるリクエストにマージできるかチェックする。
bioのマージには、"front merge"(マージ先の先頭に追加) と"back merge"(マージ先の末尾に追加)が存在する。

例えば、ブロック番号4,5,6 に対する bio A,B,C のための request a とマージする場合を考える。

front_merge と back_merge となる例

この時、bio X (ブロック番号3) は ”front merge”、bio Y(ブロック番号7)は"back merge"となる。

まずは、マージできるかどうかをelv_merge関数について確認する。
elv_merge関数の定義は次の通りとなっている。

// 303:
enum elv_merge elv_merge(struct request_queue *q, struct request **req,
        struct bio *bio)
{
    struct elevator_queue *e = q->elevator;
    struct request *__rq;

    /*
    * Levels of merges:
    *  nomerges:  No merges at all attempted
    *  noxmerges: Only simple one-hit cache try
    *  merges:    All merge tries attempted
    */
    if (blk_queue_nomerges(q) || !bio_mergeable(bio))
        return ELEVATOR_NO_MERGE;

    /*
    * First try one-hit cache.
    */
    if (q->last_merge && elv_bio_merge_ok(q->last_merge, bio)) {
        enum elv_merge ret = blk_try_merge(q->last_merge, bio);

        if (ret != ELEVATOR_NO_MERGE) {
            *req = q->last_merge;
            return ret;
        }
    }

    if (blk_queue_noxmerges(q))
        return ELEVATOR_NO_MERGE;

    /*
    * See if our hash lookup can find a potential backmerge.
    */
    __rq = elv_rqhash_find(q, bio->bi_iter.bi_sector);
    if (__rq && elv_bio_merge_ok(__rq, bio)) {
        *req = __rq;

        if (blk_discard_mergable(__rq))
            return ELEVATOR_DISCARD_MERGE;
        return ELEVATOR_BACK_MERGE;
    }

    if (e->type->ops.request_merge)
        return e->type->ops.request_merge(q, req, bio);

    return ELEVATOR_NO_MERGE;
}

elv_merge関数では、ElevetorにキャッシュされているリクエストとI/Oスケジューラに登録されているリクエストからマージできるかどうか検索する。

Elevetorは、最後に扱ったリクエストをlast_mergeとしてキャッシュし、扱ったリクエストを hashで管理している。
ここでキャッシュされたリクエストとマージできない場合には、I/Oスケジューラで管理しているリクエストを検索する。

mq-deadline I/Oスケジューラの場合には、dd_request_merge関数によって赤黒木を検索することになる。

elv_merge関数における検索方法について

ただし、bioとリクエストキューのフラグによってはマージができないこともある。
そのため、blk_queue_nomerges関数, bio_mergeable関数, blk_queue_noxmerges関数でマージ可能かどうかをチェックする。

確認する項目は次の通りとなっている。

  • test_bit(QUEUE_FLAG_NOMERGES, &(q)->queue_flags)
  • bio->bi_opf & REQ_NOMERGE_FLAGS)
  • test_bit(QUEUE_FLAG_NOXMERGES, &(q)->queue_flags)

さらに、リクエストの内容によってはマージできないこともあるため、elv_bio_merge_ok関数によるチェックも必要となる。

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

// 74:
bool elv_bio_merge_ok(struct request *rq, struct bio *bio)
{
    if (!blk_rq_merge_ok(rq, bio))
        return false;

    if (!elv_iosched_allow_bio_merge(rq, bio))
        return false;

    return true;
}

mq-deadline I/Oスケジューラにが、特別なrequestのマージ条件がない (allow_mergeが未定義)であるため、blk_rq_merge_ok関数によってマージ可能か判定できる。

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

// 850:
bool blk_rq_merge_ok(struct request *rq, struct bio *bio)
{
    if (!rq_mergeable(rq) || !bio_mergeable(bio))
        return false;

    if (req_op(rq) != bio_op(bio))
        return false;

    /* different data direction or already started, don't merge */
    if (bio_data_dir(bio) != rq_data_dir(rq))
        return false;

    /* must be same device */
    if (rq->rq_disk != bio->bi_bdev->bd_disk)
        return false;

    /* only merge integrity protected bio into ditto rq */
    if (blk_integrity_merge_bio(rq->q, rq, bio) == false)
        return false;

    /* Only merge if the crypt contexts are compatible */
    if (!bio_crypt_rq_ctx_compatible(rq, bio))
        return false;

    /* must be using the same buffer */
    if (req_op(rq) == REQ_OP_WRITE_SAME &&
        !blk_write_same_mergeable(rq->bio, bio))
        return false;

    /*
    * Don't allow merge of different write hints, or for a hint with
    * non-hint IO.
    */
    if (rq->write_hint != bio->bi_write_hint)
        return false;

    if (rq->ioprio != bio_prio(bio))
        return false;

    return true;
}

blk_rq_merge_ok関数で確認する内容は次の通りとなっている。

  • 対象のrequestがマージ可能な操作である
    • REQ_OP_READ
    • REQ_OP_WRITE
    • REQ_OP_DISCARD (blk_discarrd_mergable関数参照)
    • REQ_OP_SECURE_ERASE
    • REQ_OP_WRITE_SAME
    • REQ_OP_ZONE_OPEN
    • REQ_OP_ZONE_CLOSE
    • REQ_OP_ZONE_FINISH
    • REQ_OP_ZONE_RESET
    • REQ_OP_ZONE_RESET_ALL
  • 対象のrequestがマージ可能なステータスである
    • cmd_flagsREQ_NOMERGE_FLAGSが立っていない
    • rq_flagsRQF_NOMERGE_FLAGSが立っていない
  • マージ対象のbioがマージ可能なステータスである
    • bi_opfREQ_NOMERGE_FLAGSが立っていない
  • 対象のrequestbioが同じ操作であること
  • 対象のrequestbioがデータ方向 (Read か Write)が同じであること
  • 対象のrequestbioの書き込み先デバイスが同じであること
  • 対象のrequestbioのBlock layer data integrity機能でマージ可能であること
  • 対象のrequestbioのBlock layer inline encryption機能でマージ可能であること
  • REQ_OP_WRITE_SAMEの場合には、書き込み先のページとオフセットが同じであること
  • 対象のrequestbioのwrite hintが同じであること
  • 対象のrequestbioのI/O優先度が同じであること

ここで、requestへのマージが可能であれば、そのリクエストをreqに代入し、 ELEVATOR_FRONT_MERGE (または、ELEVATOR_BACK_MERGE)を返す。
もし、elv_merge関数が マージ不可能(ELEVATOR_NO_MERGE)と判断されれば、ここで処理が終了する。

マージできる可能性がある場合には、blk_mq_sched_allow_merge関数によって I/Oスケジューラ独自のチェック関数を呼び出す。 (mq-deadline I/Oスケジューラは未定義) このチェックも通った場合には、bio_attemp_XXX_merge(XXX = back or front or discard)関数によるマージを試みる。

// 919:
static enum bio_merge_status bio_attempt_back_merge(struct request *req,
        struct bio *bio, unsigned int nr_segs)
{
    const int ff = bio->bi_opf & REQ_FAILFAST_MASK;

    if (!ll_back_merge_fn(req, bio, nr_segs))
        return BIO_MERGE_FAILED;

    trace_block_bio_backmerge(bio);
    rq_qos_merge(req->q, req, bio);

    if ((req->cmd_flags & REQ_FAILFAST_MASK) != ff)
        blk_rq_set_mixed_merge(req);

    req->biotail->bi_next = bio;
    req->biotail = bio;
    req->__data_len += bio->bi_iter.bi_size;

    bio_crypt_free_ctx(bio);

    blk_account_io_merge_bio(req);
    return BIO_MERGE_OK;
}

static enum bio_merge_status bio_attempt_front_merge(struct request *req,
        struct bio *bio, unsigned int nr_segs)
{
    const int ff = bio->bi_opf & REQ_FAILFAST_MASK;

    if (!ll_front_merge_fn(req, bio, nr_segs))
        return BIO_MERGE_FAILED;

    trace_block_bio_frontmerge(bio);
    rq_qos_merge(req->q, req, bio);

    if ((req->cmd_flags & REQ_FAILFAST_MASK) != ff)
        blk_rq_set_mixed_merge(req);

    bio->bi_next = req->bio;
    req->bio = bio;

    req->__sector = bio->bi_iter.bi_sector;
    req->__data_len += bio->bi_iter.bi_size;

    bio_crypt_do_front_merge(req, bio);

    blk_account_io_merge_bio(req);
    return BIO_MERGE_OK;
}

static enum bio_merge_status bio_attempt_discard_merge(struct request_queue *q,
        struct request *req, struct bio *bio)
{
    unsigned short segments = blk_rq_nr_discard_segments(req);

    if (segments >= queue_max_discard_segments(q))
        goto no_merge;
    if (blk_rq_sectors(req) + bio_sectors(bio) >
        blk_rq_get_max_sectors(req, blk_rq_pos(req)))
        goto no_merge;

    rq_qos_merge(q, req, bio);

    req->biotail->bi_next = bio;
    req->biotail = bio;
    req->__data_len += bio->bi_iter.bi_size;
    req->nr_phys_segments = segments + 1;

    blk_account_io_merge_bio(req);
    return BIO_MERGE_OK;
no_merge:
    req_set_nomerge(q, req);
    return BIO_MERGE_FAILED;
}

ここでは、bio_attempt_discard_merge関数については取り扱わないものとする。

bio_attempt_back_merge関数とbio_attempt_back_merge関数では、ll_back_merge_fn, ll_front_merge_fn関数によるセグメントを含めたチェックが必要となる。

セグメントは、Scatter-Gather DMAにおけるデータ転送の単位であり、隣接するメモリページ (の一部) となっている。

bioとpageとsegment

バイス(NVMe, SCSIなど)では、一度に扱うことができる書き込み単位や個数が定義されていたりする。 (q->limits.seg_boundary_maskq->limits.max_segments)。

ll_back_merge_fn, ll_front_merge_fn関数では、req_gap_front_merge,req_gap_front_merge関数によってメモリページのアドレスを確認したり、ll_new_hw_segment関数によるセグメント情報の更新をする。

セグメント観点でもマージができそうであれば、リクエスreq のデータを更新することでbioをマージする。

request に bio を back merge する場合

また、リクエスreqbioを追加したことによって、既存のリクエスト間でマージできることがある。

例えば、リクエスrqにブロック番号"7,8,9,10,11"のbioに"12"を追加した場合を考える。
この時、リクエスrqの次のノード nextとブロック番号が連続することになる。

リクエスト間でマージする場合

そこで、attempt_back_merge, attempt_front_merge関数によってリクエストとのマージを試みる。

  • back_mergeした場合には、次のリクエスnextelv_latter_request関数で取得する。
  • front_mergeした場合には前のリクエスprevelv_former_request関数で取得する。
// 817:
static struct request *attempt_back_merge(struct request_queue *q,
        struct request *rq)
{
    struct request *next = elv_latter_request(q, rq);

    if (next)
        return attempt_merge(q, rq, next);

    return NULL;
}

static struct request *attempt_front_merge(struct request_queue *q,
        struct request *rq)
{
    struct request *prev = elv_former_request(q, rq);

    if (prev)
        return attempt_merge(q, prev, rq);

    return NULL;
}

隣接するリクエストが取得できたら、attemp_merge関数によってリクエストのマージを試みる。

// 725:
static struct request *attempt_merge(struct request_queue *q,
                     struct request *req, struct request *next)
{
    if (!rq_mergeable(req) || !rq_mergeable(next))
        return NULL;

    if (req_op(req) != req_op(next))
        return NULL;

    if (rq_data_dir(req) != rq_data_dir(next)
        || req->rq_disk != next->rq_disk)
        return NULL;

    if (req_op(req) == REQ_OP_WRITE_SAME &&
        !blk_write_same_mergeable(req->bio, next->bio))
        return NULL;

    /*
    * Don't allow merge of different write hints, or for a hint with
    * non-hint IO.
    */
    if (req->write_hint != next->write_hint)
        return NULL;

    if (req->ioprio != next->ioprio)
        return NULL;

    /*
    * If we are allowed to merge, then append bio list
    * from next to rq and release next. merge_requests_fn
    * will have updated segment counts, update sector
    * counts here. Handle DISCARDs separately, as they
    * have separate settings.
    */

    switch (blk_try_req_merge(req, next)) {
    case ELEVATOR_DISCARD_MERGE:
        if (!req_attempt_discard_merge(q, req, next))
            return NULL;
        break;
    case ELEVATOR_BACK_MERGE:
        if (!ll_merge_requests_fn(q, req, next))
            return NULL;
        break;
    default:
        return NULL;
    }

    /*
    * If failfast settings disagree or any of the two is already
    * a mixed merge, mark both as mixed before proceeding.  This
    * makes sure that all involved bios have mixable attributes
    * set properly.
    */
    if (((req->rq_flags | next->rq_flags) & RQF_MIXED_MERGE) ||
        (req->cmd_flags & REQ_FAILFAST_MASK) !=
        (next->cmd_flags & REQ_FAILFAST_MASK)) {
        blk_rq_set_mixed_merge(req);
        blk_rq_set_mixed_merge(next);
    }

    /*
    * At this point we have either done a back merge or front merge. We
    * need the smaller start_time_ns of the merged requests to be the
    * current request for accounting purposes.
    */
    if (next->start_time_ns < req->start_time_ns)
        req->start_time_ns = next->start_time_ns;

    req->biotail->bi_next = next->bio;
    req->biotail = next->biotail;

    req->__data_len += blk_rq_bytes(next);

    if (!blk_discard_mergable(req))
        elv_merge_requests(q, req, next);

    /*
    * 'next' is going away, so update stats accordingly
    */
    blk_account_io_merge_request(next);

    trace_block_rq_merge(next);

    /*
    * ownership of bio passed from next to req, return 'next' for
    * the caller to free
    */
    next->bio = NULL;
    return next;
}

dd_request_merge

dd_request_merge関数は、elevetor_typeにあるopsrequest_mergeに設定され、__blk_mq_sched_bio_merge関数から呼び出される関数となっている。

request_mergeでは、発行される bio が deadline I/O スケジューラ の赤黒木にあるrequestと可能であればマージする。

request_mergeの概略図

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

// 607:
static int dd_request_merge(struct request_queue *q, struct request **rq,
                struct bio *bio)
{
    struct deadline_data *dd = q->elevator->elevator_data;
    const u8 ioprio_class = IOPRIO_PRIO_CLASS(bio->bi_ioprio);
    const enum dd_prio prio = ioprio_class_to_prio[ioprio_class];
    struct dd_per_prio *per_prio = &dd->per_prio[prio];
    sector_t sector = bio_end_sector(bio);
    struct request *__rq;

    if (!dd->front_merges)
        return ELEVATOR_NO_MERGE;

    __rq = elv_rb_find(&per_prio->sort_list[bio_data_dir(bio)], sector);
    if (__rq) {
        BUG_ON(sector != blk_rq_pos(__rq));

        if (elv_bio_merge_ok(__rq, bio)) {
            *rq = __rq;
            if (blk_discard_mergable(__rq))
                return ELEVATOR_DISCARD_MERGE;
            return ELEVATOR_FRONT_MERGE;
        }
    }

    return ELEVATOR_NO_MERGE;
}

front_mergeが無効になっている場合は、dd_request_merge関数での処理は何も実施しない。

それ以外の場合には、elv_rb_find関数で対象のbioの書き込みセクタと同じrequestを探索する。

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

// 283:
struct request *elv_rb_find(struct rb_root *root, sector_t sector)
{
    struct rb_node *n = root->rb_node;
    struct request *rq;

    while (n) {
        rq = rb_entry(n, struct request, rb_node);

        if (sector < blk_rq_pos(rq))
            n = n->rb_left;
        else if (sector > blk_rq_pos(rq))
            n = n->rb_right;
        else
            return rq;
    }

    return NULL;
}

rb_entry関数により対象の赤黒木root->rb_nodeからrequest取得し、木をたどっていく。
bioと同じ_sectorを持っているrequestが見つかればそれを返し、そうでなければNULLを返す。

requestが見つかった場合には、elv_bio_merge_ok関数によってマージ可能かどうかを判定する。

blk_rq_merge_ok関数については前述した通りとなっている。

dd_limit_depth

dd_limit_depth関数は、elevetor_typeにあるopslimit_depthに設定され、__blk_mq_alloc_request関数から呼び出される関数となっている。

limit_depthでは、blk_mq_alloc_datashallow_depth をI/Oスケジューラ毎に値を設定する。

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

// 498:
static void dd_limit_depth(unsigned int op, struct blk_mq_alloc_data *data)
{
    struct deadline_data *dd = data->q->elevator->elevator_data;

    /* Do not throttle synchronous reads. */
    if (op_is_sync(op) && !op_is_write(op))
        return;

    /*
    * Throttle asynchronous requests and writes such that these requests
    * do not block the allocation of synchronous requests.
    */
    data->shallow_depth = dd->async_depth;
}

dd_limit_depth関数では、非同期要求が施入できるキューの深さ async_depthに設定する。

dd_prepare_request

dd_prepare_request関数は、elevetor_typeにあるopsprepare_requestに設定され、blk_mq_rq_ctx_init関数から呼び出される関数となっている。

prepare_requestでは、blk_mq_rq_ctx_init をI/Oスケジューラ毎に必要に応じて初期化する。

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

// 731:
/* Callback from inside blk_mq_rq_ctx_init(). */
static void dd_prepare_request(struct request *rq)
{
    rq->elv.priv[0] = NULL;
}

dd_insert_requests

dd_insert_requests関数は、elevetor_typeにあるopsinsert_requestsに設定され、blk_mq_sched_insert_request関数などから呼び出される関数となっている。

insert_requestsでは、Software Staging queues に request を追加する。  

dd_insert_request の概略図

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

// 713:
static void dd_insert_requests(struct blk_mq_hw_ctx *hctx,
                   struct list_head *list, bool at_head)
{
    struct request_queue *q = hctx->queue;
    struct deadline_data *dd = q->elevator->elevator_data;

    spin_lock(&dd->lock);
    while (!list_empty(list)) {
        struct request *rq;

        rq = list_first_entry(list, struct request, queuelist);
        list_del_init(&rq->queuelist);
        dd_insert_request(hctx, rq, at_head);
    }
    spin_unlock(&dd->lock);
}

dd_insert_requests関数は、リクエスト毎にdd_insert_request関数を呼び出す。

// 672:
static void dd_insert_request(struct blk_mq_hw_ctx *hctx, struct request *rq,
                  bool at_head)
{
    struct request_queue *q = hctx->queue;
    struct deadline_data *dd = q->elevator->elevator_data;
    const enum dd_data_dir data_dir = rq_data_dir(rq);
    u16 ioprio = req_get_ioprio(rq);
    u8 ioprio_class = IOPRIO_PRIO_CLASS(ioprio);
    struct dd_per_prio *per_prio;
    enum dd_prio prio;
    LIST_HEAD(free);

    lockdep_assert_held(&dd->lock);

    /*
    * This may be a requeue of a write request that has locked its
    * target zone. If it is the case, this releases the zone lock.
    */
    blk_req_zone_write_unlock(rq);

    prio = ioprio_class_to_prio[ioprio_class];
    dd_count(dd, inserted, prio);
    rq->elv.priv[0] = (void *)(uintptr_t)1;

    if (blk_mq_sched_try_insert_merge(q, rq, &free)) {
        blk_mq_free_requests(&free);
        return;
    }

    trace_block_rq_insert(rq);

    per_prio = &dd->per_prio[prio];
    if (at_head) {
        list_add(&rq->queuelist, &per_prio->dispatch);
    } else {
        deadline_add_rq_rb(per_prio, rq);

        if (rq_mergeable(rq)) {
            elv_rqhash_add(q, rq);
            if (!q->last_merge)
                q->last_merge = rq;
        }

        /*
        * set expire time and add to fifo list
        */
        rq->fifo_time = jiffies + dd->fifo_expire[data_dir];
        list_add_tail(&rq->queuelist, &per_prio->fifo_list[data_dir]);
    }
}

‘dd_insert_request‘関数は、次のような処理を実施する。

  • sysfsからリクエストの統計を取得できるように dd_countマクロで insertedをインクリメントする。
  • 簡単に既存のリクエストにマージできるのであれば、マージを試みる
  • リクエストをmq-deadline I/Oスケジューラの赤黒木とFIFO (または、dispatchキュー)に挿入する

mq-deadline I/Oスケジューラでは、リクエストの統計をユーザランドから確認できるように sysfs インターフェースに統計情報を記録している。 dd_countマクロの定義は次のようになっている。

// 107:
#define dd_count(dd, event_type, prio) do {               \
   struct io_stats *io_stats = get_cpu_ptr((dd)->stats);        \
                                   \
   BUILD_BUG_ON(!__same_type((dd), struct deadline_data *));    \
   BUILD_BUG_ON(!__same_type((prio), enum dd_prio));        \
   local_inc(&io_stats->stats[(prio)].event_type);           \
   put_cpu_ptr(io_stats);                       \
} while (0)

この結果により、queued

# cat /sys/kernel/debug/block/mmcblk0/sched/queued
/sys/kernel/debug/block/mmcblk0/sched/queued
0 1 0

ここでは、リクエストキューがマージに対応しているかどうか確認する rq_mergeableマクロとマージを試みる elv_attempt_insert_merge関数の結果の論理積となる。
elv_attempt_insert_merge関数では、back mergeが可能であればマージする関数となっている。

既存リクエストへのマージをトライアルはblk_mq_sched_try_insert_merge関数によって実施される。
blk_mq_sched_try_insert_merge関数の定義は次のようになっている。

// 402:
bool blk_mq_sched_try_insert_merge(struct request_queue *q, struct request *rq,
                   struct list_head *free)
{
    return rq_mergeable(rq) && elv_attempt_insert_merge(q, rq, free);
}

マージする場合と同様にrq_mergeable関数がリクエストの状態を確認し、blk_attempt_req_merge関数がマージする処理となる。
blk_attempt_req_merge関数の定義は次のようになっている。

// 359:
bool elv_attempt_insert_merge(struct request_queue *q, struct request *rq,
                  struct list_head *free)
{
    struct request *__rq;
    bool ret;

    if (blk_queue_nomerges(q))
        return false;

    /*
    * First try one-hit cache.
    */
    if (q->last_merge && blk_attempt_req_merge(q, q->last_merge, rq)) {
        list_add(&rq->queuelist, free);
        return true;
    }

    if (blk_queue_noxmerges(q))
        return false;

    ret = false;
    /*
    * See if our hash lookup can find a potential backmerge.
    */
    while (1) {
        __rq = elv_rqhash_find(q, blk_rq_pos(rq));
        if (!__rq || !blk_attempt_req_merge(q, __rq, rq))
            break;

        list_add(&rq->queuelist, free);
        /* The merged request could be merged with others, try again */
        ret = true;
        rq = __rq;
    }

    return ret;
}

おわりに

本記事では、mq-deadline I/Oスケジューラのdd_bio_merge関数とdd_insert_requests関数を確認した。

変更履歴

  • 2023/04/30: 記事公開

参考

ファイルシステムの違いによるストレージへの書き込み特性を比較する

注意

本記事は、特定の環境下における特定の書き込みパターンを測定した結果であり、ファイルシステムの優劣を決めるものではない。

概要

本記事では、eBPF と fio を用いて、次のようなストレージへの書き込み特性について確認した。

  • IO要求の発行回数
  • 書き込み総量
  • 書き込み頻度とオフセット

Linuxファイルシステム (ext4, xfs, nilfs2, f2fs, exfat, btrfs, jfs) 毎にどのような特性があるかをまとめた。

はじめに

フラッシュメモリは、ハードディスクといったストレージデバイスと比較して、アクセス速度や省電力が優れている。 そのため近年、SDメモリカードSSDといったフラッシュメモリを搭載したストレージデバイスが広く普及している。

一方で、フラッシュメモリはその仕組みにより、ハードディスクと比較して「データ書き込み回数」に限りがあるといった課題がある。

ストレージデバイス搭載コントローラが直寿命化するような工夫が施されていたりするが、アプリケーション側でも工夫することでその効果はさらに高めることができる。

ここでは、OSの機能の一機能 "ファイルシステム"に注目して、それぞれの書き込みの違いを確認する。

目的

ファイルシステムの違いによって、次のような書き込み特性がどのように変化するかを調査する。

  • IO要求の発行回数
  • 書き込み総量
  • 書き込み頻度とオフセット

実行環境

Raspberry Pi 4 Model B (Raspberry Pi 4) は microSDカード経由でRaspberry Pi OSを起動させる。 また、Raspbery Pi 4 の USB3.0ポートにポータブルSSDを接続する。

計測環境の概要

ここで使用するRaspberry Pi 4のスペックについて、必要な情報だけ抜粋したものを下記に示す。

項目 概要
Board Raspberry Pi 4
CPU Cortex-A72 (ARM v8) 1.5GHz
メモリ 4GB LPDDR4-3200
OS Raspberry Pi OS (64 bit) (Feb 21st 2023)
kernel v5.15.92
I/O Scheduler mq-deadline
BCC Utilities bpfcc-tools version 0.18.0+ds-2
ext4 Utilities E2fsprogs version 1.46.2
xfs Utilities xfsprogs version 5.10.0-4
nilfs2 Utilities nilfs-tools version 2.2.8-1
f2fs Utilities f2fs-tools version 1.14.0-2
exfat Utilities exfatprogs version : 1.1.0-1
btrfs Utilities btrfs-progs version 5.10.1-2
jfs Utilities jfsutils version 1.1.15-5
fio fio-3.25-2
OS格納先ストレージ microSDHC 16GB Class10 UHS-1
計測用ストレージ SL-MG5

この実験では、 ポータブル SSD (SL-MG5) に対する書き込みを計測する。

計測方法

BPF Compiler Collection (BCC) を利用することで、ユーザプログラムから IO要求発行に任意の処理を追加し、IO要求の内容を確認する。

今回の計測では、BCCのサンプルスクリプトとして提供されている biosnoop を利用する

biosnoop は、IO要求発行(blk_mq_start_request)と IO完了(blk_account_io_done) における BIO のステータスを確認することができる。

このスクリプトを実行することで得られる書き込み先デバイス (DISK) と アクセス方向 (T) でフィルターをかける。
ここから、タイムスタンプ (TIME), オフセット (SECTOR)とサイズ (SIZE)を抽出する。

また、ストレージへの書き込みをする負荷プログラムとして fio を実行する

fioでは、合計 32GB となるように 以下の2パターンからデータを書き込みをする。

  1. 並列度1 でシーケンシャルな書き込みをする

     fio -directory=/mnt/fio-test -direct=0 -rw=write -bs=256k -size=32G -numjob=1 -invalidate=1 -fsync_on_close=1 -group_reporting
    
  2. 並列度8 でランダムな書き込みをする

     fio -directory=/mnt/fio-test -direct=0 -rw=randwrite -bs=256k -size=4G -numjob=8 -invalidate=1 -randseed=1 -fsync_on_close=1 -group_reporting
    

この実験では、これらのツールを活用して異なるファイルシステムに実行する。

毎計測時、デフォルトパラメータでmkfsコマンドの実行と、 echo 3 > /proc/sys/vm/drop_cachesによるキャッシュ解放を実施する。
biosnoopによる計測は、ファイルシステムをマウント(mount)してからアンマウント(umount)までとする。

その区間に負荷プログラム(fio)を実行し、そのときのIO要求のデータを使用する。

計測のタイムライン

準備

Linuxカーネルの再構築

BCCは、特定のカーネルコンフィグに依存しているが、Raspberry pi OS のデフォルトで無効となっている。

そこで、公式手順に基に、独自にカーネルのビルド・インストールを実施する。
カーネルコンフィグは、デフォルトのカーネルコンフィグから次のように修正する。

user@hostname:~/linux$ ./scripts/diffconfig .config.old .config
IKHEADERS n -> y

必要なパッケージのインストール

今回の計測するにあたって、Raspberry Pi OS にプリインストールされているパッケージのみでは不足している。

そこで、計測用にdebianパッケージを追加でインストールする。

pi@raspberrypi:~$ sudo apt install bpfcc-tools
pi@raspberrypi:~$ sudo apt install nilfs-tools exfatprogs xfsprogs jfsutils reiserfsprogs f2fs-tools btrfs-progs

BPFスクリプトの修正

debian (bullseye) が提供する bpfcc-toolsパッケージは、upstreamより古いバージョンとなっている。

今回使用しているバージョンには、BYTESの値で不適切となる不具合があったため、次のコミットをcherry-pickした。

github.com

また、今回の計測では短時間に多くのBIO情報が出力されるため、リングバッファ (page_cnt) のサイズも拡張しておく。

実験結果

IO要求の発行回数

biosnoop によって得られた結果から BYTES を抽出し、IO要求の発行回数をサイズごとに抽出した。

シーケンシャルな書き込み (並列度1) の場合は、次のような結果となった。

並列度1 で 32GBのデータを書き込みしたときのIO要求発行回数

この場合、それぞれのファイルシステムで大きな差は見られなかった。
また、それぞれのファイルシステムでは、fioによる書き込みサイズ 256K より大きいサイズで発行されていることも分かった。

一方で、ランダムな書き込み (並列度8) の場合は、次のような結果となった。

並列度8 で 32GBのデータを書き込みしたときのIO要求発行回数

この場合、先ほどのシーケンシャルな書き込みと比較していくつかの特徴がみられた。

  • f2fs と nilfs2 ファイルシステムは、シーケンシャルな書き込み(並列度1) とランダムな書き込み (並列度8) で似た傾向となった
  • ext4、xfs と btrfs ファイルシステムは、ランダムな書き込み (並列度8) では アプリケーションからの書き込みサイズ(256K) のIOが増えたため、IO総数も増えた
  • exfat と jfs ファイルシステムは、ランダムな書き込み (並列度8) では小さいIOに分割されて発行されたため、IO総数が増大した。

メタデータ書き込み総量

biosnoop によって得られた結果から BYTES を抽出し、その合計値を計算することでストレージへの書き込み総量を計算する。
さらに、ファイルの実データ (32GB) を減算することで、メタデータの書き込み総量を算出した。

  • Pattern 1: シーケンシャルな書き込み (並列度1)
  • Pattern 2: ランダムな書き込み (並列度8)

上記2パターンで計測したときのメタデータの書き込み総量は、次のようになった。 (単位は MB)

ext4 xfs nilfs2 f2fs exfat btrfs jfs
1 70.54 2.11 572.77 130.05 0.21 56.00 488.95
2 440.60 4.88 2296.04 414.20 32743.73 376.04 1148.64

ここから、メタデータによる書き込み総量の増加率を計算した結果、次のようになった。

ext4 xfs nilfs2 f2fs exfat btrfs jfs
1 +0.22% +0.01% +1.75% +0.40% +0.00% +0.17% +1.49%
2 +1.34% +0.01% +7.01% +1.26% +99.93% +1.15% +3.51%

この結果から、次のような特徴がみられた。

  • exfatを除いたファイルシステムは、並列度1から8にしたときにメタデータ書き込み総量は増加している。
    • 書き込み先ファイルが 1 から 8 に増えたため、メタデータの総量が増えるのは妥当であると思える。
  • exfatファイルシステムは、並列度1 ではメタデータ書き込み総量が最も少なかったが、並列度8になると膨大となった。
    • 並列度1の場合、exfatは連続したクラスタ確保 (NoFatChain属性) では、メタデータの書き込みを一部スキップできるのが要因と推測する。
    • 並列度8の場合、exfatの書き込み総量が急激に増加するのは、sparse fileに対応していないために、ランダムな書き込みで実データの書き込み回数が増えているのではないかと推測する。

書き込み頻度とオフセット

biosnoop によって得られた結果からTIME,SECTOR,BYTES を抽出することで、「いつ」「どこに」書き込みを実施したかを図示する。

シーケンシャルな書き込み(並列度1)を青色、ランダムな書き込み (並列度8) を水色で同時に図示している。

ext4ファイルシステムの書き込み先オフセット

ext4ファイルシステムは、次のようなストレージアクセスが見られた。

シーケンシャルな書き込み (並列度1)の場合、ジャーナルファイルとターゲットファイルの2つの直線が見られる。
その一方で、ランダムな書き込み(並列度8)の場合、書き込みが分散されているように見える。

xfsファイルシステムの書き込み先オフセット

xfsファイルシステムは、同じジャーナリングファイルシステムであるext4と傾向は似ている。

nilfs2ファイルシステムの書き込み先オフセット

nilfs2ファイルシステムは、ログ構造化ファイルシステムであるため、どちらのパターンにおいても直線となっている。

f2fsファイルシステムの書き込み先オフセット

f2fsファイルシステムも、ログ構造化ファイルシステムであるため、nilfs2と傾向は似ている。

exfatファイルシステムの書き込み先オフセット

exfatファイルシステムは、一定のところまで書いた後に断片化した書き込みが長時間続いている。

btrfsファイルシステムの書き込み先オフセット

btrfsファイルシステムでは、並列度に応じて書き込みが分散しているように見える。

jfsファイルシステムの書き込み先オフセット

jfsファイルシステムでは、ストレージデバイス全体を一定間隔に書き込みをしている。

パフォーマンス

memo

今回の実験では、Trim命令を明示的に発行していない。また、測定用にバックグラウンドプロセスがいくつか動いている。そのため、これらの値は参考値として算出した。

fio によって得られた結果にあるbwを抽出した。

  • Pattern 1: シーケンシャルな書き込み (並列度1)
  • Pattern 2: ランダムな書き込み (並列度8)

上記2パターンで計測したときの書き込み性能は、次のようになった。

fioによるパフォーマンス比較

ここでは、 f2fs と exfat ファイルシステムの特性について特筆する。

  • f2fs ファイルシステムは、並列かつランダムな書き込みであっても一定のパフォーマンスを出すことができる。
  • exfat ファイルシステムでは、並列かつランダムな書き込みが苦手であり、シーケンシャルな書き込みに比べて 20% ほどしかパフォーマンスが出ていない。

おわりに

本記事では、ファイルシステムの違いによるストレージへの書き込みについて以下の3点に着目して計測した。

  • IO要求の発行回数
  • メタデータ書き込み総量
  • 書き込み頻度とオフセット

計測の結果、アプリケーションの書き込みパターンがこれらに大きく影響するような結果となった。

また、今回は対象外としたが、ファイルシステムによってはデータ完全性を保障したり、ユーザビリティを向上させるような機能がある。
さらに、今回はデフォルトのパラメータで計測したが、ファイルシステムによってはmkfsのオプションやmountのオプションでデバイスに最適化した書き込みができるようになる。

そのため、今回のデータのみではファイルシステムの優劣をつけることはできず、それぞれのユースケースに沿ってファイルシステムを選定することが必要となってくる

変更履歴

  • 2023/3/22: 記事公開

参考

Linuxカーネルのファイルアクセスの処理を追いかける (14) mq-deadline

関連記事

概要

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

本章では、mq-deadline I/Oスケジューラの概要紹介と初期処理(init_sched/init_hctx)・解放処理(exit_sched)を確認した。

はじめに

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

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

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

mq-deadlineの概要

mq-deadline I/Oスケジューラは、deadline I/Oスケジューラをマルチキュー向けに作り直されたI/Oスケジューラである。

deadline (mq-deadline) I/Oスケジューラでは、現在処理中のリクエストから近いI/Oリクエストから順に処理をしていくが、deadlineを過ぎているリクエストは優先して処理される。

mq-deadline I/Oスケジューラでは、セクタ位置をキーとする赤黒木とリクエスト挿入順を保持するFIFOのデータ構造を持ち、これらがReadとWriteの2種類が用意される。
さらに、mq-deadlineでは I/O優先度RT, BE, IDLEがサポートされており、優先度の高いリクエストがすべて終了した後に、低い優先度の要求がdispatchされることが保証されている。

www.spinics.net

mq-deadlineスケジューラの概略図

mq-deadline I/Oスケジューラでは、同期リクエストのためにリクエストキューの25%を確保している。(後述するasync_depthで変更可能)

lwn.net

そして、ReadリクエストはWriteリクエストより優先されて処理される。

mq-deadlineのパラメータ

mq-deadline I/Oスケジューラでは、いくつかのパラメータをsysfs経由で設定・確認することができる。

    # ls /sys/block/mmcblk0/queue/iosched
    async_depth     front_merges    write_expire
    fifo_batch      read_expire     writes_starved
  • read_expire: Readリクエストのdeadline (ミリ秒)
  • write_expire: Writeリクエストのdeadline (ミリ秒)
  • fifo_batch: FIFOデータ構造のサイズ (リクエスト数)
  • writes_starved: Readリクエストのディスパッチ回数に対するWriteリクエストのディスパッチ回数の優先回数(ディスパッチ数)
  • front_merges: フロントマージの有効/無効化 (Bool値)
  • async_depth: 非同期要求が占有できるリクエストキュー (リクエスト数)

mq-deadlineとタイムライン

また、debugfsからはmq-deadline I/Oスケジューラのステータスを確認することができる。

    # ls /sys/kernel/debug/block/mmcblk0/sched/
    async_depth       owned_by_driver   read1_next_rq     write0_next_rq
    batching          queued            read2_fifo_list   write1_fifo_list
    dispatch0         read0_fifo_list   read2_next_rq     write1_next_rq
    dispatch1         read0_next_rq     starved           write2_fifo_list
    dispatch2         read1_fifo_list   write0_fifo_list  write2_next_rq

ここで、0RT, 1BE, 2IDLEを表している。

mq-deadlineとステータス

deadline_data構造体

Linux の IO scheduler elevetor_queueelevetor_dataに各IO scheduler固有のデータ構造を設定することができる。

mq-deadlineでは、deadline_dataが設定される。

// 79:
struct deadline_data {
    /*
    * run time data
    */

    struct dd_per_prio per_prio[DD_PRIO_COUNT];

    /* Data direction of latest dispatched request. */
    enum dd_data_dir last_dir;
    unsigned int batching;        /* number of sequential requests made */
    unsigned int starved;     /* times reads have starved writes */

    struct io_stats __percpu *stats;

    /*
    * settings that change how the i/o scheduler behaves
    */
    int fifo_expire[DD_DIR_COUNT];
    int fifo_batch;
    int writes_starved;
    int front_merges;
    u32 async_depth;

    spinlock_t lock;
    spinlock_t zone_lock;
};

mq-deadlineでは、リクエストをREADWRITEに分類し、それぞれを別のキュー(赤黒木)で管理する。
ここでは、dir (direction)として扱われ、dd_data_dirで定義されている。

// 38:
enum dd_data_dir {
    DD_READ     = READ,
    DD_WRITE    = WRITE,
};

enum { DD_DIR_COUNT = 2 };

mq-deadlineではリクエストを、dd_per_prio構造体にある キュー(赤黒木)で管理する。
この構造体は、各I/O優先度に用意される。

// 71:
struct dd_per_prio {
    struct list_head dispatch;
    struct rb_root sort_list[DD_DIR_COUNT];
    struct list_head fifo_list[DD_DIR_COUNT];
    /* Next request in FIFO order. Read, write or both are NULL. */
    struct request *next_rq[DD_DIR_COUNT];
};

mq-deadlineの関数群

ここでは、初期処理(init_sched/init_hctx)・解放処理(exit_sched)の関数のみ注目する。

dd_init_sched

dd_init_sched関数は、elevetor_typeにあるopsinit_schedに設定され、blk_mq_init_sched関数から呼び出される関数となっている。

init_schedではrequest_queue (I/Oスケジューラ固有のelevator_dataを含む) の確保と request_queue->elevatorへの設定をする。

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

// 552:
static int dd_init_sched(struct request_queue *q, struct elevator_type *e)
{
    struct deadline_data *dd;
    struct elevator_queue *eq;
    enum dd_prio prio;
    int ret = -ENOMEM;

    eq = elevator_alloc(q, e);
    if (!eq)
        return ret;

    dd = kzalloc_node(sizeof(*dd), GFP_KERNEL, q->node);
    if (!dd)
        goto put_eq;

    eq->elevator_data = dd;

    dd->stats = alloc_percpu_gfp(typeof(*dd->stats),
                     GFP_KERNEL | __GFP_ZERO);
    if (!dd->stats)
        goto free_dd;

    for (prio = 0; prio <= DD_PRIO_MAX; prio++) {
        struct dd_per_prio *per_prio = &dd->per_prio[prio];

        INIT_LIST_HEAD(&per_prio->dispatch);
        INIT_LIST_HEAD(&per_prio->fifo_list[DD_READ]);
        INIT_LIST_HEAD(&per_prio->fifo_list[DD_WRITE]);
        per_prio->sort_list[DD_READ] = RB_ROOT;
        per_prio->sort_list[DD_WRITE] = RB_ROOT;
    }
    dd->fifo_expire[DD_READ] = read_expire;
    dd->fifo_expire[DD_WRITE] = write_expire;
    dd->writes_starved = writes_starved;
    dd->front_merges = 1;
    dd->last_dir = DD_WRITE;
    dd->fifo_batch = fifo_batch;
    spin_lock_init(&dd->lock);
    spin_lock_init(&dd->zone_lock);

    q->elevator = eq;
    return 0;

free_dd:
    kfree(dd);

put_eq:
    kobject_put(&eq->kobj);
    return ret;
}

dd_init_sched関数では、I/O優先度毎にリスト(赤黒木)と各パラメータを初期化する。

dd_init_hctx

dd_init_hctx関数は、elevetor_typeにあるopsinit_hctxに設定され、blk_mq_init_sched関数から呼び出される関数となっている。

init_hctxではHardware dispatch queuesをI/Oスケジューラに向けて初期化する。

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

// 526:
static int dd_init_hctx(struct blk_mq_hw_ctx *hctx, unsigned int hctx_idx)
{
    dd_depth_updated(hctx);
    return 0;
}

dd_init_hctx関数は、後述するdd_depth_updated関数を呼び出す。

dd_depth_updated

dd_depth_updated関数は、elevetor_typeにあるopsdepth_updatedに設定され、blk_mq_update_nr_requests関数から呼び出される関数となっている。

depth_updatedでは、リクエストキューの深さが変化したときにI/Oスケジューラの内部情報を更新することができる。

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

// 514:
static void dd_depth_updated(struct blk_mq_hw_ctx *hctx)
{
    struct request_queue *q = hctx->queue;
    struct deadline_data *dd = q->elevator->elevator_data;
    struct blk_mq_tags *tags = hctx->sched_tags;

    dd->async_depth = max(1UL, 3 * q->nr_requests / 4);

    sbitmap_queue_min_shallow_depth(tags->bitmap_tags, dd->async_depth);
}

mq-deadline I/Oスケジューラでは、非同期リクエストが占有できる割合をdd->async_depthに設定することができる。

ただし、最低でも25% ({\frac{1}{4}}) は同期リクエストのために確保されているので、dd->async_depthに設定できる値は高々75% ({\frac{3}{4}})となる。

dd_exit_sched

dd_exit_sched関数は、elevetor_typeにあるopsexit_schedに設定され、blk_mq_exit_sched関数から呼び出される関数となっている。

exit_schedではI/Oスケジューラ周りのエラーハンドリングやI/Oスケジューラの変更により、データ構造を解放する。

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

// 532:
static void dd_exit_sched(struct elevator_queue *e)
{
    struct deadline_data *dd = e->elevator_data;
    enum dd_prio prio;

    for (prio = 0; prio <= DD_PRIO_MAX; prio++) {
        struct dd_per_prio *per_prio = &dd->per_prio[prio];

        WARN_ON_ONCE(!list_empty(&per_prio->fifo_list[DD_READ]));
        WARN_ON_ONCE(!list_empty(&per_prio->fifo_list[DD_WRITE]));
    }

    free_percpu(dd->stats);

    kfree(dd);
}

おわりに

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

変更履歴

  • 2023/02/27: 記事公開

参考

Buildroot で作成した ARM64環境で xfstests を実行する

概要

xfstestsは複数のパッケージと依存関係があるが、Buildrootを使うことで容易に設定することができる。
Buildrootですでに提供されているパッケージと、独自で修正したものを利用してxfstestsを実行できる環境を作成した。

Buildrootの修正については次のブランチにコミットしてある。

github.com

はじめに

xfstestsはファイルシステム回帰テストのテストスイートである。 xfstestsでは、Linuxファイルシステム(xfs, ext2, ext4, cifs, btrfs, f2fs, reiserfs, gfs, jfs, udf, nfs, and tmpfs)でも使用することができ、多くのLinuxファイルシステムを開発しているメンテナは、本流にマージする前にxfstestsのテストを実施している。

一方で、BuildRootは、ビルド環境・実行環境を構築するツールである。
このBuildrootでは、パッケージの追加やユーザランドのカスタマイズがすることができる。

そこで、xfstestsを実行するために必要な環境をBuildrootで作成する手順を確認してみる。

実験環境

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

環境 概要
CPU AMD Ryzen 3 3300X
RAM DDR4-2666 16GB ×2
Host OS Ubuntu Desktop 22.04.1
kernel v5.15.0-56-generic
Storage SL-MG5
File-System ext4
Buildroot 2022.11
xfstests v2022.11.27

開発用PC には、Buildrootで構築した開発環境 (Buildenv) と テスト実施環境 (Targetenv) があり、virtio-scsiでストレージデバイスとつながっている。

実行環境の概略図

aarch64(virt) のNFSroot実行環境を構築する

  1. テスト用のストレージの準備 (このストレージ内のデータは消去されるので注意)

     leava@kbuild:/work$ export TARGET_DEVICE=/dev/disk/by-id/usb-Sony_Storage_C2JBB2040599-0:0
     leava@kbuild:/work$ sudo parted ${TARGET_DEVICE} --script \
                         'mklabel gpt mkpart primary 0% 25% mkpart primary 25% 50% mkpart primary 50% 75% mkpart primary 75% 100% print quit'
    
  2. ホスト環境に nfsd を設定する

     leava@kbuild:/work$ export NFSROOT="/srv/nfsroot/arm64/buildroot"
     leava@kbuild:/work$ sudo exportfs -av
     leava@kbuild:/work$ exporting 127.0.0.1:/srv/nfsroot
     leava@kbuild:/work$ sudo mkdir -p ${NFSROOT}
    
  3. Buildrootを入手する。

     leava@kbuild:/work$ git clone https://github.com/buildroot/buildroot.git
     leava@kbuild:/work$ cd buildroot
     leava@kbuild:/work/buildroot$ export BUILDROOT_DIR="/work/buildroot"
     leava@kbuild:/work/buildroot$ git checkout 2022.11
    
  4. Buildrootのデフォルトの設定を使用する。

     leava@kbuild:/work/buildroot$ make qemu_aarch64_virt_defconfig
    
  5. Buildrootの設定からユーザランドを構築する。

     leava@kbuild:/work/buildroot$ make
     leava@kbuild:/work/buildroot$ cd ..
    
  6. 生成されたルファイルシステムを展開する

     leava@kbuild:/work$ sudo tar xf buildroot/output/images/rootfs.tar.xz -C /srv/nfsroot/arm64/buildroot
    
  7. aarch64(virt)の環境を起動させるスクリプトを用意する。

// 1:
#!/bin/sh
(
BINARIES_DIR="${BUILDROOT_DIR}/output/images/"
TARGET_ROOTFS="/dev/nfs"
EXTRA_CMDLINE="nfsroot=${NFSROOT},vers=3,tcp ip=on"
CMDLINE="console=ttyAMA0 rootwait root=${TARGET_ROOTFS} rw ${EXTRA_CMDLINE}"
cd ${BINARIES_DIR}

if [ "${1}" = "serial-only" ]; then
    EXTRA_ARGS='-nographic'
else
    EXTRA_ARGS=''
fi

export PATH="${BUILDROOT_DIR}/output/host/bin:${PATH}"
exec qemu-system-aarch64 \
        -M virt \
        -cpu cortex-a53 \
        -smp 4 \
        -m 4096 \
        -kernel Image \
        -append "${CMDLINE}" \
        -device virtio-scsi-pci,id=scsi0 \
        -device scsi-hd,drive=drive0,bus=scsi0.0,channel=0,scsi-id=0,lun=0 \
        -drive file=${TARGET_DEVICE}"-part1",if=none,format=raw,id=drive0 \
        -device scsi-hd,drive=drive1,bus=scsi0.0,channel=0,scsi-id=1,lun=0 \
        -drive file=${TARGET_DEVICE}"-part2",if=none,format=raw,id=drive1 \
        -device scsi-hd,drive=drive2,bus=scsi0.0,channel=0,scsi-id=2,lun=0 \
        -drive file=${TARGET_DEVICE}"-part3",if=none,format=raw,id=drive2 \
        -device scsi-hd,drive=drive3,bus=scsi0.0,channel=0,scsi-id=3,lun=0 \
        -drive file=${TARGET_DEVICE}"-part4",if=none,format=raw,id=drive3 \
        ${EXTRA_ARGS} -s
)

このファイルをPATHの通っているディレクトリに配置しておき、実行権限もつけておく。

xfstestsのビルドに向けてのセットアップ

前章で作成したビルド環境・ターゲットファイルシステムでは、xfstestsをビルド・テスト実行することができない。
そこで、ビルドや実行に必要なライブラリ・バイナリをインストールしていく。

ただし、xfsprogsのライブラリに関しては、既存のBuildRootではインストールすることができないため、修正を加える必要がある。

Buildrootのパッケージビルド設定を新規に追加する

xfstestsをビルドするためには、xfsprogs開発用ライブラリがビルド環境にインストールされている必要がある。
ただし、既存のBuildrootは、ターゲットファイルシステムにxfsprogsパッケージのビルド設定のみ用意されている。 (ライブラリのインストールはされない)

そこで、既存のxfsprogsのビルド設定をコピーして新規に libxfsprogsとしてビルド設定を作成していく。

  1. 既存のxfsprofsのビルド設定をコピーする。

     leava@kbuild:/work$ cd ${BUILDROOT_DIR}
     leava@kbuild:/work/buildroot$ cp -r package/xfsprogs/ package/libxfsprogs
    
  2. 変数名XFSPROGSLIBXFSPROGSに変更する。

     leava@kbuild:/work/buildroot$ sed -i 's/XFSPROGS_/LIBXFSPROGS_/gI' package/libxfsprogs/Config.in
     leava@kbuild:/work/buildroot$ sed -i 's/XFSPROGS_/LIBXFSPROGS_/gI' package/libxfsprogs/xfsprogs.mk
    
  3. ファイル名 xfsprogs.mklibxfsprogs.mkに変更する

     leava@kbuild:/work/buildroot$ mv package/libxfsprogs/xfsprogs.mk package/libxfsprogs/libxfsprogs.mk
    
  4. libxfsprogsのビルド設定を読み込むように変更する。

     leava@kbuild:/work/buildroot$ cat << EOF >> package/Config.in
     menu "For xfstests library"
    
             source "package/libxfsprogs/Config.in"
    
     endmenu
     EOF
    
  5. ライブラリをビルド環境にインストールするように更新する

     leava@kbuild:/work/buildroot$ cat << EOF >> package/libxfsprogs/libxfsprogs.mk
     LIBXFSPROGS_INSTALL_STAGING = YES
     LIBXFSPROGS_INSTALL_TARGET = NO
     define LIBXFSPROGS_INSTALL_STAGING_CMDS
             \$(TARGET_MAKE_ENV) \$(MAKE) -C \$(@D) DIST_ROOT=\$(STAGING_DIR) install; \
             \$(TARGET_MAKE_ENV) \$(MAKE) -C \$(@D) DIST_ROOT=\$(STAGING_DIR) install-dev
     endef
     EOF
    

これにより、make menuconfigFor xfstests libraryのセクションが追加される。
このセクションにある xfsprogsを有効にすることで、xfsprogs開発ライブラリがビルド環境にインストールされる。

Buildrootの設定をアップデートする

xfsprogs開発ライブラリ以外にもxfstestsをビルドするために必要なライブラリを追加する。

  1. xfstestsに必要なパッケージをBuildrootで生成するようにconfigを修正する。

     leava@kbuild:/work/buildroot$ make menuconfig
    
     Toolchain  --->
        C library (glibc)  --->
      Target packages  --->
        Libraries  --->
          Other 
            [*]   liburcu
          Text and terminal handling  --->
            [*] inih
        System tools  --->
          [*] acl
          [*] util-linux  ---> 
            [*]   libblkid
            [*]   libuuid
      For xfstests  ---> 
        [*] xfsprogs
    
  2. Buildrootで再度ビルドし、ビルド環境をアップデートする

     leava@kbuild:/work/buildroot$ make
    
  3. 開発用ライブラリが期待通りインストールされているか確認する

     leava@kbuild:/work/buildroot$ ls output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/libacl.la output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/*.la
     output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/libacl.la          output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/libpcre2-posix.la
     output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/libacl.la          output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/libtirpc.la
     output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/libattr.la         output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/liburcu-bp.la
     output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/libblkid.la        output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/liburcu-cds.la
     output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/libf2fs.la         output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/liburcu-common.la
     output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/libf2fs_format.la  output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/liburcu-mb.la
     output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/libgdbm.la         output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/liburcu-memb.la
     output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/libhandle.la       output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/liburcu-qsbr.la
     output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/libkmod.la         output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/liburcu-signal.la
     output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/liblzma.la         output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/liburcu.la
     output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/libpcre2-8.la      output/host/aarch64-buildroot-linux-gnu/sysroot/usr/lib/libuuid.la     
    
     leava@kbuild:/work/buildroot$ ls output/host/aarch64-buildroot-linux-gnu/sysroot/usr/include/xfs/xfs.h
     output/host/aarch64-buildroot-linux-gnu/sysroot/usr/include/xfs/xfs.h
    

これによって、sysroot以下に/usr/lib/libacl.la, /usr/lib/libuuid.la, /usr/include/xfs/xfs.hが追加される。

xfstestsのテスト実行に向けてのセットアップ

ここまででxfstestsはビルドできるようになったが、大半のテストはパッケージ不足によりskipされてしまう。
そこで、ビルド環境とターゲットファイルシステムに必要なパッケージを追加でインストールしていく。

xfstestsのビルドに必要なライブラリをインストールする

    leava@kbuild:/work/buildroot$ make menuconfig

    Target packages  --->
      Libraries
        Compressors and decompressors 
          [*] lz4
          [*]   install programs
        Database
          [*] gdbm
        Hardware handling
          [*] libaio 
        Other 
          [*] libcap
          [*]   install tools     
          [*] liburing
        Security
          [*] libselinux
Buildrootの既存パッケージのビルド設定を修正する

xfstestsの一部のテストはndbm開発ライブラリが必要になる。
このライブラリは、GNU dbm (gdbm)パッケージが互換性を持っており、BuildRootはこのパッケージのビルドを用意している。

しかし、gdbmをビルドするとき(./configure) に、--enable-libgdbm-compat を指定していないと、このライブラリを用意されず、BuildRootのデフォルトの設定ではこれが提供されていない。

  1. gdbmパッケージのビルド設定を修正する

     leava@kbuild:/work/buildroot$ echo "GDBM_CONF_OPTS = --enable-libgdbm-compat" >> package/gdbm/gdbm.mk
    

これによって、sysroot以下に/usr/include/ndbm.hが追加される。

xfstestsの必須パッケージをターゲットファイルシステムにインストールする

    leava@kbuild:/work/buildroot$ make menuconfig

    Target packages  --->
      [*]   Show packages that are also provided by busybox
      Development tools
        [*] grep
      Filesystem and flash utilities
        [*] xfsprogs
      Interpreter languages and scripting
        [*] perl
      Shell and utilities
        [*] bash 
      System tools  --->
        [*] coreutils
        [*] kmod
        [*]   kmod utilities
        -*- util-linux  --->
          [*]   basic set

xfstestsの推奨パッケージをターゲットファイルシステムにインストールする

Buildrootのパッケージビルド設定を新規に追加する

xfstestsでは、fs-verityを利用するテストが存在している。 ただし、既存のBuildrootは、ターゲットファイルシステムにfsverity-utilsパッケージのビルド設定は用意されていない。

そこで、新規に fsverity-utils のビルド設定を作成していく。

  1. fsverity-utilsのディレクトリを追加する

     leava@kbuild:/work/buildroot$ mkdir package/fsverity-utils
    
  2. libxfsprogsのビルド設定を読み込むように変更する。

     leava@kbuild:/work/buildroot$ cat << EOF > package/fsverity-utils/Config.in
     config BR2_PACKAGE_FSVERITY_UTILS
             bool "fsverity-utils"
             select BR2_PACKAGE_OPENSSL
             help
               A set of userspace utilities for fs-verity.
    
               git://git.kernel.org/pub/scm/linux/kernel/git/ebiggers/fsverity-utils.git
     EOF
    
  3. fsverity-utilsのビルド設定を読み込むように変更する。

     leava@kbuild:/work/buildroot$ sed -i -e 's/fxload/source "package\/fsverity-utils\/Config.in/g" package/Config.in
    
  4. libxfsprogsのビルド設定を読み込むように変更する。

     leava@kbuild:/work/buildroot$ cat << EOF > package/fsverity-utils/fsverity-utils.hash
     cat package/fsverity-utils/fsverity-utils.hash
     # Locally computed
     sha256  830e38ec081ef8171eb210461cf8bee8a707c7c60f9018a4b567af145a510884  fsverity-utils-1.5.tar.gz
     sha256  b03d4d3e8cdb7011013f8e954d4b0b72d649b8f9c60298488d0df03f753e8524  README.md
     EOF
    
  5. ライブラリをビルド環境にインストールするように更新する

     leava@kbuild:/work/buildroot$ cat << EOF >> package/fsverity-utils/fsverity-utils.mk
     ################################################################################
     #
     # fsverity-utils
     #
     ################################################################################
    
     FSVERITY_UTILS_VERSION = 1.5
     FSVERITY_UTILS_SITE = https://git.kernel.org/pub/scm/linux/kernel/git/ebiggers/fsverity-utils.git/snapshot
     FSVERITY_UTILS_DEPENDENCIES = libopenssl
     FSVERITY_UTILS_LICENSE = Copyright 2019 the fsverity-utils authors
     FSVERITY_UTILS_LICENSE_FILES = LICENSE
    
     #PKG_CONFIG_PATH="$(STAGING_DIR)/usr/lib/pkgconfig"
    
     FSVERITY_UTILS_ENV = \
                     PKGCONF=$(HOST_DIR)/bin/pkg-config \
                     CC=$(HOST_DIR)/bin/aarch64-buildroot-linux-gnu-gcc \
                     GCC=$(HOST_DIR)/bin/aarch64-buildroot-linux-gnu-gcc
    
     define FSVERITY_UTILS_BUILD_CMDS
             $(TARGET_MAKE_ENV) $(MAKE) -C $(@D) $(FSVERITY_UTILS_ENV)
     endef
    
     define FSVERITY_UTILS_INSTALL_TARGET_CMDS
             $(TARGET_MAKE_ENV) $(MAKE) -C $(@D) DESTDIR="$(TARGET_DIR)" $(FSVERITY_UTILS_ENV) install
     endef
    
     $(eval $(generic-package))
     EOF
    
Buildrootの設定をアップデートする
    leava@kbuild:/work/buildroot$ make menuconfig

    Toolchain
      [*] Install glibc utilities
    Target packages  --->
      Compressors and decompressors 
        [*] xz-utils
      Debugging, profiling and benchmark  --->
        [*] fio 
      Development tools
        [*] gawk
      Filesystem and flash utilities
        [*] e2fsprogs  --->
          [*]   debugfs 
          [*]   e2image
          [*]   e4defrag
          [*]   resize2fs
      Hardware handling
          [*] fsverity-utils
          [*] lvm2 & device mapper            
      System tools  --->
        [*] keyutils
        [*] quota
        [*] quotatool
        -*- util-linux  --->
          [*]   losetup

Buildrootのターゲットファイルシステムのセットアップ

xfstestsには、実行環境に依存するテストも存在する。
その中にある「cgroup2のマウント」と「ユーザアカウントの追加」をBuildrootで生成するターゲットファイルシステムに反映させる。

Buildrootのターゲットファイルシステムのオーバーレイ

Buildrootには、ターゲットファイルシステムの内容の一部を上書きする仕組みとして「オーバーレイ」がある。
ここでは、オーバーレイの仕組みで/etc/fstabを書き換えることで、cgropu2をマウントするように変更する。

また、xfstestsで必要となるマウントポイント /mnt/test/mnt/scratch もオーバーレイの仕組みを用いて作成する。

  1. aarch64(virt)用のoverlayディレクトリを作成する

     leava@kbuild:/work/buildroot$ mkdir -p board/qemu/aarch64-virt/rootfs_overlay
     leava@kbuild:/work/buildroot$ mkdir -p board/qemu/aarch64-virt/rootfs_overlay/etc
    
  2. ターゲットファイルシステム/etc/fstabを自前の/etc/fstabに置き換える

     leava@kbuild:/work/buildroot$ cat << EOF > board/qemu/aarch64-virt/rootfs_overlay/etc/fstab
     # <file system> <mount pt>      <type>  <options>       <dump>  <pass>
     /dev/root       /               ext2    rw,noauto       0       1
     proc            /proc           proc    defaults        0       0
     devpts          /dev/pts        devpts  defaults,gid=5,mode=620,ptmxmode=0666   0       0
     tmpfs           /dev/shm        tmpfs   mode=0777       0       0
     tmpfs           /tmp            tmpfs   mode=1777       0       0
     tmpfs           /run            tmpfs   mode=0755,nosuid,nodev  0       0
     sysfs           /sys            sysfs   defaults        0       0
     none            /sys/fs/selinux selinuxfs       noauto  0       0
     none            /sys/fs/cgroup  cgroup2 defaults        0       0        
    
  3. ターゲットファイルシステム/mnt/mnt/test/mnt/scratchを追加する

     leava@kbuild:/work/buildroot$ mkdir -p board/qemu/aarch64-virt/rootfs_overlay/mnt/test
     leava@kbuild:/work/buildroot$ mkdir -p board/qemu/aarch64-virt/rootfs_overlay/mnt/scratch
    
  4. overlayのパスを指定する

     leava@kbuild:/work/buildroot$ make menuconfig
    
     System configuration  ---> 
     (board/qemu/aarch64-virt/rootfs_overlay) Root filesystem overlay directories
    
Buildrootのユーザアカウントの追加

Buildrootには、makeusers文法の則ったファイルを配置することで、ユーザを追加することができる。
ここでは、テストで必要となるfsgqa, 123456-fsgqa, fsgqa2ユーザを追加する。

  1. aarch64(virt)用のoverlayディレクトリを作成する

     leava@kbuild:/work/buildroot$ mkdir -p board/qemu/aarch64-virt
    
  2. ターゲットファイルシステム/etc/fstabを自前の/etc/fstabに置き換える

     leava@kbuild:/work/buildroot$ cat << EOF > board/qemu/aarch64-virt/users.txt
     fsgqa -1 fsgqa -1 = /home/fsgqa /bin/sh - xfstests User1
     123456-fsgqa -1 123456-fsgqa -1 = - /bin/sh - xfstests User2
     fsgqa2 -1 fsgqa2 -1 = /home/fsgqa2 /bin/sh - xfstests User3        
    
  3. overlayのパスを指定する

     leava@kbuild:/work/buildroot$ make menuconfig
    
     System configuration  ---> 
     (board/qemu/aarch64-virt/users.txt) Path to the users tables
    
NFSrootのアップデート

xfstestsを実行できるビルド環境とターゲットファイルシステムが生成できたので、NFSrootの環境をアップデートする。

  1. 以前のターゲットファイルシステムを退避させる

     leava@kbuild:/work/buildroot$ mv ${NFSROOT} ${NFSROOT}`date +%Y%m%d%H%M`
     leava@kbuild:/work/buildroot$ mkdir ${NFSROOT}
    
  2. 新規のターゲットファイルシステムを展開する

      leava@kbuild:/work/buildroot$ sudo tar xf output/images/rootfs.tar.xz -C ${NFSROOT}
    
  3. xfstestsの設定をアップデートする

     leava@kbuild:/work/buildroot$ cat << EOF > board/qemu/aarch64-virt/users.txt
     export TEST_DEV=/dev/sda
     export TEST_DIR=/mnt/test
     export SCRATCH_DEV=/dev/sdb
     export SCRATCH_MNT=/mnt/scratch
     export FSTYP=ext4
     export LOGWRITES_DEV=/dev/sdc
     export SCRATCH_LOGDEV=/dev/sdd
     export USE_EXTERNAL=yes
    

xfstestsをクロスコンパイルする

Buildrootで生成したビルド環境を使ってxfstestsのビルドを実施する。

  1. xfstestsのgit repositoryを開発PCにcloneする。

     leava@kbuild:/work/buildroot$ cd ..
     leava@kbuild:/work$ git clone git://git.kernel.org/pub/scm/fs/xfs/xfstests-dev.git
     leava@kbuild:/work$ cd xfstests-dev
    
  2. ロスコンパイルのために環境変数を設定する。

     leava@kbuild:/work/xfstests-dev$ export CROSSENV="/work/buildroot/output/host"
     leava@kbuild:/work/xfstests-dev$ export CROSS_COMPILE=aarch64-buildroot-linux-
     leava@kbuild:/work/xfstests-dev$ export SDKTARGETSYSROOT="${CROSSENV}/${CROSS_COMPILE}gnu/sysroot"
     leava@kbuild:/work/xfstests-dev$ export PATH="${CROSSENV}/bin:${CROSSENV}/sbin:$PATH"
     leava@kbuild:/work/xfstests-dev$ export AR="${CROSSENV}/bin/aarch64-buildroot-linux-gnu-gcc-ar"
     leava@kbuild:/work/xfstests-dev$ export AS="${CROSSENV}/bin/aarch64-buildroot-linux-gnu-as"
     leava@kbuild:/work/xfstests-dev$ export LD="${CROSSENV}/bin/aarch64-buildroot-linux-gnu-ld"
     leava@kbuild:/work/xfstests-dev$ export NM="${CROSSENV}/bin/aarch64-buildroot-linux-gnu-gcc-nm"
     leava@kbuild:/work/xfstests-dev$ export CC="${CROSSENV}/bin/aarch64-buildroot-linux-gnu-gcc"
     leava@kbuild:/work/xfstests-dev$ export GCC="${CROSSENV}/bin/aarch64-buildroot-linux-gnu-gcc"
     leava@kbuild:/work/xfstests-dev$ export CPP="${CROSSENV}/bin/aarch64-buildroot-linux-gnu-cpp"
     leava@kbuild:/work/xfstests-dev$ export CXX="${CROSSENV}/bin/aarch64-buildroot-linux-gnu-g++"
     leava@kbuild:/work/xfstests-dev$ export FC="${CROSSENV}/bin/aarch64-buildroot-linux-gnu-gfortran"
     leava@kbuild:/work/xfstests-dev$ export F77="${CROSSENV}/bin/aarch64-buildroot-linux-gnu-gfortran"
     leava@kbuild:/work/xfstests-dev$ export RANLIB="${CROSSENV}/bin/aarch64-buildroot-linux-gnu-gcc-ranlib"
     leava@kbuild:/work/xfstests-dev$ export READELF="${CROSSENV}/bin/aarch64-buildroot-linux-gnu-readelf"
     leava@kbuild:/work/xfstests-dev$ export STRIP="${CROSSENV}/bin/aarch64-buildroot-linux-gnu-strip"
     leava@kbuild:/work/xfstests-dev$ export OBJCOPY="${CROSSENV}/bin/aarch64-buildroot-linux-gnu-objcopy"
     leava@kbuild:/work/xfstests-dev$ export OBJDUMP="${CROSSENV}/bin/aarch64-buildroot-linux-gnu-objdump"
     leava@kbuild:/work/xfstests-dev$ export PKG_CONFIG="${CROSSENV}/bin/pkg-config"
     leava@kbuild:/work/xfstests-dev$ export PKGCONF="${CROSSENV}/bin/pkg-config"
    
  3. xfstestsをビルド環境のセットアップする

     leava@kbuild:/work/xfstests-dev$ ./configure \
                               --target=aarch64-buildroot-linux-gnu \
                               --host=aarch64-buildroot-linux-gnu \
                               --build=x86_64-pc-linux-gnu \
                               --prefix=${SDKTARGETSYSROOT}/usr \
                               --exec-prefix=${NFSROOT} \
                               --program-prefix=""\
                               INSTALL_USER=root \
                               INSTALL_GROUP=root \
                               --enable-static
    
  4. xfstestsをクロスビルドする

     leava@kbuild:/work/xfstests-dev$ make
    
  5. ターゲットファイルシステム (NFSroot) 以下に xfstestsをインストールする

     leava@kbuild:/work/xfstests-dev$ make install
    

テスト実施

xfstestsはターゲットファイルシステム/xfstestsにインストールされている。
その環境で ./check スクリプトを実行することでファイルシステムのテストが実施される。

  1. Buildrootで作成したテスト実施環境を起動させる

     leava@kbuild:/work/xfstests-dev$ sudo start-qemu-arm64.sh serial-only
    
  2. インストールしたxfstestsディレクトリに移動する

     buildroot login: root
     # cd /xfstests/
    
  3. テストを実施する

     # ./check -g quick
    

ただし、いくつかのテスト(ext4/058, ext4/059など) は BUG_ON で例外が発生するため注意。

おわりに

本記事では、xfstestsをビルド・実行できる環境をBuildrootで生成した。
ただし、いくつかの設定 (パッケージの追加やターゲット環境の修正) は Buildrootのデフォルトでは対応できなかったため、Buildrootに修正を加えた。 (下記 repository を参照)

github.com

変更履歴

  • 2022/12/18: 記事公開

参考文献

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: 記事公開

参考