Linux I/Oの仕組みと全体像を徹底解説|read・writeからPage Cache・ストレージまで IO

はじめに

Linuxサーバを運用していると、「ディスクI/Oが遅い」「I/O waitが高い」「ファイルの読み書きに時間がかかる」といった問題に遭遇することがあります。しかし、read()やwrite()を実行したあと、Linux内部でどのような処理が行われているのかをイメージするのは簡単ではありません。

アプリケーションがファイルを読み書きする場合、データがアプリケーションからストレージへ直接移動するわけではありません。システムコール、VFS、ファイルシステム、Page Cache、Block I/O、デバイスドライバなど、Linuxカーネルが提供する複数の仕組みを経由します。

これらの役割を理解すると、I/O性能の問題がアプリケーション、メモリ、ファイルシステム、ストレージのどこで発生しているのかを整理しやすくなります。また、キャッシュが効いている読み込みと実際にストレージへアクセスする読み込みの違いや、write()が完了してもデータが直ちに永続化されるとは限らない理由も理解できます。

本記事では、LinuxにおけるI/Oの基本から、ファイルを読み書きするときにApplicationからStorageまで処理が進む流れを、各レイヤーの役割とともに分かりやすく解説します。

LinuxにおけるI/Oとは

I/O(Input / Output)とは

I/OとはInput / Outputの略で、コンピュータが外部からデータを受け取る処理と、外部へデータを送り出す処理を意味します。Inputは入力、Outputは出力です。

CPUによる計算だけで完結する処理とは異なり、I/Oでは、ストレージやネットワークインターフェースなどとのデータの受け渡しが発生します。またLinuxでは、パイプやソケットなど、物理デバイスを直接伴わない入出力もI/Oとして扱われます。一般に、こうした装置へのアクセスはCPUやメモリ内の処理より遅いため、I/Oの待ち時間はシステム性能に大きく影響します。

Linuxでは何がI/Oになるのか

Linuxでは、多くの入出力対象がファイルディスクリプタを通じて扱われます。通常のファイルだけでなく、端末、パイプ、ソケット、デバイスなども、アプリケーションからは共通したread()やwrite()などのインターフェースで操作できます。

  • ファイルからデータを読み込む
  • ファイルへデータを書き込む
  • SSDやHDDへブロック単位の要求を送る
  • ネットワークを通じてパケットを送受信する
  • キーボードや端末から入力を受け取る
  • 画面や端末へ結果を出力する

ファイルI/O・ストレージI/O・ネットワークI/O

I/Oは対象や処理するレイヤーによって、いくつかの種類に分けて考えることができます。

I/Oの種類概要
ファイルI/Oアプリケーションがファイルという形式でデータを読み書きする処理設定ファイルの読み込み、ログファイルへの書き込み
ストレージI/OLinuxカーネルがブロックデバイスへデータの読み書きを要求する処理SSDやHDDからの読み込み、データの永続化
ネットワークI/Oソケットを通じて別のホストやプロセスとデータを送受信する処理Webリクエストの受信、データベースへの通信

ファイルI/OとストレージI/Oは同じではありません。ファイルを読み込んでも、必要なデータがメモリ上のPage CacheにあればストレージI/Oは発生しません。一方、キャッシュにデータがなければ、LinuxカーネルはストレージへI/Oを要求します。

Linux I/Oの全体像

ApplicationからHardwareまでの流れ

一般的なファイルI/Oでは、Application、System Call、VFS、Filesystem、Page Cache、Block Layer、Device Driver、Storageという複数のレイヤーを処理が通過します。

各レイヤーは、アプリケーション向けの共通インターフェース、ファイルの管理、メモリ上のキャッシュ、ブロック要求の管理、実機の制御といった役割を分担しています。この分離により、アプリケーションは利用しているファイルシステムやストレージ機器の細かな違いを意識せずにI/Oを実行できます。

read / writeとSystem Call

read()とwrite()は、ファイルディスクリプタを指定してデータを読み書きする代表的なシステムコールです。実際のプログラムでは標準ライブラリの関数を経由する場合もありますが、最終的にはシステムコールを通じてカーネルへ処理が依頼されます。

  • read():カーネルが管理するデータをUser Spaceのバッファへ読み込む
  • write():User Spaceのバッファにあるデータをカーネルへ渡す

システムコールが呼び出されると、CPUはアプリケーションの命令を実行するユーザーモードから、カーネルの処理を実行できるカーネルモードへ移ります。処理が完了すると戻り値がアプリケーションへ返され、ユーザーモードでの実行が再開されます。

Linux I/Oの流れ

一般的なファイルI/Oのレイヤーを一連の流れで整理すると、次のようになります。

Applicationはread()やwrite()を呼び出し、System Callを通じてカーネルへ処理を依頼します。VFSが要求を共通的に受け付け、ext4やXFSなどのFilesystemへ処理を渡します。Filesystemはファイルの論理的な位置とストレージ上のデータを対応付けます。

通常のバッファードI/OではPage Cacheが読み書きの中継地点になります。キャッシュだけで処理できる場合、Block I/O以降の処理はその時点では発生しません。ストレージへのアクセスが必要な場合は、Block Layer、Device Driverを経由してStorageへ要求が送られます。

この図は全体像を理解するための基本的なモデルです。実際のLinuxでは、mmap、Direct I/O、非同期I/O、ネットワークファイルシステム、Device Mapper、ソフトウェアRAIDなどにより、経路や完了のタイミングが変わる場合があります。ここでは理解しやすくするため、Page Cacheを独立したレイヤーとして表現しています。

次章以降で詳細を見ていきましょう。

ファイルを読み込むときの流れ

Applicationからread()を呼び出す

アプリケーションは、事前にopen()などで取得したファイルディスクリプタ、読み込み先のバッファ、読み込みたいバイト数を指定してread()を呼び出します。ファイルディスクリプタは、プロセスが開いているファイルやソケットなどを識別するための番号です。

Kernelへ処理が移る

read()によってシステムコールが発生すると、処理はUser SpaceからKernel Spaceへ移ります。カーネルは、ファイルディスクリプタが有効か、読み込み権限があるか、指定されたメモリ領域を使用できるかなどを確認します。

VFSからFilesystemへ

カーネル内では、まずVFS(Virtual File System)が要求を受け取ります。VFSはファイル操作の共通インターフェースを提供し、対象ファイルを管理するext4やXFSなどの具体的なファイルシステムへ処理を渡します。

ファイルシステムは、ファイルのメタデータやオフセットをもとに、要求されたデータがファイル内のどこにあるかを判断します。そして、対応するデータがPage Cacheに存在するかを確認します。

Page CacheまたはStorageからデータを取得する

必要なデータがPage Cacheに存在する状態をキャッシュヒットと呼びます。この場合、カーネルはメモリ上のデータをアプリケーションのバッファへコピーできるため、通常はストレージへアクセスする必要がありません。

Page Cacheにデータが存在しないキャッシュミスの場合、ファイルシステムはBlock Layerを通じてストレージへ読み込み要求を送ります。同期的なread()では、必要なデータが到着するまでプロセスが待ち状態になることがあります。その間、CPUは別の実行可能なプロセスを処理できます。

ストレージから読み込まれたデータはPage Cacheへ格納され、その後アプリケーションのバッファへコピーされます。同じデータを再び読み込む場合はPage Cacheを利用できるため、2回目以降の読み込みが高速になる可能性があります。

ファイルを書き込むときの流れ

Applicationからwrite()を呼び出す

アプリケーションは、書き込み先のファイルディスクリプタ、書き込むデータが置かれたバッファ、データのバイト数を指定してwrite()を呼び出します。処理がKernel Spaceへ移ると、カーネルは権限や引数を確認し、VFSを通じて対象のファイルシステムへ要求を渡します。

Page Cacheへの書き込み

通常のバッファードI/Oでは、write()で渡されたデータはまずPage Cacheへコピーされます。変更されたものの、まだストレージへ書き出されていないページはDirty Pageと呼ばれます。

この段階でwrite()が成功として戻ることがあります。そのため、通常のバッファードI/Oでは、write()が正常終了しても、それはデータがPage Cacheへ書き込まれた段階であり、SSDやHDDへの永続化まで完了したことを意味するとは限りません。

Filesystemによる管理

ファイルシステムは、ファイルサイズ、更新時刻、データを配置するブロックなどを管理します。xt4やXFSのようなジャーナリングファイルシステムでは、主にメタデータの更新情報などをジャーナルへ記録し、障害発生時にファイルシステムの整合性を回復しやすくします。

Storageへの書き出し

Dirty Pageは、カーネルのWriteback処理によって後からストレージへ書き出されます。複数の書き込みをまとめたり、適切なタイミングで処理したりすることで、アプリケーションを長時間待たせず、ストレージを効率よく利用できます。

データの永続化を明示的に要求する場合は、fsync()やfdatasync()などを使用します。ただし、実際の永続性にはファイルシステム、ストレージ機器のキャッシュ、接続方式なども関係します。また、O_DIRECTを使用するDirect I/Oなど、Page Cacheを基本的に経由しないI/O方式もあります。

VFSとFilesystemの役割

VFSとは

VFSはVirtual File Systemの略で、Linuxカーネル内にあるファイル操作の共通レイヤーです。アプリケーションから受け取ったopen()、read()、write()、close()などの要求を、対象となる具体的なファイルシステムへ振り分けます。

VFSは、ファイルを表すinode、開かれたファイルを表すfile、ディレクトリエントリを表すdentry、マウントされたファイルシステムを表すsuperblockなどの共通オブジェクトを使用して、ファイルやディレクトリを管理します。

ext4やXFSとの関係

ext4やXFSは、ディスク上にファイルやディレクトリをどのように配置し、メタデータをどのように管理するかを定める具体的なファイルシステムです。それぞれはVFSが定める操作に対応する処理を実装します。

アプリケーションがread()を呼び出したとき、VFSは対象がext4上のファイルであればext4の処理へ、XFS上のファイルであればXFSの処理へ要求を渡します。NFSなどのネットワークファイルシステムや仮想的なファイルシステムも、VFSを通じて同様の操作体系で扱えます。

なぜVFSが必要なのか

VFSがなければ、アプリケーションはファイルシステムごとに異なる操作方法を使い分けなければなりません。VFSが差異を吸収することで、アプリケーションは保存先がext4、XFS、NFSのいずれであっても、基本的に同じシステムコールを利用できます。

つまりVFSは、アプリケーションに統一されたインターフェースを提供しながら、Linuxが多様なファイルシステムを扱えるようにする抽象化レイヤーです。

Page Cacheの役割

Page Cacheとは

Page Cacheは、ファイルのデータをメモリ上に保持するLinuxカーネルのキャッシュ機構です。一度読み込んだデータや、これからストレージへ書き出すデータをRAMに保持し、低速なストレージアクセスの回数を減らします。

MemoryとStorageの関係

Memoryは高速ですが、一般に電源が失われると内容を保持できません。StorageはMemoryより低速ですが、電源を切ってもデータを保持できます。Page Cacheは、高速なMemoryを利用してStorageとの速度差を吸収します。

Linuxは、アプリケーションが使用していない空きメモリをPage Cacheとして積極的に利用します。アプリケーションが追加のメモリを必要とした場合は、再読み込み可能なクリーンなキャッシュなどを解放してメモリを確保できます。そのため、Page Cacheによって空きメモリが少なく見えても、直ちにメモリ不足とは限りません。

read / writeとPage Cache

read / writeとPage Cacheの関係は以下の図表の通りです。

処理Page Cacheの役割
read()データがキャッシュにあればメモリから返す。なければストレージから読み込み、キャッシュへ格納する
write()通常はデータをキャッシュへ書き込み、Dirty Pageとして管理したあと、Writebackでストレージへ反映する

Page Cacheによってread()とwrite()の応答を高速化できますが、アプリケーションから見える処理完了の時点と、物理ストレージ上で処理が完了する時点が異なる場合があります。この違いは、性能調査やデータ保護を考えるうえで重要です。

Block I/Oの役割

Block Deviceとは

Block Deviceは、データを一定の単位でランダムアクセスできるデバイスです。HDD、SSD、NVMeデバイス、仮想ディスクなどが代表例で、Linuxでは/dev/sdaや/dev/nvme0n1のようなデバイスファイルとして確認できます。

アプリケーションは通常、物理的な位置を指定してBlock Deviceを直接操作するのではなく、ファイルシステム上のファイルを操作します。ファイルシステムが、ファイル内の位置をストレージ上のブロックへ対応付けます。

FilesystemからBlock Layerへ

Page Cacheにないデータを読み込む場合や、Dirty Pageをストレージへ書き出す場合、ファイルシステムは対象ブロックを特定し、Block LayerへI/O要求を渡します。

Block Layerは、ファイルシステムとブロックデバイスドライバの間に位置する共通レイヤーです。要求をキューで管理し、隣接した要求をまとめるなど、デバイスへ効率よくI/Oを発行するための処理を行います。具体的な挙動はデバイスやカーネルのI/Oスケジューラ構成によって異なります。

Device DriverへのI/O要求

Block Layerで管理されたI/O要求は、対象デバイスを担当するDevice Driverへ渡されます。この段階で、ファイルという論理的な単位の操作は、特定のブロック範囲を読み書きするデバイス向けの要求になっています。

Device DriverからStorageへ

Device Driverの役割

Device Driverは、Linuxカーネルからの共通的なI/O要求を、実際のデバイスやコントローラが理解できる命令へ変換するソフトウェアです。また、デバイスの初期化、キューの管理、エラー処理、I/O完了通知の処理なども担当します。

HDD / SATA SSD / NVMe SSD へのI/O

同じファイルI/Oであっても、その先にあるストレージの種類によって物理的な処理は異なります。

HDDは磁気ディスクを回転させ、ヘッドを目的の位置まで移動してデータを読み書きします。そのため、シークや回転待ちが発生し、特にランダムI/Oでは遅延が大きくなります。

SATA SSDはNAND Flashを使用するため、HDDのような機械的な動作がありません。そのため低遅延ですが、SATAインターフェースを利用するため、転送速度や同時に処理できるI/Oには制約があります。

NVMe SSDもNAND Flashを使用しますが、PCI Express経由で接続し、NVMeプロトコルによって多数のキューと並列処理を活用できます。そのため、高いスループットと低いレイテンシを実現できます。

また、ストレージとMemory間のデータ転送では、CPUがデータを1バイトずつ運ぶのではなく、一般にDMA(Direct Memory Access)が利用されます。

CPUはデータ転送を開始するための処理を行いますが、実際のデータ転送をDMAに任せることで、その間に別の処理を実行できます。

I/O完了がApplicationへ戻るまで

デバイスがI/Oを完了すると、通常は割り込みなどによってCPUへ完了を通知します。デバイスドライバとBlock Layerは要求の完了処理を行い、読み込みであれば取得したデータを利用可能な状態にします。

必要なデータを待っていたプロセスは実行可能な状態になり、CPUスケジューラによって再びCPUが割り当てられると処理を再開します。カーネルはデータをUser Spaceのバッファへ渡し、read()は読み込んだバイト数などを戻り値として返します。エラーが発生した場合は、エラーを示す戻り値と原因を表す情報がアプリケーションへ通知されます。

なお、書き込みでは、write()を呼び出したプロセスと、後からDirty Pageをストレージへ書き出す処理が同じタイミングで動くとは限りません。非同期に進むWritebackまで含めて考えることが重要です。

まとめ

LinuxにおけるI/Oは、アプリケーションと外部のデバイスとの間でデータを受け渡す処理です。ファイルI/Oでは、アプリケーションが呼び出したread()やwrite()がシステムコールとしてカーネルへ渡され、VFS、ファイルシステム、Page Cache、Block Layer、デバイスドライバなどが連携して処理します。

読み込みでは、Page Cacheにデータがあればメモリから高速に取得し、なければストレージへI/Oを発行します。書き込みでは、通常はPage Cacheへデータを書き込んだあと、Writebackによってストレージへ反映します。そのため、ファイルI/Oの発生が常に物理的なストレージI/Oの発生を意味するわけではありません。

Linux I/Oを理解するうえでは、ApplicationからStorageまでを一つの処理として見るだけでなく、各レイヤーが何を担当し、どの時点で待ち時間が発生するのかを分けて考えることが重要です。この全体像を押さえておくことで、I/O waitの上昇やアプリケーションの遅延が発生した際にも、原因を体系的に切り分けられるようになります。

I/Oの各機能については、以下のシリーズで詳しく解説していきますので、ぜひあわせてご覧ください。

記事テーマリンク
第1回Linux I/Oの全体像本記事
第2回ファイルを読み書きすると何が起こるのか記載中
第3回VFS(Virtual File System)の仕組み記載中
第4回ファイルシステムの仕組み記載中
第5回Page CacheとファイルI/Oの関係記載中
第6回Block I/Oの仕組み記載中
第7回Device Driverとストレージデバイス記載中
第8回HDD・SSD・NVMeの違いとLinuxからの見え方記載中

コメント