関連記事
- Part 1: 環境セットアップ
- Part 2: System call Interface
- Part 3: VFS
- Part 4: ext2 (1) write_iter
- Part 5: ext2 (2) write_begin
- Part 6: ext2 (3) get_block
- Part 7: ext2 (4) write_end
- Part 8: writeback (1) work Queue
- Part 9: writeback (2) wb_writeback
- Part 10: writeback (3) writepages
- Part 11: writeback (4) write_inode
- Part 12: block (1) submit_bio
- Part 13: block (2) blk_mq
- Part 14: I/O scheduler (1) mq-deadline
- Part 15: I/O scheduler (2) insert_request
- Part 16: I/O scheduler (3) dispatch_request
- Part 17: block (3) blk_mq_run_work_fn
- Part 18: block (4) block: blk_mq_do_dispatch_sched
- Part 19: MMC (1) initialization
- Part 20: PL181 (1) mmci_probe
- Part 21: MMC (2) mmc_start_host
- Part 22: MMC (3) mmc_rescan
- Part 23: MMC (4) mmc_attach_sd
- 概要
- はじめに
- kblockd の概要
- block用のワーカスレッドの作成
- dispatch用のworkを初期化する
- dispatch用のworkを遅延実行する
- リクエストを Hardware Queue に送信する
- おわりに
- 変更履歴
- 参考
概要
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
を利用する。
この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の作成については、過去の記事を参照。
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
関数では、大きく分けて二つの処理を担う。
- Software Staging Queue / I/Oスケジューラにあるリクエストを Hardware Dispatch Queue に追加する
- Hardware Dispatch Queue のリクエスト を デバイスドライバに追加する
ここで、デバイスドライバの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: 記事公開