このページの目次

ブロックデバイス層

カバー範囲: bio → request → plug/unplug → blk-mq (multi-queue) → IO scheduler (mq-deadline/kyber/bfq) → blktrace/blkparse デバッグ カーネルバージョン: 2.6 ~ 6.x。blk-mq 再構築 (3.13~5.x) に重点注記

概要

ブロック層は、ファイルシステムとデバイスドライバの間のトランスレーション層です。入力としてファイルシステムから提出された bio(「N番目のセクタを読み書きし、結果をこれらのページに配置し、完了時に通知してください」)を受け取り、出力としてデバイスドライバへ request を送信します。この間、ブロック層は結合、ソート、スケジューリングを行い、IO レイテンシを最小化しつつスループットを最大化することを目指します。

2.6 の単一リクエストキューから 5.x のマルチキュー (blk-mq) へ、Linux ブロック層は完全な書き換えを経験しました。ドライバモデルは「1デバイス:1キュー:1ロック」から「per-CPU 送信キュー + 複数のハードウェアキュー」へと変化しました。


bio: ブロック IO の原子単位

// include/linux/blk_types.h
struct bio {
    struct block_device *bi_bdev;        // 対象ブロックデバイス
    sector_t            bi_sector;       // 開始セクタ (512B単位)
    unsigned int        bi_size;         // 残りのIOバイト数
    unsigned int        bi_status;       // 完了ステータス (BLK_STS_OK/error)

    struct bio_vec      *bi_io_vec;      // ページベクタ (scatter-gatherリスト)
    unsigned short      bi_vcnt;         // ベクタエントリ数
    unsigned short      bi_max_vecs;

    bio_end_io_t        *bi_end_io;      // 完了コールバック
    void                *bi_private;     // コールバックコンテキスト

    unsigned short      bi_write_hint;   // IO優先度のヒント
    unsigned short      bi_ioprio;
};

bio_vec: メモリページ上のIOの記述

// include/linux/bvec.h
struct bio_vec {
    struct page     *bv_page;    // 対象ページ
    unsigned int    bv_len;      // このセグメントの長さ
    unsigned int    bv_offset;   // ページ内オフセット
};

1つのbioは複数の bio_vec を含むことができます。これが scatter-gather (分散-結合) IO です。ファイルシステムは連続しないメモリページを1つのIOにパックでき、ブロック層とドライバはそれをデバイスが許容するサイズに分割します。

bio 送信パス

// block/blk-core.c
void submit_bio(struct bio *bio) {
    // 1. blk-cgroupを使用しているか確認 → cgroupにアカウント処理
    // 2. bioサイズがデバイスの制限を超えていないか確認 → 必要に応じて分割
    // 3. submit_bio_noacct() を呼び出し → __submit_bio_noacct_mq()
    //    → blk_mq_submit_bio()  (blk-mqパス、現在デフォルト)
}

blk-mq: マルチキューアーキテクチャ

blk-sq から blk-mq への移行理由

blk-sq (単一キュー, 2.6 ~ 3.x):
  struct request_queue *q;  ← 1つの大きなロックがすべての送信を保護
  全CPUが q->queue_lock で競合 → マルチコアでのスケーラビリティのボトルネック
  IO schedulerのシングルスレッドモデル → 高IOPSデバイス (NVMe) で頭打ち

blk-mq (3.13+, 5.x で blk-sq を完全に置換):
  2層のキュー:
    ソフトウェアキュー (ctx): per-CPU または per-cgroup → ロック競合ゼロ
    ハードウェアキュー (hctx): デバイスのハードウェアキューにマッピング (NVMe SQ, SCSI tag)

  Tags: ハードウェアキューの並列リクエスト数を管理するために使用
    → 各ハードウェアキューには固定のtagプールがあり、自然なレート制限となる

データ構造

// include/linux/blk-mq.h
struct blk_mq_hw_ctx {
    unsigned int        queue_num;       // ハードウェアキュー番号
    struct request_queue *queue;
    struct blk_mq_tags  *tags;           // tagプール (並列制限用)
    unsigned int        nr_ctx;          // ソフトウェアキュー数

    struct list_head    dispatch;        // スケジューラからのリクエスト
    unsigned int        dispatched;

    struct blk_mq_ctx   **ctxs;          // 配下のソフトウェアキュー
};

struct blk_mq_ctx {
    unsigned int        cpu;             // バインドされたCPU
    struct blk_mq_hw_ctx *hctxs[];       // マッピングされたハードウェアキュー
    struct list_head    rq_lists[];      // 送信リスト
};

送信パス: plug → scheduler → dispatch → driver

bio 送信パス: blk_mq_submit_bio() の5つの段階 block/blk-mq.c ① bioが大きすぎるか? 制限超過 → blk_bio_discard_split()/blk_bio_segment_split() で複数のbioに分割 ② plugマージの試行 blk_mq_attempt_plug_merge() → front/back merge (既存のrequestの前後に結合) ③ request + tag の割り当て blk_mq_get_new_requests() → blk_mq_get_tag() でtagを取得、枯渇時は待機 ④ request の初期化 blk_mq_rq_ctx_init() → bioと関連付け、初期化完了 ⑤ スケジューラへの挿入 blk_mq_sched_insert_request() → スケジューラ経由か直接ディスパッチかを決定 あり なし IO schedulerあり blk_mq_sched_try_insert_merge() → スケジューラにキューイング IO schedulerなし blk_mq_run_dispatch_ops() → 直接ディスパッチ 5つの段階が順次実行される: 制限超過時はまず分割、結合可能な場合は既存のrequestに結合、tagが不足すれば待機; 最後に挿入スケジューリング段階で、IO schedulerにキューイングするか、ハードウェアキューに直接ディスパッチするかを決定する。

Plug: バッチ送信

// block/blk-mq.c + block/blk-plug.c
// plug は per-task の bio 蓄積メカニズムである:
//   blk_start_plug(current)     // plug を開始
//   submit_bio() × N            // N 個の bio を蓄積し、即時ディスパッチしない
//   blk_finish_plug(current)    // すべての bio を一括でフラッシュ

// 設計原理:
//   1. ロック競合の削減 (1回のロック取得で N 個のリクエストを挿入)
//   2. マージ機会の増加 (plug 期間中に front/back merge 可能)
//   3. ファイルシステムは通常、書き込みパスで plug を利用する:
//      ext4_writepages() → blk_start_plug() → 複数回 submit_bio → blk_finish_plug()

IO スケジューラ

マルチキュー IO スケジューラと旧単一キュースケジューラの根本的な違い:⁠スケジューラはグローバルではなく、per-hctx(ハードウェアキューごと)である⁠。各ハードウェアキューは独立してスケジューリングされ、異なるキューは異なるCPUからの送信に対応しているためです。

mq-deadline

// block/mq-deadline.c
// 読み書きそれぞれで赤黒木 (sector順ソート) + FIFO (時計順ソート) を維持
// 
// 主要パラメータ:
//   read_expire:  読みリクエストの最大待機時間 (デフォルト 500ms)
//   write_expire: 書きリクエストの最大待機時間 (デフォルト 5000ms)
//   writes_starved: 書きリクエストの割り当て (何バッチの読みを優先処理した後に書きを処理するか)
//
// スケジューリングロジック:
//   1. FIFOを先に確認: 期限切れのリクエストがある場合 → 赤黒木からそのsector近辺のリクエストを取得 (一括送信)
//   2. それ以外: directionごとのbatchで送信 (読みを優先するが、writes_starvedにより書きが飢えるのを防止)
//   3. 1回のdispatchで最大16リクエストまで (他のキューの飢えを防ぐため)
//
// 適しているもの: NVMe, 汎用SSD

kyber

// block/kyber-iosched.c
// トークンバケットに基づく適応型スケジューリング
// 目標: 読み/書きキューの深さを制御し、レイテンシを目標値以内に維持
//
// 動作原理:
//   読み/書きのレイテンシヒストグラムを維持
//   レイテンシに基づき、その方向のトークンバケットレートを動的に調整
//   レイテンシが低い → バケットを増やす → より多くのIO並列実行
//   レイテンシが高い → バケットを減らす → IO並列実行を減らす (デバイスキュー深さを低減)
//
// 適しているもの: レイテンシに敏感なSSD負荷 (例: データベース)

bfq (Budget Fair Queuing)

// block/bfq-iosched.c
// cgroup/プロセスごとにIO帯域幅を割り当て
// 各プロセスには「バジェット」(送信可能なセクタ数)が1つある
// B-WF2Q+ アルゴリズムに基づく (CFSの公平スケジューリングに類似)
//
// 特徴:
//   - インタラクティブなプロセスは自動的に高いウェイトを得る (think timeを検出)
//   - cgroup blkioコントローラをサポート
//   順序読みが検出される → より多くのバジェットを付与 → 高スループット
//
// 適しているもの: デスクトップシステム (バックグラウンドのバックアップがフォアグラウンドアプリの応答性を妨げないよう確保)

スケジューラの切り替え

cat /sys/block/nvme0n1/queue/scheduler
# [mq-deadline] kyber bfq none

echo kyber > /sys/block/nvme0n1/queue/scheduler

キューパラメータのチューニング

# ハードウェアキュー数 (通常はCPU数に等しい)
cat /sys/block/nvme0n1/mq/*/cpu_list

# max_sectors_kb: 1回のIOの最大サイズ (デフォルト 512KB = 1024セクタ)
cat /sys/block/sda/queue/max_sectors_kb

# nr_requests: 各ハードウェアキューの最大リクエスト数
cat /sys/block/nvme0n1/queue/nr_requests  # デフォルト 256

# スケジューラパラメータ
cat /sys/block/nvme0n1/queue/iosched/read_expire  # mq-deadline

デバッグと観測

# blktrace: 各IOの完全なライフサイクルを追跡
blktrace -d /dev/nvme0n1 -o trace
blkparse -i trace > trace.txt
# 出力: time, CPU, action (Q=キューイング, G=取得, I=挿入, D=ディスパッチ, C=完了)

# iostat: デバイスレベルの統計
iostat -x 1 nvme0n1
# r/s, w/s, r_await, w_await, aqu-sz (平均キュー深さ), %util

# 単一デバイスの生カウント
cat /sys/block/nvme0n1/stat
# フィールド: read_ios, read_merges, read_sectors, read_ticks, write_ios, ...

# blk-mq デバッグ
cat /sys/kernel/debug/block/nvme0n1/hctx*/tags
# 各ハードウェアキューのtag使用状況

# ftrace でブロック層イベントを追跡
echo 1 > /sys/kernel/debug/tracing/events/block/block_bio_queue/enable
echo 1 > /sys/kernel/debug/tracing/events/block/block_rq_complete/enable
cat /sys/kernel/debug/tracing/trace_pipe

主要な設定とコンパイル

# カーネル起動パラメータ
scsi_mod.use_blk_mq=1    # SCSI に blk-mq を強制使用 (5.0+ でデフォルト)
elevator=mq-deadline      # デフォルト IO スケジューラ
elevator=bfq              # デスクトップ推奨

# カーネルコンパイルオプション
CONFIG_BLK_MQ=y           # blk-mq を有効化
CONFIG_IOSCHED_BFQ=y      # BFQ サポート
CONFIG_BLK_DEV_THROTTLING=y  # cgroup blkio

参考と拡張

  • ソースコード⁠: block/blk-mq.c (~4000行、blk-mq コア)、block/blk-merge.cblock/mq-deadline.cblock/bfq-iosched.cblock/kyber-iosched.c
  • カーネルドキュメント⁠: Documentation/block/Documentation/block/bfq-iosched.rst
  • LWN:
    • "The multi-queue block layer" (lwn.net/Articles/552904/)
    • "blk-mq and the I/O schedulers" (lwn.net/Articles/738449/)

キーワード: bio, bio_vec, blk-mq, software queue, hardware queue, plug, mq-deadline, kyber, bfq, blktrace, iostat, IO scheduler