このページの目次

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

基本的な使い方

#include <liburing.h>  // liburingヘルパーライブラリ

struct io_uring ring;
io_uring_queue_init(256, &ring, 0);  // SQに256エントリ

// readリクエストの準備:
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, size, offset);  // offset: 固定位置
sqe->user_data = 42;  // ユーザータグ (CQEで返される)

// リクエストの提出:
io_uring_submit(&ring);

// 完了の回収:
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);  // 完了1つを待つ(ブロッキング)
printf("res=%d, user_data=%lu\n", cqe->res, cqe->user_data);
io_uring_cqe_seen(&ring, cqe);

高度な機能

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を事前に登録:
io_uring_register_buffers(&ring, iovecs, nr_iovs);  // 固定バッファ
io_uring_register_files(&ring, fds, nr_fds);        // 固定ファイル

// その後、登録済みバッファを使用 → 毎回の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_uringlibaioPOSIX 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