はじめに
Linuxでファイルを読み書きすると、Applicationから発生した要求はVFS、Filesystem、Page Cacheなどを経由します。さらにSSDやHDDなどのStorageへアクセスする必要がある場合、その要求はBlock I/OとしてLinux KernelのBlock Layerへ渡されます。
Block I/Oでは、Device上の領域とMemory上のデータを結び付けるbio、I/O要求をQueueで管理するrequest、複数CPUからの要求を効率よく処理するblk-mq、Dispatchを調整するI/O Schedulerなどが連携します。最終的にrequestはDevice Driverへ渡され、Deviceが処理できるCommandへ変換された後、Storage Deviceへ送信されます。
この仕組みを理解すると、ファイルI/OがどのようにStorageへの要求へ変換されるのか、bioとrequestは何が違うのか、HDD・SSD・NVMeでI/O処理の考え方が異なる理由、I/O完了がどのように上位レイヤーへ通知されるのかを体系的に整理できます。
本記事では、Block Deviceの基本からbio、request、blk-mq、I/O Scheduler、Device Driver、I/O完了通知まで、Linux Block I/Oの流れを順番に解説します。
Block I/Oとは
Block Deviceとは
Block Deviceは、一定の大きさに区切られた領域へランダムアクセスできるデバイスです。HDD、SSD、NVMe Device、仮想ディスク、Logical Volumeなどが代表例です。
Linuxでは、Block Deviceは通常/dev/sda、/dev/vda、/dev/nvme0n1、/dev/mapper/vg-lvなどのDevice Fileとして確認できます。ApplicationはBlock Deviceを直接開くこともできますが、一般的にはその上に作成されたFilesystemを通じてファイルを操作します。
ここでいうブロックには複数の意味があります。Filesystem Block、Block Layerで扱うSector、Storage Device内部の物理単位は必ずしも同じ大きさではありません。どのレイヤーの単位を指しているのか区別することが重要です。
Character Deviceとの違い
Character Deviceは、基本的にバイトストリームとして順番にデータを扱うデバイスです。端末、シリアルポート、一部の入力デバイスなどが該当します。

どちらもDevice Fileとして見えますが、Kernel内部では異なる仕組みとDevice Driverによって処理されます。Device Fileの種類はls -lの先頭文字で確認でき、Block Deviceはb、Character Deviceはcと表示されます。
LinuxにおけるBlock I/Oの役割
LinuxのBlock I/Oは、Filesystemなどの上位レイヤーから発生した読み書き要求をBlock Deviceへ届けるための仕組みです。Block Layerが共通の仕組みを提供することで、上位レイヤーがStorage Deviceごとの細かな違いを直接意識する必要を減らしています。
- 読み込み・書き込み対象の領域を表現する
- I/O要求を分割または結合する
- requestをQueueで管理する
- I/O Schedulerによって要求を調整する
- Device Driverへ要求をDispatchする
- 完了やエラーを上位レイヤーへ通知する
Linux Block I/Oの全体像
Linuxでファイルに対してread()やwrite()を実行しても、I/O要求が必ずすぐにStorage Deviceへ送られるわけではありません。一般的なBuffered I/OではPage Cacheが利用され、Storageへのアクセスが必要になった場合にBlock I/Oが発生します。
全体の流れは次のとおりです。

Readの場合、必要なデータがPage Cacheに存在すればMemoryから返されます。Page Cacheに存在しない場合は、Storage Deviceからデータを読み込む必要があります。Writeの場合は、通常はPage Cache上のPageが更新されてDirty Pageとなり、その後WritebackによってStorage Deviceへ書き出されます。
Storageへのアクセスが必要になると、Filesystemなどの上位レイヤーでファイル上の位置がBlock Device上の領域へ対応付けられ、その情報をもとにBlock I/Oが構成されます。
Block I/Oを表現する基本的なデータ構造がbioです。bioには対象となるDevice上の領域、ReadやWriteなどの操作、Memory上のデータ領域などの情報が含まれます。
bioはBlock Layerへ渡され、requestとしてQueue管理されます。複数のbioが一つのrequestへまとめられる場合もあります。requestはblk-mqによって管理され、I/O Schedulerを利用する場合はDispatchの順序やタイミングなどが調整されます。
その後、requestはDevice DriverへDispatchされます。Device DriverはrequestをDeviceが処理できるCommandへ変換し、HDD、SATA SSD、NVMe SSDなどのStorage Deviceへ送信します。
大まかな流れは、Filesystem / Page Cache → bio → request → blk-mq → Device Driver → Storage Deviceと整理できます。
以降では、Block I/Oの中心となるbio、blk-mq、I/O Scheduler、Device Driverについて詳しく見ていきます。
Block I/O要求はどのように作られるのか
Applicationから見ると、I/Oはファイルを読み書きする処理です。一方、Storage Deviceでは、Device上のどの領域に対してデータを読み書きするのかを指定する必要があります。
Filesystemは、Applicationが扱うファイル上の位置とBlock Device上の領域を対応付け、ファイルI/OをBlock I/Oへつなぐ役割を持っています。
ファイルI/OからBlock I/Oへ
例えばApplicationがread()を実行して、ファイルの一部を読み込むとします。
Applicationは「SSDのこの場所を読み込む」と指定するわけではありません。File Descriptorやファイル内の位置を指定して読み込みを要求します。
FilesystemはInodeなどの情報を使って、指定されたファイルの位置がBlock Device上のどのBlockに対応しているのかを調べます。
イメージすると次のようになります。

つまり、Applicationが扱うファイル上の位置をBlock Device上の領域へ対応付けることが、Filesystemの重要な役割の一つです。Storageへのアクセスが必要になると、この対応関係をもとに上位レイヤーでBlock I/Oが構成され、Block Layerへ送られます。
このBlock I/Oを表現する基本的なデータ構造がbioです。bioの構造については次の章で詳しく説明します。
論理Blockと実際の保存場所
Linuxが指定するBlockの位置と、Storage Device内部の物理的な保存場所が必ずしも直接対応しているわけではありません。
例えばSSDでは、Linuxから見えるLogical Block Addressと実際のNAND Flash上の保存場所との対応を、SSD内部のFlash Translation Layerが管理しています。
また、LVM、RAID、暗号化、仮想マシンなどを利用している場合は、途中に別の変換レイヤーが存在することがあります。

そのため、Block Layerは基本的にLinuxから見える論理的なBlockを対象としてI/Oを行うと考えると分かりやすいです。
Block I/O要求がDeviceへ送られるまで
上位レイヤーからBlock Layerへ送られたBlock I/Oは、最終的にDevice Driverを経由してStorage Deviceへ送信されます。その過程では、条件が合えば複数のI/Oがまとめられることがあります。また、Deviceの制限などに応じて要求が分割される場合もあります。

Block I/OはrequestとしてQueueで管理され、blk-mqによってDevice DriverへDispatchされます。一つのrequestに複数のbioが含まれる場合もあります。
ここでは、Applicationが指定するファイル上の位置がBlock Device上の領域へ対応付けられ、bioとしてBlock Layerへ送られ、requestとしてQueue管理されるという流れを押さえておけば十分です。
bioとは
bioは、Linux Kernel内でBlock I/Oを表現する基本的なデータ構造です。簡単に言えば、Device上のどの領域とMemory上のどのデータの間で、どのようなI/Oを行うのかを表します。

bioが保持する情報
bioにはBlock I/Oを実行するために必要な情報が保持されます。
主な情報は次のとおりです。
- 対象となるBlock Device
- Device上の開始SectorとI/Oサイズ
- ReadやWriteなどの操作
- データを保持するMemory Page
- I/O完了時に必要となる情報
Kernel内部のFieldや表現はKernel Versionによって変化しますが、重要なのはbioがDevice上の領域とMemory上の領域を結び付けていることです。
MemoryとBlock Deviceをどのように結び付けるのか
bioはbio_vecと呼ばれる要素を使って、I/O対象となるMemory領域を管理します。
イメージすると次のようになります。

bio_vecにはMemory Page、Page内のOffset、Lengthなどの情報が含まれます。一つのbioに複数のMemory領域を含めることもできます。
例えばReadの場合は、Block Deviceの指定されたSectorから読み込んだデータが、bioで指定されたMemory Pageへ格納されます。
Writeの場合は反対に、Memory Page上のデータがBlock Deviceの指定されたSectorへ書き込まれます。
bioとrequestの違い
bioを理解するときに重要なのがrequestとの違いです。bioは、Device上の領域とMemory上のデータ領域を結び付け、どのようなBlock I/Oを行うのかを表現します。一方、requestは、一つ以上のbioをもとにblk-mqがQueue管理やDevice DriverへのDispatchを行う単位です。
bioとrequestは同じものではなく、複数のbioが一つのrequestへまとめられる場合もあります。簡単に整理すると、bioはBlock I/Oの内容を表現する単位、requestはQueue管理やDriverへのDispatchに使われる単位と考えると分かりやすいです。
実際のデータ転送では一般にDMAが利用され、DeviceやControllerがMemoryとの間でデータを転送します。
両者の比較を以下にまとめます。

Linux Multi-Queue Block I/O(blk-mq)の仕組み
blk-mqとは
blk-mqは、LinuxのMulti-Queue Block I/O Layerです。多数のCPU Coreと、複数のCommand Queueを持つ高速なStorage Deviceへ対応するため、I/O要求を複数Queueで並列に処理します。
従来の単一Queueを中心とした構造では、多数のCPUが同じLockやQueueを共有することによる競合が問題になりました。blk-mqはSoftware QueueとHardware Dispatch Queueを分け、CPU間の競合を抑えながらDeviceの並列性を活用します。

Software Queue
Software Queueは、CPU側から提出されたI/O要求を受け付けるためのQueueです。blk-mqではCPUごとにSoftware Staging Queueを持つことで、多数のCPUが一つの共有Queueへ集中することを避けます。
各CPUから提出された要求はSoftware Queueを経由し、対応するHardware Queue側へ送られます。この構造によって共有Lockへの競合を減らし、Multi-Core環境でI/Oを並列に処理しやすくします。
Hardware Queue
Hardware Queueは、Device DriverへrequestをDispatchするためのQueueです。Kernel内部ではHardware Dispatch Queueとして扱われ、実際のDeviceが持つCommand Queueとの対応を考慮して構成されます。
Hardware Queueの数がCPU数と同じになるとは限りません。DeviceとDriverが利用できるQueue数、MSI-X Interrupt、CPU Affinity、Kernel設定などによってMappingが決まります。複数のSoftware Queueが一つのHardware QueueへMappingされる場合もあります。
複数CPUからのI/O要求を処理する仕組み
複数CPUから提出された要求は、それぞれのSoftware Queueで受け付けられ、対応するHardware QueueへDispatchされます。NVMeのように多数のQueueを扱えるDeviceでは、複数のHardware Queueを使って要求を並行に処理できます。
blk-mqはrequestを識別するTagも管理します。Deviceへ送信中のrequestをTagによって追跡し、完了通知を対応するrequestへ結び付けます。Queueが処理できる数を超えた場合は、新たな要求が待たされることがあります。
I/O Schedulerの役割
I/O要求をどのように並べるのか
I/O Schedulerは、Block Layerで管理されるrequestのDispatch順序やタイミングを調整します。Schedulerによっては要求のMergeや順序調整を行い、Latency、Throughput、公平性などを考慮してDevice Driverへrequestを渡します。
HDDではHeadの移動を減らすため、近いSectorへの要求を効率よく処理することが性能に大きく影響します。SSDやNVMeには機械的なSeekがありませんが、要求のMerge、Read Latencyの制御、Process間の公平性などが意味を持つ場合があります。
blk-mqとI/O Schedulerの関係
現在のBlock Layerでは、blk-mqに対応したI/O SchedulerがDriverへDispatchする前のrequestを管理し、Dispatchする順序やタイミングを調整します。I/O Schedulerはblk-mqとは独立した別のI/O経路ではなく、blk-mqのrequest処理に組み込まれる選択可能な仕組みです。
ただし、I/O Schedulerは必ず高度な並べ替えを行うわけではありません。noneを選択した場合は、Schedulerによる複雑な順序調整を最小限にしてblk-mqからDriverへ要求を渡します。Device Mapperを含む構成では、どのBlock Device階層にSchedulerを適用するかも重要です。
mq-deadline / kyber / none / bfq
主要なスケジューラの比較を下表に記載します。
| Scheduler | 概要 | 主な考え方 |
|---|---|---|
| mq-deadline | 要求にDeadlineを設けて長時間の待ちを抑えながら、Sector順なども考慮する | Latencyの上限とThroughputのバランス |
| kyber | Read / WriteなどのI/OをQueueで管理し、同時に発行する要求数を調整する | Latencyを一定範囲に抑えながら高いThroughputを目指す |
| none | 複雑な並べ替えを行わず、簡素にDispatchする | 高速Deviceや下位層がSchedulingする構成 |
| bfq | ProcessやI/O ContextへBudgetを割り当てる | 公平性や対話的処理の応答性 |
利用可能なSchedulerと現在選択されているSchedulerは、Deviceごとに/sys/block/<device>/queue/schedulerで確認できます。角括弧で囲まれた名前が現在の選択です。
以下は、AlmaLinux9におけるデフォルト設定を確認した結果です。

上記の表示は、次の状態を示しています。
[none]:現在選択されているScheduler
mq-deadline:選択可能
kyber:選択可能
bfq:選択可能
利用可能な種類や既定値はKernel、Distribution、Device、Driverによって異なります。名称だけで決めず、実際のWorkloadでLatencyとThroughputを測定して選択します。
Device DriverへI/O要求が渡されるまで
Block LayerからDevice Driverへ
Dispatch対象となったrequestはHardware Queueを通じてDevice Driverへ渡されます。Driverはrequestに含まれる操作、Sector、Data Segmentなどを確認し、Deviceが処理できるCommandを構成します。
Deviceが一時的に追加要求を受け取れない場合、DriverはQueueがBusyであることをBlock Layerへ示し、後からDispatchを再開することがあります。Queue Depthは同時に処理できる要求数に関係し、性能とLatencyへ影響します。
Hardware Queueからデバイスへ
DriverはCommand DescriptorやMemory Bufferの情報をDeviceから参照できるようにし、Doorbell Registerへの書き込みなど、Hardware固有の方法で新しいCommandを通知します。
データ転送には一般にDMAが使われます。ReadではDeviceからMemoryへ、WriteではMemoryからDeviceへデータが転送されます。CPUはCommandの準備や完了処理を担当しますが、すべてのデータをCPU命令で1Byteずつコピーするわけではありません。
I/Oが完了すると何が起こるのか
ストレージデバイスによるI/O処理
Commandを受け取ったStorage Deviceは、指定されたLogical Block Addressに対するReadやWriteを実行します。HDDではHead移動と回転、SSDではControllerによるFlash管理、NVMeではSubmission QueueからのCommand取得など、内部動作はDeviceによって異なります。
Readでは要求されたデータをMemoryへ転送し、WriteではMemoryから受け取ったデータをDevice側で処理します。Write Commandの完了がDevice内部の不揮発媒体への確実な保存をどこまで意味するかは、Write Cache、Flush、FUA、電源断保護などにも依存します。
I/O完了通知
DeviceがCommandを完了すると、DeviceやProtocolに応じた方法で完了情報がDriverへ伝えられます。NVMeではCompletion Queueへ結果が記録されます。通常はHardware InterruptによってCPUへ通知されますが、高速Deviceでは複数の完了をまとめて処理したり、Pollingによって完了を確認したりする構成もあります。
Interruptを受けたDevice Driverは完了したCommandを特定し、成功、I/O Error、Timeoutなどの状態を確認します。blk-mqのTagにより、Device側の完了と送信済みrequestを対応付けられます。
Block Layerへの完了通知
Device Driverはrequestの完了をBlock Layerへ通知します。Block Layerはrequestに含まれるbioの完了状態を更新し、すべての対象範囲が処理されたかを確認します。
bioの完了処理が呼び出されると、FilesystemやPage Cacheなどの上位レイヤーへ結果が伝わります。Readならデータが入ったPageを利用可能な状態にし、Writebackなら書き出し中の状態を解除して、正常完了時にはDirty状態を解消します。
Applicationまで処理が戻る流れ
同期Readでデータを待っていたProcessは、I/O完了によって実行可能な状態になります。CPU Schedulerから再びCPUを割り当てられると、KernelはPage Cache上のデータをUser Space Bufferへ渡し、read()が読み込んだByte数を返します。
Buffered Writeでは、Applicationのwrite()がBlock I/Oより前に完了している場合があります。その場合、I/O完了は主にWriteback処理へ通知されます。fsync()で待っているProcessがあれば、必要な書き込みと同期処理の結果が揃った後にfsync()が戻ります。
非同期I/Oでは、Completion Event、io_uringのCompletion Queue、Callbackなど、使用するInterfaceに応じた方法でApplicationへ完了が通知されます。Block Layerより上の待機・通知方式はI/O APIによって異なります。
まとめ
LinuxのBlock I/Oでは、FilesystemやPage Cacheなどの上位レイヤーから発生したStorageへのI/O要求がBlock Layerを通じてDeviceへ送られます。
Block I/Oを理解するうえで重要なのが、bio、request、blk-mqの関係です。bioはDevice上の領域とMemory上のデータ領域を結び付けてBlock I/Oの内容を表現し、requestは一つ以上のbioをもとにQueue管理やDevice DriverへのDispatchを行う単位です。
blk-mqはSoftware QueueとHardware Queueを利用し、多数のCPUから発生するI/O要求を効率よく処理します。必要に応じてI/O SchedulerがrequestのDispatchを調整し、その後Device DriverがDevice固有のCommandへ変換してStorage Deviceへ送信します。
I/Oが完了すると、その結果はDevice DriverからBlock Layer、bio、FilesystemやPage Cacheなどの上位レイヤーへ伝えられます。同期Read、Buffered Write、fsync()、非同期I/OではApplicationが完了を認識するタイミングが異なる点も重要です。
Filesystem / Page Cache → bio → request → blk-mq → Device Driver → Storage Deviceという流れを理解しておくと、iostatなどを使ったI/O性能調査や、Queue Depth、I/O Scheduler、NVMeなどの仕組みを理解するときにも役立ちます。


コメント