LeavaTailの日記

LeavaTailの日記

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

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

関連記事

概要

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

本章では、リクエストをディスパッチがカーネルスレッドとして起動するところから、I/Oスケジューラからディスパッチする処理の直前 (__blk_mq_sched_dispatch_requests )までを確認した。

はじめに

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

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

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

kblockd の概要

Linux v5.15 の blk-mqでは、Hardware dispatch queues へのディスパッチを実現するためにもWork Queueと呼ばれる機構を用いている。

dispatch用のワーカスレッドはblock共通kblockdを利用する。

kblockd の workqueue

このWork Queueでは、専用API kblockd_schedule_work または kblockd_mod_delayed_work_on 関数を通して、タスクを追加することができる。

// 1619:
int kblockd_schedule_work(struct work_struct *work)
{
    return queue_work(kblockd_workqueue, work);
}
EXPORT_SYMBOL(kblockd_schedule_work);

int kblockd_mod_delayed_work_on(int cpu, struct delayed_work *dwork,
                unsigned long delay)
{
    return mod_delayed_work_on(cpu, kblockd_workqueue, dwork, delay);
}
EXPORT_SYMBOL(kblockd_mod_delayed_work_on);
  • kblockd_schedule_work: 引数で設定されたworkを、kblockdに追加する
  • kblockd_mod_delayed_work_on: 引数で指定されたworkを、指定されたCPUで遅延実行する。

block用のワーカスレッドの作成

kblockd は、blk-mqの初期化時に下記の関数によって生成される。

// 1761:
int __init blk_dev_init(void)
{
    BUILD_BUG_ON(REQ_OP_LAST >= (1 << REQ_OP_BITS));
    BUILD_BUG_ON(REQ_OP_BITS + REQ_FLAG_BITS > 8 *
            sizeof_field(struct request, cmd_flags));
    BUILD_BUG_ON(REQ_OP_BITS + REQ_FLAG_BITS > 8 *
            sizeof_field(struct bio, bi_opf));

    /* used for unplugging and affects IO latency/throughput - HIGHPRI */
    kblockd_workqueue = alloc_workqueue("kblockd",
                        WQ_MEM_RECLAIM | WQ_HIGHPRI, 0);
    if (!kblockd_workqueue)
        panic("Failed to create kblockd\n");

    blk_requestq_cachep = kmem_cache_create("request_queue",
            sizeof(struct request_queue), 0, SLAB_PANIC, NULL);

    blk_debugfs_root = debugfs_create_dir("block", NULL);

    return 0;
}

ここで、block用のWork Queue(kblockd_workqueue)はグローバル変数である。

Work Queueの作成については、過去の記事を参照。

leavatail.hatenablog.com

dispatch用のworkを初期化する

Dispatch用のWorkは、Hardware dispatch queues の初期化フェーズにおいて、blk_mq_run_work_fn関数を定義する。

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

// 2759:
static struct blk_mq_hw_ctx *
blk_mq_alloc_hctx(struct request_queue *q, struct blk_mq_tag_set *set,
        int node)
{
    struct blk_mq_hw_ctx *hctx;
    gfp_t gfp = GFP_NOIO | __GFP_NOWARN | __GFP_NORETRY;

    hctx = kzalloc_node(blk_mq_hw_ctx_size(set), gfp, node);
    if (!hctx)
        goto fail_alloc_hctx;

    if (!zalloc_cpumask_var_node(&hctx->cpumask, gfp, node))
        goto free_hctx;

    atomic_set(&hctx->nr_active, 0);
    if (node == NUMA_NO_NODE)
        node = set->numa_node;
    hctx->numa_node = node;

    INIT_DELAYED_WORK(&hctx->run_work, blk_mq_run_work_fn);
    spin_lock_init(&hctx->lock);
    INIT_LIST_HEAD(&hctx->dispatch);
    hctx->queue = q;
    hctx->flags = set->flags & ~BLK_MQ_F_TAG_QUEUE_SHARED;

    INIT_LIST_HEAD(&hctx->hctx_list);

    /*
    * Allocate space for all possible cpus to avoid allocation at
    * runtime
    */
    hctx->ctxs = kmalloc_array_node(nr_cpu_ids, sizeof(void *),
            gfp, node);
    if (!hctx->ctxs)
        goto free_cpumask;

    if (sbitmap_init_node(&hctx->ctx_map, nr_cpu_ids, ilog2(8),
                gfp, node, false, false))
        goto free_ctxs;
    hctx->nr_ctx = 0;

    spin_lock_init(&hctx->dispatch_wait_lock);
    init_waitqueue_func_entry(&hctx->dispatch_wait, blk_mq_dispatch_wake);
    INIT_LIST_HEAD(&hctx->dispatch_wait.entry);

    hctx->fq = blk_alloc_flush_queue(hctx->numa_node, set->cmd_size, gfp);
    if (!hctx->fq)
        goto free_bitmap;

    if (hctx->flags & BLK_MQ_F_BLOCKING)
        init_srcu_struct(hctx->srcu);
    blk_mq_hctx_kobj_init(hctx);

    return hctx;

 free_bitmap:
    sbitmap_free(&hctx->ctx_map);
 free_ctxs:
    kfree(hctx->ctxs);
 free_cpumask:
    free_cpumask_var(hctx->cpumask);
 free_hctx:
    kfree(hctx);
 fail_alloc_hctx:
    return NULL;
}

dispatch用のworkは、INIT_DELAYED_WORKによって、Hardware dispatch queue (struct blk_mq_hw_ctx) のrun_workに設定される。

dispatch用のworkを遅延実行する

Dispatch用のWorkは、__blk_mq_delay_run_hw_queue関数によってWork Queueに追加 (遅延実行) される。

// 1560:
static void __blk_mq_delay_run_hw_queue(struct blk_mq_hw_ctx *hctx, bool async,
                    unsigned long msecs)
{
    if (unlikely(blk_mq_hctx_stopped(hctx)))
        return;

    if (!async && !(hctx->flags & BLK_MQ_F_BLOCKING)) {
        int cpu = get_cpu();
        if (cpumask_test_cpu(cpu, hctx->cpumask)) {
            __blk_mq_run_hw_queue(hctx);
            put_cpu();
            return;
        }

        put_cpu();
    }

    kblockd_mod_delayed_work_on(blk_mq_hctx_next_cpu(hctx), &hctx->run_work,
                    msecs_to_jiffies(msecs));
}

__blk_mq_delay_run_hw_queue関数では、引数asyncによって非同期で実行させることができる。

  • async == FALSE: Dispatch を試みる (ドライバが BLK_MQ_F_BLOCKING でない場合のみ)
  • async == TRUE: mcescミリ秒後に Dispatch を実行する

ただし、mmcドライバはBLK_MQ_F_BLOCKINGとなるため、ここでは kblockd_mod_delayed_work_on関数による処理のみを確認する。

kblockd_mod_delayed_work_on関数では、blk_mq_alloc_hctx関数で初期化したblk_mq_run_work_fn関数を実行することになる。

blk_mq_run_work_fnの定義は次のようになっている。

// 1812:
static void blk_mq_run_work_fn(struct work_struct *work)
{
    struct blk_mq_hw_ctx *hctx;

    hctx = container_of(work, struct blk_mq_hw_ctx, run_work.work);

    /*
    * If we are stopped, don't run the queue.
    */
    if (blk_mq_hctx_stopped(hctx))
        return;

    __blk_mq_run_hw_queue(hctx);
}

blk_mq_run_work_fn関数は、Hardware dispatch queueが停止状態でないことを確認する。

停止状態でなければ、__blk_mq_run_hw_queue関数を呼び出す。

リクエストを Hardware Queue に送信する

__blk_mq_run_hw_queue関数は次のような定義となっている。

// 1479:
static void __blk_mq_run_hw_queue(struct blk_mq_hw_ctx *hctx)
{
    int srcu_idx;

    /*
    * We can't run the queue inline with ints disabled. Ensure that
    * we catch bad users of this early.
    */
    WARN_ON_ONCE(in_interrupt());

    might_sleep_if(hctx->flags & BLK_MQ_F_BLOCKING);

    hctx_lock(hctx, &srcu_idx);
    blk_mq_sched_dispatch_requests(hctx);
    hctx_unlock(hctx, srcu_idx);
}

Dispatchでは、リクエストをキューに入れる際にブロッキングする恐れがある。
そのため、__blk_mq_delay_run_hw_queue関数と同様に、排他制御を意識する必要がある。

blk-mqでは、hctx_unlock関数とhctx_lock関数によって排他制御を担う。

ただし、RCUを取得している間は、blockingやsleeping が禁じられているため、BLK_MQ_F_BLOCKINGの場合には、Sleepable RCU (SRCU) の使用が必要となる。

// 691:
static void hctx_unlock(struct blk_mq_hw_ctx *hctx, int srcu_idx)
    __releases(hctx->srcu)
{
    if (!(hctx->flags & BLK_MQ_F_BLOCKING))
        rcu_read_unlock();
    else
        srcu_read_unlock(hctx->srcu, srcu_idx);
}

static void hctx_lock(struct blk_mq_hw_ctx *hctx, int *srcu_idx)
    __acquires(hctx->srcu)
{
    if (!(hctx->flags & BLK_MQ_F_BLOCKING)) {
        /* shut up gcc false positive */
        *srcu_idx = 0;
        rcu_read_lock();
    } else
        *srcu_idx = srcu_read_lock(hctx->srcu);
}

RCU (または、SRCU) によりロックが確保できた場合、Dispatchの本処理に移る。
その後、blk_mq_sched_dispatch_requests関数を実行する。

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

// 346:
void blk_mq_sched_dispatch_requests(struct blk_mq_hw_ctx *hctx)
{
    struct request_queue *q = hctx->queue;

    /* RCU or SRCU read lock is needed before checking quiesced flag */
    if (unlikely(blk_mq_hctx_stopped(hctx) || blk_queue_quiesced(q)))
        return;

    hctx->run++;

    /*
    * A return of -EAGAIN is an indication that hctx->dispatch is not
    * empty and we must run again in order to avoid starving flushes.
    */
    if (__blk_mq_sched_dispatch_requests(hctx) == -EAGAIN) {
        if (__blk_mq_sched_dispatch_requests(hctx) == -EAGAIN)
            blk_mq_run_hw_queue(hctx, true);
    }
}

blk_mq_sched_dispatch_requests関数では、Hardware Dispatch Queueが停止状態・静止状態(quiesced)でない場合、__blk_mq_sched_dispatch_requests関数によってリクエストをディスパッチする。

この時、Hardware Dispatch Queueがビジー状態 (-EAGAIN) となる可能性がある。

blk_mq_sched_dispatch_requests関数では、Dispatchを2回まで実行する。
それでもリソースがビジー状態であった場合、blk_mq_run_hw_queue関数によって __blk_mq_delay_run_hw_queue関数を実行する。

また、リクエストのDispatch処理が呼び出された回数は debugfs上のインターフェースから確認することができる。

    # cat /sys/kernel/debug/block/mmcblk0/hctx0/run
    6

また、__blk_mq_sched_dispatch_requests関数の定義は次の通りとなっている。

// 294:
static int __blk_mq_sched_dispatch_requests(struct blk_mq_hw_ctx *hctx)
{
    struct request_queue *q = hctx->queue;
    const bool has_sched = q->elevator;
    int ret = 0;
    LIST_HEAD(rq_list);

    /*
    * If we have previous entries on our dispatch list, grab them first for
    * more fair dispatch.
    */
    if (!list_empty_careful(&hctx->dispatch)) {
        spin_lock(&hctx->lock);
        if (!list_empty(&hctx->dispatch))
            list_splice_init(&hctx->dispatch, &rq_list);
        spin_unlock(&hctx->lock);
    }

    /*
    * Only ask the scheduler for requests, if we didn't have residual
    * requests from the dispatch list. This is to avoid the case where
    * we only ever dispatch a fraction of the requests available because
    * of low device queue depth. Once we pull requests out of the IO
    * scheduler, we can no longer merge or sort them. So it's best to
    * leave them there for as long as we can. Mark the hw queue as
    * needing a restart in that case.
    *
    * We want to dispatch from the scheduler if there was nothing
    * on the dispatch list or we were able to dispatch from the
    * dispatch list.
    */
    if (!list_empty(&rq_list)) {
        blk_mq_sched_mark_restart_hctx(hctx);
        if (blk_mq_dispatch_rq_list(hctx, &rq_list, 0)) {
            if (has_sched)
                ret = blk_mq_do_dispatch_sched(hctx);
            else
                ret = blk_mq_do_dispatch_ctx(hctx);
        }
    } else if (has_sched) {
        ret = blk_mq_do_dispatch_sched(hctx);
    } else if (hctx->dispatch_busy) {
        /* dequeue request one by one from sw queue if queue is busy */
        ret = blk_mq_do_dispatch_ctx(hctx);
    } else {
        blk_mq_flush_busy_ctxs(hctx, &rq_list);
        blk_mq_dispatch_rq_list(hctx, &rq_list, 0);
    }

    return ret;
}

__blk_mq_sched_dispatch_requests関数では、大きく分けて二つの処理を担う。

  1. Software Staging Queue / I/Oスケジューラにあるリクエストを Hardware Dispatch Queue に追加する
  2. Hardware Dispatch Queue のリクエスト を デバイスドライバに追加する

__blk_mq_sched_dispatch_requests関数によるリクエスト送信

ここで、デバイスドライバのHardware Queueのサイズには限りがあるため、必要以上のリクエストをディスパッチすることは避けたい。
また、Hardware QueueにDispatchされたリクエストは、I/Oスケジューラによる最適化の恩恵を受けることができない。

そのため、blk-mqでは、デバイスドライバにディスパッチする予定のリクエストをリスト(hctx->dispatch)として管理し、このリストが空の場合にI/Oスケジューラからリクエストをディスパッチする。

このリストは、次のような条件によって要素が追加される。

  • blk_mq_request_bypass_insert関数: PREFLUSH/FUAなどを即座にDispatch Queueに追加するため
  • blk_mq_dispatch_rq_list関数: 自身が処理しきれなかったリクエストを積んて置くため

__blk_mq_sched_dispatch_requests関数はblk-mq内のデータの状態に応じて異なる処理を実行する。

  • hctx->dispatchが空でない場合: hctx->dispatchの要素をrq_listに追加し、blk_mq_dispatch_rq_list関数を実行する。
  • I/Oスケジューラが登録されている場合: I/Oスケジューラからリクエストをディスパッチするためにblk_mq_do_dispatch_sched関数を実行する。
  • Hardware Dispatch Queueがビジー状態の場合: Software Staging Queue から リクエストをディスパッチする。
  • それ以外: Software Staging Queue内の全要素をrq_listに追加し、blk_mq_dispatch_rq_list関数を実行する。

おわりに

本記事では、リクエストをディスパッチがカーネルスレッドとして起動するところから、I/Oスケジューラからディスパッチする処理の直前となっている次の関数について確認した。

  • __blk_mq_delay_run_hw_queue
  • blk_mq_run_work_fn
  • __blk_mq_run_hw_queue
  • blk_mq_sched_dispatch_requests
  • __blk_mq_sched_dispatch_requests

変更履歴

  • 2023/07/16: 記事公開

参考