このページの目次
io_uring: 革命的な非同期I/O
カバー範囲: SQ/CQリング → 固定バッファ → ポーリングIO → libaio/posix aioとの比較 → パフォーマンスの考慮事項 対象: Linux 5.1+
概要
io_uringはLinux 5.1で導入された非同期I/Oフレームワークであり、LinuxにおけるI/Oの扱い方を根本から変えました。ユーザー空間とカーネルの間で、2つの共有メモリリングバッファ(SQ/CQ)を使用してI/Oリクエストと完了通知をやり取りします。システムコールを使用せずにI/Oの提出と回収が可能です。最適な条件下では、io_uringは単一コアで約6M IOPSを達成できますが、従来のAIOでは約1Mです。
リングバッファモデル
flowchart LR
subgraph USER["ユーザー空間"]
APP["App<br/>SQへの書き込み / CQの読み取り"]
end
subgraph SHARED["共有メモリ (mmap、ゼロコピー)"]
SQ["Submission Queue<br/>I/Oリクエストキュー"]
CQ["Completion Queue<br/>I/O完了キュー"]
end
subgraph KERNEL["カーネル空間"]
K["Kernel<br/>SQの読み取り / CQへの書き込み"]
end
APP -->|"① SQEへの書き込み"| SQ
SQ -->|"② カーネルが読み取り"| K
K -->|"③ CQEへの書き込み"| CQ
CQ -->|"④ Appが回収"| APP
classDef user fill:#e3f2fd,stroke:#1565c0
classDef shared fill:#fff3e0,stroke:#ef6c00
classDef kernel fill:#f3e5f5,stroke:#7b1fa2
class APP user
class SQ,CQ shared
class K kernel
基本的な使い方
struct io_uring ring;
; // SQに256エントリ
// readリクエストの準備:
struct io_uring_sqe *sqe = ;
; // offset: 固定位置
sqe->user_data = 42; // ユーザータグ (CQEで返される)
// リクエストの提出:
;
// 完了の回収:
struct io_uring_cqe *cqe;
; // 完了1つを待つ(ブロッキング)
;
;
高度な機能
SQポーリング(カーネル側でのビジーウェイト)
// io_uring_setup(..., IORING_SETUP_SQPOLL)
// カーネルがkthreadを起動し、SQを継続的にポーリングする → リクエストがあれば即座に処理
// → io_uring_submit()が不要 → システムコールゼロ!
// → io_uring_enter(ring_fd, 0, 0, IORING_ENTER_GETEVENTS, NULL)
// 完了のみ回収し、提出は行わない(提出はSQポラーターが自動で行う)
// 代償: SQポラータースレッドがCPUを継続的に占有する(1コアの5-10%)
// 適した用途: I/Oが継続的に発生するシナリオ(データベース、ファイルサーバー)
固定バッファ / 固定ファイル
// バッファとfdを事前に登録:
; // 固定バッファ
; // 固定ファイル
// その後、登録済みバッファを使用 → 毎回のI/Oでget_user_pagesを回避
// その後、固定fdを使用 → 毎回のI/Oでfget/fputを回避
// → さらなるレイテンシの低減(カーネルオーバーヘッドを約30-50%削減)
IORING_SETUP_DEFER_TASKRUN
// デフォルト: CQE → カーネルが割り込みコンテキストで完了をマーク
// DEFER_TASKRUN: カーネルがI/O完了処理を遅延させ → 次のio_uring_enter呼び出し時にバッチ処理
// → 割り込みの削減 + バッチ処理 → より高いスループット
aioとの比較
| io_uring | libaio | POSIX aio | |
|---|---|---|---|
| 完了通知 | CQのポーリング | io_getevents() | シグナル / コールバック |
| バッファ | 事前登録または毎回の指定 | 毎回の指定 | 毎回の指定 |
| バッファリングI/O | サポートあり (5.6以降) | サポートなし (O_DIRECTのみ) | サポートあり |
| システムコール | 理論上0(SQポーリング時) | 提出ごと + 回収ごと | スレッドプールベース |
| パフォーマンス (4Kランダムリード) | ~6M IOPS | ~1M IOPS | ~0.1M IOPS |
パフォーマンスの落とし穴
1. CQEの回収: io_uring_wait_cqeは1回で1つしか回収しない → 複数の回収にはio_uring_wait_cqesを使用
2. SQが満杯: io_uring_get_sqeがNULLを返す → まずio_uring_submitを実行してから再試行
3. 大きなSQ深度: 256-512で十分 → より大きな深度はメモリを無駄にするだけ
4. リンクタイムアウト: 各リクエストにタイムアウトを設定 → IORING_OP_LINK_TIMEOUT
→ 低速I/Oがリング全体をブロックするのを防止
参考
- liburing: https://github.com/axboe/liburing (examples + tests)
- カーネルソース:
fs/io_uring.c(約20000行) - LWN: "The io_uring API", "io_uring performance improvements"
キーワード: io_uring, SQ, CQ, SQE, CQE, fixed buffers, SQPOLL, DEFER_TASKRUN, libaio