I/O(Input/Output/入出力)とは、コンピュータやプログラムが、自分の外側とデータを受け渡す処理です。OSが管理するdevice I/Oを仕組みまで追うと、アプリの要求、メモリと機器の間のdata transfer、完了結果という往復に分けられます。memo.txtへ「A」を保存するなら、write()要求をOSがqueueへつなぎ、多くの場合DMAでmemoryからstorageへdataを運び、interruptまたはpollingでcompletionを返してwaiting threadをReadyへ戻しますが、通常のbuffered write()はdevice transfer完了より前にreturnすることもあります。

I/Oを「キーボード入力やファイル保存」とだけ覚えると、なぜCPU使用率が低いのに遅いのか、DMA中にCPUは何をしているのか、非同期にすると何が変わるのかがつながりません。この記事では、要求・データ・完了の3本の経路を分けて追います。
I/Oの入力・出力は「誰から見た向きか」で決まる
NISTの用語集はI/Oを、コンピュータとの通信に使う機器と、その通信で扱うデータを含む一般用語としています。実務で「入力」「出力」と言うときは、最初に主体と境界を置きます。この記事では、原則として「アプリ/コンピュータから見た向き」で呼びます。
| 場面 | アプリ/コンピュータから見た向き | データの主な移動 |
|---|---|---|
| Aキーを押す | 入力 | キーボード側 → OSの入力処理 → エディタ |
| 画面へAを描く | 出力 | エディタの描画内容 → OS/GPU等 → ディスプレイ |
memo.txtを開く |
入力(read) | ストレージ → メモリ → エディタ |
memo.txtを保存する |
出力(write) | エディタのメモリ → ストレージ |
| データを送受信する | 送信は出力、受信は入力 | メモリ ↔ ネットワーク機器 ↔ 相手 |
Aを入力しただけでは、まだストレージI/Oは起きていない
エディタで「A」を入力すると、まず実行中プロセスのメモリにある編集バッファが変わります。CPUはエディタの命令を実行し、レジスタやメモリの状態を更新します。画面にもAが見えますが、この時点のmemo.txtが不揮発媒体まで更新されたとは限りません。
保存操作で初めて、アプリはOSへ「このバッファのバイト列を、このファイルのこの位置へ書いてほしい」と依頼します。Linux/POSIX系なら概念上はwrite(fd, buffer, length)のような形です。実際のエディタはライブラリ、別スレッド、一時ファイルへの置換、メモリマップなど別の方式を使うこともありますが、メモリ中のデータをOS管理の対象へ渡す境界は共通しています。
アプリからカーネルへ安全に処理を依頼する入口はシステムコールの解説、ファイル名・オフセットを保存領域へ対応付ける部分はファイルシステムの解説で掘り下げています。
write要求からI/O完了までを7段階で追う
ここからは、memo.txtのデータをストレージへ反映する一般的な経路を追います。個別OS・ファイルシステム・装置で実装は異なりますが、要求を渡す、データを移す、完了を返すという3種類の仕事を分けると本質が見えます。
- アプリが要求を作る: ファイルの参照、メモリ上のバッファ、書く長さなどをOSへ渡す。
- カーネルが検証する: ファイルが書き込み可能か、アドレスと長さが妥当かを確認し、通常のバッファ付きI/Oならページキャッシュ等へデータを受け取る。
- I/O要求へ組み立てる: ファイルシステムが対象ブロックを決め、必要な時点でブロック層等がread/write、論理アドレス、長さ、データ位置を持つ要求を作る。
- キューへ入れる: OSやドライバは要求を並べ、結合、順序調整、装置の空きに応じた投入を行う。空きがあれば途中の待ちキューを通らず直接投入する場合もある。
- ドライバが機器固有コマンドへ変換する: コマンド記述子やDMA用メモリを準備し、コントローラのレジスタやdoorbellへ通知する。
- データを転送する: 現代のストレージでは多くの場合、コントローラがDMAを使い、writeならメインメモリ→装置、readなら装置→メインメモリへデータを運ぶ。
- 完了を返す: コントローラが完了状態を記録し、割り込みまたはポーリングでドライバが検出する。カーネルが要求を完了させ、待っていたスレッドや非同期完了の受け手へ結果を渡す。

memo.txt保存の3経路。紺は要求、緑はデータ、紫は完了です。通常のバッファ付きwrite()は上段で戻る場合があり、下段の機器完了を待つのはread miss、direct I/O、fsync()等の別の同期点になることがあります。要求経路:アプリ → カーネル → ドライバ → コントローラ
要求経路が運ぶ中心は「何を、どこへ、どれだけ、どの条件で処理するか」という制御情報です。WindowsではI/Oマネージャがアプリとドライバの間を管理し、I/O Request Packet(IRP)をドライバスタックへ渡します。MicrosoftのI/Oマネージャ資料が示すように、1つの要求がファイルシステムや複数のドライバ層を通る場合もあります。
LinuxのブロックI/Oでは、blk-mqの公式資料が、ソフトウェア側のstaging queueと、装置のsubmission queue/DMA ringへ対応するhardware dispatch queueを分けています。要求は常に1本の列を通るのではなく、CPUごと・装置ごと・hardware contextごとなど複数のキューに分かれ得ます。
データ経路:メモリ ↔ コントローラ ↔ 機器
データ経路が運ぶのは、memo.txtの内容そのものです。writeでは「A」を含むバイト列がメインメモリから装置側へ、readでは逆向きに進みます。要求記述子とデータ本体は別の場所にあり、コントローラが要求内のアドレス情報を使ってデータへアクセスする構成が一般的です。
LinuxカーネルのDMA資料は、DMA_TO_DEVICEをメインメモリから機器、DMA_FROM_DEVICEを機器からメインメモリへの転送と定義しています。DMAならCPUが各バイトを命令でコピーし続ける必要はありません。ただし、CPUは要求作成、バッファ管理、アドレスのmapping、キャッシュ整合、コマンド投入、完了処理などを担います。
完了経路:コントローラ → ドライバ/カーネル → 呼び出し側
装置は処理結果をcompletion queueやstatus register等へ記録します。ドライバは割り込みで通知を受ける場合も、CPUがポーリングして完了を見つける場合もあります。割り込みはCPUへ「すぐ確認してほしい」と通知する方式、ポーリングはCPUが状態を繰り返し確認する方式です。低頻度では割り込みが無駄な確認を減らしやすく、高頻度では割り込みの切り替えコストを避けるためポーリングを組み合わせる実装もあります。
Windowsの公式なI/O要求の例では、機器の割り込みをISRが最小限処理し、DPCへ引き継いで要求を完了させる流れが示されています。これは「割り込みが来た瞬間にアプリが即再開する」のではなく、ドライバとカーネルが状態を確定し、完了結果を上位へ戻す段階があることを表しています。
キューは「遅い装置の前に1本ある待ち行列」ではない
I/O要求をキューへ置く理由は、単に待たせるためではありません。アプリの要求速度と装置の受入速度を切り離し、隣接要求の結合、公平性、優先度、複数チャネル・複数キューへの振り分け、完了との対応付けを可能にします。NVMeのような装置ではhost memory上のsubmission queueへコマンドを置き、controllerがcompletion queueへ結果を返すqueue pairが使われます。
ここで大切なのは、queue depth(QD)は測定地点を言わなければ一意に決まらないことです。アプリが発行済みで未完了の数、OSのblock queueにある数、device submission queueにある数は一致しない場合があります。ページキャッシュによる応答、要求のmerge、再試行、仮想化、RAID、ネットワーク越しのstorageが入れば、境界ごとに数が変わります。
MMIO・ポートI/O・プログラムI/O・DMAを混同しない
「PIO」という略語は資料によってPort I/OまたはProgrammed I/Oを指すため、まず言葉を分けます。また、MMIOは「レジスタをどのアドレス空間で触るか」、DMAは「データ本体を誰が運ぶか」という別の観点です。MMIOでコマンドを投入し、DMAでデータを転送する構成は普通にあります。
| 用語 | 何を決める仕組みか | CPUと機器の関係 | memo.txt保存での位置 |
|---|---|---|---|
| MMIO Memory-Mapped I/O | 機器のcontrol/status registerをCPUのアドレス空間の一部として扱う | CPUがload/store相当の専用accessorでregisterを読む・書く | driverがqueue tailやdoorbell、status等を更新・確認する |
| ポートI/O Port-Mapped I/O | 通常のmemoryとは別のI/O port空間でregisterを扱う | x86等で専用命令/accessorを使う。architecture依存 | 古い機器や一部register accessで使われ得る |
| プログラムI/O Programmed I/O | data本体をCPU命令でdevice register/FIFOとやり取りする | CPUがdataの各word/chunkの転送へ継続的に関与 | 小さい転送や単純な機器で使われ得る |
| DMA Direct Memory Access | 準備後のdata本体をcontroller/deviceがmemoryとの間で転送する | CPUは設定・管理・完了処理を行うが、各byteのcopyを続けない | writeはmemory→device、readはdevice→memory |
Linuxのdevice I/O資料では、MMIOを「CPUのaddress spaceの一部をmemoryではなくdeviceへのaccessとして解釈する方式」、port spaceを「通常のmemory address spaceとは別のaddress range」と説明しています。MMIOのwrite命令がCPUから見て終わっても、bus上でdeviceへまだ届いていないposted writeの場合があるため、「registerへ書いた=device動作完了」でもありません。
OSの共通要求をregister、command descriptor、DMA、interruptへ翻訳する担当はデバイスドライバの解説へつながります。
I/O待ちでは、スレッドとCPUを分けて考える
保存スレッドが「装置の完了が必要になるまで先へ進めない」場合、OSはそのスレッドをWaiting等の待機状態にします。WaitingのスレッドへCPUを割り当て続けても進まないため、スケジューラはReadyな別スレッドを選べます。完了処理が条件を満たすと、待機スレッドはReadyへ戻り、後でCPUへ割り当てられてwrite()やfsync()の続きから進みます。
このとき「I/O中はCPUが何もしていない」は誤りです。CPUは別スレッドの処理を実行でき、I/O側でもカーネル、driver、割り込み処理、polling、buffer copy、protocol処理を実行します。ほかにReadyな仕事がなければCPUがidleになることはありますが、それはCPUが不要なのではなく、装置側の状態変化を待つ間に実行可能な仕事がないということです。
プロセスの共有資源、スレッド固有状態、Waiting→Ready→Runningの違いはプロセスとスレッドの解説で確認できます。CPUが命令を実行する循環はCPUの解説へつながります。
同期/非同期とblocking/non-blockingは別の2軸
この4語はAPIやOSの資料で使い方が少し異なります。初心者が混同を避けるには、完了結果をどう受け取るかと、今の呼び出し元を待たせるかを分けます。
| 軸 | 方式 | 中心の質問 | 結果 |
|---|---|---|---|
| 完了モデル | 同期I/O | この操作の結果を、呼び出しの戻りと結び付けるか | callのreturn時に、そのAPIが約束する範囲の結果が確定する |
| 非同期I/O | 要求の受理と完了の観測を分けるか | submitはcompletion前にreturnでき、後でevent、callback、future、completion queue等から結果を受け取る。すぐ完了する場合もある | |
| 待ち方 | blocking | 今すぐ進めないとき、呼び出しthreadを待機させるか | 結果やreadinessが得られるまでthreadがsleep/waitする |
| non-blocking | 今すぐ進めないとき、すぐreturnするか | EAGAIN/would-block、0件、partial result等を返し、後でretryする |
4通りを具体例で見る
| 組み合わせ | 例 | 何が起きるか |
|---|---|---|
| 同期+blocking | dataのないpipeを通常のread()で読む |
readできるまでcall元threadが待ち、read結果を持ってreturnする |
| 同期+non-blocking | O_NONBLOCKのpipe/socketをread()する |
今すぐ読めなければEAGAIN等でreturnする。操作が裏で完成して後から通知される契約ではない |
| 非同期+待たず継続 | Windows overlapped I/O、POSIX AIO等へ要求をqueueする | submit後に別の仕事へ進み、後でevent/callback/completion stateから結果を回収する |
| 非同期+後でblocking wait | 非同期要求を出した直後にcompletion eventやaio_suspend()で待つ |
I/Oの完了モデルは非同期のまま、waitを呼んだ地点ではthreadがblockingする |
POSIXのread仕様は、pipe等でO_NONBLOCKが設定され、今すぐ読めない場合にEAGAINを返す動作を定めています。一方、POSIX AIOは要求をqueueし、aio_suspend()のような別の待機点を持ちます。WindowsでもMicrosoftの同期/非同期I/O資料が、同期file I/Oでthreadがwaitし、overlapped I/Oではkernelが完了を知らせるまで別の仕事へ進めると説明しています。
write成功と「電源断後も残る」は同じではない
通常のバッファ付きfile I/Oでは、アプリのwrite()がreturnした時点で、データがOSのページキャッシュへ受理されただけの場合があります。dirty pageは後でwritebackされ、そこで初めてblock I/O要求がdeviceへ進みます。したがって、冒頭の7段階は1回のwrite()callの中ですべて終わるとは限りません。
Linuxのwrite(2) manualは、成功returnがdiskへのcommitを保証せず、errorが後のwrite()、fsync()、close()で報告される場合もあると説明しています。fsync(2) manualでは、変更dataと必要なmetadataをstorageへflushし、deviceがtransfer完了を報告するまでcallがblockするとされています。ただし、directory entryの永続化にはdirectory側のfsync()が別途必要な場合もあります。
| 見えている「完了」 | 何が確定した可能性があるか | まだ別に確認すべきこと |
|---|---|---|
| save buttonの表示更新 | アプリ自身の保存手順が一区切りした | 一時file置換、error処理、sync方針 |
write()成功 |
指定byte数がOSに受理された | deviceへのwriteback、partial write、遅延error |
| device I/O completion | controllerがそのcommandの完了を返した | 揮発性device cacheか、不揮発媒体まで到達したか |
fsync()等の成功 |
そのAPI・OS・device stackが約束する同期範囲を満たした | directory、複数fileのatomicity、remote storageの保証 |
ページキャッシュやdevice cacheを挟む理由、flushと永続化の層ごとの違いはバッファの解説、媒体側のwrite cache・flush/FUAはストレージの解説で扱っています。
I/O性能は5指標と要求パターンを同時に見る
I/O性能を「SSDが速い」「IOPSが高い」の一言で比較すると、別のworkloadを同じものとして扱ってしまいます。最低でも、latency、IOPS、throughput、request size、queue depthの測定境界と条件をそろえます。
| 指標 | 定義 | 一緒に明記する条件 |
|---|---|---|
| latency | 1要求を出してから、選んだ境界で完了するまでの時間 | app/OS/deviceのどこで測るか、queue待ちを含むか、平均かp95/p99か |
| IOPS | 1秒に完了したI/O操作数 | request size、read/write比、random/sequential、QD |
| throughput | 1秒に転送・完了したdata量 | MB/sかMiB/sか、request size、持続時間、圧縮/cacheの有無 |
| request size | 1要求が扱うlogical data量 | 4KiB、64KiB、1MiB等。OS merge後のdevice要求とapp要求を区別 |
| queue depth/outstanding | ある境界で発行済み・未完了の要求数 | どのqueue/device/threadの値か、瞬間値か平均か |
IOPSとthroughputはrequest sizeでつながる
同じ測定境界で、すべての要求サイズが等しいなら、次の関係が成り立ちます。
throughput ≈ IOPS × 1回のI/Oサイズ
例:25,000 IOPS × 4 KiB
= 102,400,000 byte/s
≈ 97.7 MiB/s
同じ25,000 IOPSでも、4KiB要求と1MiB要求では運ぶdata量が256倍違います。逆に、同じ1GiBを読むにも4KiBずつなら262,144回、1MiBずつなら1,024回です。小さなrandom accessでは1回ごとのlatencyとIOPS、大きなsequential transferではthroughputが効きやすくなります。
queue depthとlatencyは同時に読む
定常状態で測定境界が同じなら、Littleの法則から次のおおよその関係を使えます。
平均outstanding I/O数 ≈ IOPS × 平均latency(秒)
例:20,000 IOPS × 0.002秒
≈ 40 requests
throughputが伸びたとしても、outstanding要求を大量に積んだ結果、p99 latencyが長くなっていれば、対話型アプリには不向きかもしれません。LinuxのI/O statistics資料でも、進行中I/O数、read/write完了数、処理時間、weighted I/O timeが別々に公開され、隣接要求のmergeにより数え方が変わると説明されています。
要求パターンが変われば、同じ装置でも結果が変わる
| 条件 | 変えると何が起きやすいか | memo.txtとの対比 |
|---|---|---|
| random/sequential | 離れたaddressへのsmall I/Oか、連続領域のlarge I/Oかで、merge、prefetch、媒体動作が変わる | 小さい設定file更新と巨大file copyは別workload |
| read/write比 | cache、flush、media管理、network方向が変わる | openはread、saveはwrite。混ぜた値を単純比較しない |
| request size | operation回数と1回の転送効率が変わる | 1文字更新でもfile全体を書き直すeditorがある |
| concurrency/QD | 並列性を使える一方、能力超過でqueue待ちが増える | 1fileの保存と、多数userの同時保存では違う |
| sync/flush頻度 | durabilityを高める同期点が増えると、待つ回数が増える | 毎回fsyncするか、まとめて同期するかで変わる |
| cache hit/miss | memoryで応答できるか、device/networkまで進むかが変わる | 直前に開いたmemo.txtはcacheにあるかもしれない |
「CPUは低いのに遅い」を切り分ける手順
I/O問題を調べるときは、最初に「どのthreadが、どの完了条件を、どの境界で待つか」を特定します。CPU使用率とdevice使用率だけを見比べても、待ちの場所は分かりません。
- 症状の単位を決める:
memo.txtのsave buttonからreturnまでか、write()か、fsync()か、device I/Oかを区別する。 - thread stateを確認する: Running、Ready、I/O wait、lock wait、network waitのどれか。CPUが低いだけではI/O待ちと断定しない。
- latencyの分布を見る: 平均だけでなくp95/p99、queue待ちとservice time、read/write/flushを分ける。
- 量と並列性を見る: IOPS、throughput、request size、outstanding/QDを同じ時間帯・同じ境界で並べる。
- 経路を一段ずつ切る: app buffer、file system/page cache、block layer、driver、device、network/remote serverのどこで時間が増えたか確認する。
- 変更前後で同じworkloadを再測定する: async化、buffer size変更、QD調整でrequest patternそのものが変わっていないかを確かめる。
| ありがちな理解 | 本質的な読み替え |
|---|---|
| 非同期化すればdeviceが高速化する | device service timeはそのままでも、待ちと別処理を重ね、並列性を使える。積み過ぎればlatencyは悪化する |
| DMA中はCPUが完全に何もしない | CPUは各byteをcopyし続けないが、setup、mapping、queue、interrupt/polling、completion、protocol処理を行う |
| CPU使用率が低いから余裕がある | CPUには余裕があっても、device、network、queue、lockがend-to-end throughputを制限し得る |
| queue depthはdevice前の待ち件数 | ある測定境界の未完了数。app、OS、driver、deviceで値は異なり、processing中も含む定義がある |
| write成功なら保存完了 | OS受理、device completion、不揮発化を分け、必要な保証に対応する同期点を確認する |
network I/Oでは、storage controllerの代わりにnetwork interfaceとprotocol stack、その先のnetwork・相手serverが経路へ加わります。packetがどのように相手へ届くかはネットワークの解説へつながります。deviceごとの物理動作は異なっても、要求、data transfer、completion、waitingという分け方は共通の診断軸になります。
まとめ:I/Oは要求・データ・完了の往復である
memo.txtへ「A」を保存すると、アプリはメモリ上のバイト列をOSへ渡し、OSはファイルシステムとI/O層で要求を組み立て、ドライバがコントローラへ投入します。データは多くの場合DMAでメモリから装置へ移り、完了は割り込みまたはポーリングでカーネルへ戻り、待っていた処理または非同期の受け手へ結果が届きます。この要求→転送→完了の往復と、どの完了時点を待つかを分けて説明できれば、I/Oの動作原理を理解したと言えます。
