ブロックデバイス層

このページの目次

1回のアプリケーションによる書き込みは、複数のデバイスコマンドに分割される場合もあれば、キャッシュに留まる場合もあります。ブロック層では、⁠データ範囲、キューイング位置、完了の約束をそれぞれ追跡する必要があり、アプリケーション IOPS とデバイスコマンド数を直接同一視することはできません。

参照目標読書位置
bio、単位、分割と結合第 1~2 節
マルチキュー、バッチ処理、スケジューリング第 3~5 節
キュー深度、永続化、観測第 6~9 節

1. 1回の write が 1回のデバイスコマンドに相当するわけではない

バッファリング書き込みは通常、ページキャッシュを先に更新し、後続のライトバックでブロック I/O が生成されます。キャッシュヒットによる読み取りでは、デバイスにアクセスしない場合もあります。ダイレクト I/O は一部のキャッシュパスをバイパスしますが、アラインメント、ファイルシステム、デバイスの制限により、1回の呼び出しが複数のリクエストに分解される可能性があります。

ファイル操作、bio、request、ハードウェアコマンドは1対1で対応しない 上位のデータ範囲 ファイルシステム / ダイレクトブロックアクセス bio 記述 デバイス区間 + メモリフラグメント request キューとデバイスの制約に従って構成 上位への完了返却 完了と永続化は区別が必要 blk-mq スケジューリング / ディスパッチ マージ、キューイング、タグ、バックプレッシャ ドライバーとコントローラ DMA、コマンド、完了ステータス 1つの bio は分割され、複数の bio は結合される可能性があります。完了順序も送信順序と一致する必要はありません。

bio はブロックデータ操作とそのメモリ範囲を記述するものであり、ディスクトランザクションの原子単位ではありません。bio はクローン作成、分割、再マッピングが可能であり、互換性のある複数の bio は同じ request に結合されます。device-mapper、RAID などのスタック層では、異なる下位デバイス向けの I/O が生成されることもあります。

現代のカーネルでは、現在位置と残り長さは主に bio.bi_iter の bi_sector、bi_size などで記述され、bi_status は blk_status_t です。旧バージョンの平坦化メンバの擬似構造を、現在コンパイル可能な API として扱ってはいけません。

3種類の「セグメント」

  • bio_vec はメモリ範囲を記述し、開始ページ、オフセット、長さを含みます。現代の multipage bvec はページを跨ぐことができるため、必ずしも1つが1つのページに正確に対応するわけではありません。
  • bio イテレータは現在処理中の部分を記述しており、bi_vcnt のみから残りの I/O を推測することはできません。
  • ドライバーが最終的に使用する DMA スキャターギャザーセグメントは、マッピングとマージを経ており、元の bvec の数と一致する必要はありません。

マッピングルールについては、IOMMU と DMA API を参照してください。

2. セクタ単位と分割・結合

同じデバイスのバイト位置でも、異なる層で使用される単位は異なります。

表現層1 MiB 開始位置の表現
バイトオフセット1048576
Linux ブロック層 512 バイト セクタ2048
4096 バイト ロジカルブロックの NVMe LBA256

ブロック層のセクタは固定で 512 バイト単位でカウントされ、デバイスのロジカルブロックも 512 バイトである必要はありません。

連続する2つの 4 KiB bio のデバイス区間がそれぞれ [2048,2056) と [2056,2064) セクタの場合、操作、フラグ、キューの制限などが互換性があれば、8 KiB の request に合成される可能性があります。メモリの物理アドレスが隣接しているかどうかは唯一の条件ではなく、request は SG を使用して離散メモリを表現できるためです。

逆に、256 KiB の bio は、128 KiB の転送しか許可されていないパスや、セグメント数や境界の制約が厳しいパスでは、複数の request に分割される可能性があります。制限は総バイト数だけでなく、最大セグメント数、単一セグメントの長さ、境界、アラインメント、および異なる操作タイプの特異な制約を含みます。

したがって、アプリケーション IOPS、bio 数、request 数、デバイスコマンド数は同じ指標ではありません。マージ率の向上はコマンドオーバーヘッドを削減する可能性がありますが、それによってビジネスリクエストが減ったと見なすことはできません。

3. blk-mq:マルチエントリ並列実行、必ずしもロックレスではない

blk_mq_ctx は CPU 向けのソフトウェア送信コンテキストを提供し、blk_mq_hw_ctx はドライバーへのディスパッチキューの状态を管理します。CPU はキューマッピングに基づいて hctx を選択し、読み取り、書き込み、ポーリングなどのキュータイプにも異なるマッピングを持たせることができます。

hctx はソフトウェア構造体であり、コントローラが直接読み取る SQ メモリではありません。ドライバーは request をデバイスコマンドに変換し、自前のキューに配置します。ソフトウェアキュー、ハードウェアキュー、CPU数、割り込みベクタ数は等しいことが保証されておらず、ctx を汎用の per-cgroup キューと呼ぶことはできません。

マルチキューはホットスポットを分散共有しますが、キューリスト、スケジューラ、タグ割り当て、ドライバーでは依然としてロックやアトミック操作が必要になる場合があります。1つのスケジューラが request_queue 範囲の共有状態を維持することもあり、例えば mq-deadline のスケジューリング状態などです。「すべてのスケジューラが各 hctx 上で独立して動作する」と一律に記述することはできません。blk-mq ドキュメント を参照してください。

タグとバックプレッシャ

タグは、進行中の request を識別・管理するために使用され、完了パスがリクエストを特定できるようにします。スケジューラタグ、ドライバータグ、共有タグセットなど、異なる構成が存在します。各 request が作成から完了まで常に同じ固定ハードウェアタグのみを使用するとは限りません。

リソースが不足している場合、リクエストは待機、ディスパッチの遅延、再キューイング、または NOWAIT などの規約に従って即時実行不能として返される可能性があります。これは、デバイスがコマンドを受信した後にメディアエラーを返すこととは異なります。ドライバーがリクエストを受け入れた後、プロトコルに従って最終的に完了させる必要があり、「一時的にビジー」と「リクエストを保持しているが後で完了する」を混同してはいけません。

4. プラグ:短時間のバッチ処理、トランザクションではない

プラグは現在のタスクに関連付けられ、バッチ処理に適した request を一時保存し、マージの機会を増やし、個別ディスパッチのオーバーヘッドを削減します。基本的な呼び出し形式は以下の通りです。これは既存のカーネル送信パス内のローカル構造であり、ユーザー空間プログラムではありません。

struct blk_plug plug;
blk_start_plug(&plug);
/* ここで、正しく構築され、寿命が管理された bio を送信する。 */
blk_finish_plug(&plug);

パラメータは struct blk_plug * であり、current ではありません。プラグは、finish まですべての I/O を常に保留するわけではありません。ブロッキングスケジューリング、容量などの条件により、早期の送信を促すことがあります。また、バッチ全体の原子性完了や、送信順序でのバッチ全体の完了を保証するものではありません。

バッチ処理はスループットを増加させる一方で、待機時間も増加させる可能性があるため、バッチサイズを無限に拡大することはできません。読み書きの依存関係、flush、ファイルシステムトランザクションの順序は、対応するメカニズムによって保証されます。

5. スケジューラが解決するのはトレードオフ

スケジューラ核心的な考え方検証が必要なコスト
noneエレベータソート戦略を余分に加えないブロック層/ドライバーのキューイングが残っており、マージや待機がないわけではない
mq-deadlineアドレスソートと期限切れキューの組み合わせで、読み書きの待機をバランスさせるexpire はスケジューリングの目標であり、ハードウェア完了のリアルタイム上限ではない
kyberレイテンシフィードバックに基づき、異なるカテゴリの進行中リクエストを制限トークンで並行性を制御しており、単純な固定レートトークンバーストではない
BFQエンティティごとの予算と重み付けにより、帯域幅、公平性、応答性を提供公平性/インタラクティブ性の最適化、キュー待機、スループットの間にトレードオフが存在

「NVMe には none、デスクトップには BFQ」といった一般論で測定を代替しないでください。ストレージメディア、デバイス内部の並列性、混合読み書き、cgroup、バックグラウンドライトバック、CPUコストは結論を変更します。

read_expire / write_expire は、アプリケーションから完了までの最大レイテンシを保証するものではありません。writes_starved は読み書きの選択戦略を制御するものであり、各ビジネスに SLA を設定するものではありません。スケジューラパラメータの単位と利用可能な項目は、実際の sysfs を参照してください。mq-deadline、Kyber、BFQ を参照してください。

6. キュー深度、スループット、レイテンシの定量的関係

安定したシステム、同じ測定境界において、リトルの法則 L = λ × W を使用します。

入力数値
完了率 λ100000 IOPS
平均応答時間 W200 μs = 0.0002 s
平均在途数 L100000 × 0.0002 = 20

統計的な境界を合わせてから、この平均値を解釈します。

この 20 は観測平均値であり、sysfs パラメータを強制的に 20 に設定する必要があるわけではありません。キューには変動があり、ソフトウェアとハードウェアにはそれぞれ割り当てがあります。並行性を増やすと、デバイスが飽和していない場合はスループットが向上する可能性があります。飽和後に深度を増やすと、主にキューイングとテールレイテンシが増加します。

同様に、100000 回の 4096 バイトのデータ操作は約 409.6 MB/s、つまり 390.625 MiB/s です。IOPS は、I/O サイズ、読み書き比率、同期セマンティクス、レイテンシと組み合わせて解釈する必要があります。

以下に、固定されたサービス能力を持つ閉ループキューを用いて、この数式を図示します。まず並行リソースを増やし、次にリクエスト深度を増やし、スループットのプラットフォームが出現した後、余分なリクエストがどこに停止するかを比較します。

この図は理想的な閉ループモデルを採用しています。Q 個のリクエストが常に在途状態にあり、K 個の並列サービススロットがあり、各サービスの所要時間は d です。λ は毎秒の完了数、W は同じ境界内の平均時間です。

図を準備しています
スループットがプラットフォームに到達した後、余分な深度はどこへ行くのか?

飽和していない状態で並行性を増やすと、完了率が向上する。サービススロットが満杯になると、余分な作業は主にキューイングを増加させる。リトルの法則は、同じ統計的範囲内で平均量を使用して適用する必要がある。

7. 完了、順序、永続化

ブロック層の完了通知は、現在のデバイススタックの契約においてその操作が終了したことを示すものであり、通常の write completion のみをもって、データが停電後も保持されるメディアに書き込まれたと断定することはできません。揮発性ライトキャッシュはこの境界を変更します。

フラグ永続化の制約
REQ_PREFLUSH後続のデータフェーズの前に、以前のキャッシュ書き込みの永続化を処理する
REQ_FUA今回の書き込みが対応する永続化条件を満たしてから完了を報告する

デバイスネイティブのサポートとブロック層のシミュレーションでは、異なるコマンドシーケンスが生成されます。どちらも、任意の範囲の書き込みの原子性を直接保証するものではありません。ライトキャッシュ制御 を参照してください。

アプリケーションの fsync() は、ファイルシステムがデータと必要なメタデータを調整し、適切なブロック層操作をプッシュダウンします。ファイル作成/名前変更に関連するディレクトリの永続化については、ディレクトリ同期も考慮する必要があり、1つの bio フラグのみでファイルトランザクション全体を解釈することはできません。

8. 読み取り専用で実際のキューパラメータを確認

from pathlib import Path

fields = ('logical_block_size', 'physical_block_size', 'max_sectors_kb',
          'max_hw_sectors_kb', 'nr_requests', 'scheduler', 'write_cache')
for dev in sorted(Path('/sys/block').glob('*')):
    if dev.name.startswith(('loop', 'ram')):
        continue
    print(dev.name)
    for field in fields:
        try:
            print(' ', field, (dev / 'queue' / field).read_text().strip())
        except OSError:
            pass
    print(' ', 'visible hctx directories:', len(list((dev / 'mq').glob('[0-9]*'))))

scheduler 内の角括弧は現在選択されている項目を示します。mq ディレクトリが存在しない場合は、スタックデバイスなどの場合による可能性があります。nr_requests を物理デバイスのキューあたりの最大コマンド数として一般的に解釈することはできず、max_sectors_kb にもすべてのデバイスに適用される固定のデフォルト値はありません。

9. 統計から証拠へ

iostat -x の await には、対応するデバイスの統計的範囲内の待機時間とサービス時間が含まれており、アプリケーションリクエストのエンドツーエンドのレイテンシではありません。%util が 100% に近づいても、並列デバイスにとって「スループットが絶対的な上限に達した」ことを意味するものではなく、CPU 利用率として扱うこともできません。

blktrace/ブロック層の tracepoint を使用して、キューイング、ディスパッチ、完了を観察できますが、分割、マージ、リトライ、部分的な完了、イベントの損失により、単純なアドレスによるペアリングは機能しなくなります。まず、アイデンティティと階層を特定し、その後で時間差を計算します。統計フィールドはブロックデバイス統計を、トレーシング方法はトレーシングとデバッグを参照してください。