TL

非同期I/Oの実装(POSIX AIO・io_uring)

ディスクI/Oで待たされないための非同期I/O。なぜPOSIX AIOは実用にならず、libaioはO_DIRECT必須で、io_uringだけが汎用の非同期I/Oを実現できたのかを実装の中身から理解できます。

応用非同期I/Oio_uringPOSIX AIOlibaioLinuxO_DIRECT最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.POSIX AIO(glibc実装)は内部でユーザースレッドプールが同期I/Oを代行するだけで、真のカーネル非同期ではなくスケールしない。
  • 2.Linux native AIO(libaio)はカーネルで非同期化するが、O_DIRECT以外ではブロックし得る「半端な非同期」で、用途がDB等に限られた。
  • 3.io_uringはSQ/CQリングで提出と完了を分離し、登録バッファ・登録ファイルでコピーと参照カウント操作を省いて真の汎用非同期I/Oを実現した。

非同期I/Oが難しい理由

read/write は呼んだスレッドを完了までブロックします。ディスクからのデータ到着には数ミリ秒かかることもあり、その間スレッドは寝たままです。これを避ける素直な発想が非同期I/O(AIO)——「I/Oを発行したら即座に戻り、完了は後で受け取る」モデルです。ところがLinuxでこの「当たり前」を汎用に実現するのは長らく困難でした。本記事は POSIX AIO → libaio → io_uring の系譜を、各段階が何を非同期化し損ねたかから解剖します。ソケットの多重化(/os/epoll-io-uring/)とは別系統の「ストレージI/Oの非同期化」が主題です。

POSIX AIO:スレッドプールという擬態

aio_read/aio_write というPOSIX標準APIがあります。一見「カーネルが非同期でI/Oしてくれる」ように見えますが、Linuxの主要実装(glibc)の中身はユーザー空間のスレッドプールです。

  • aio_read は内部スレッドにI/O要求をキューイングして即座に戻る。
  • 別スレッドが普通の同期 pread を実行し、ブロックして待つ。
  • 完了したらシグナルやコールバックでアプリに知らせる。

つまり「非同期に見せかけているだけで、実体は同期I/Oをスレッドに肩代わりさせている」のです。これでは並行I/O数だけスレッドが要り、文脈切り替えとスレッド管理のコストがそのまま乗ります。数万の並行I/Oには到底スケールせず、POSIX AIOが本番で使われない最大の理由になりました。

POSIX AIOは「カーネル非同期」ではない

APIがPOSIX標準でも、実装がカーネルの非同期機構を使うとは限りません。glibcのPOSIX AIOはユーザースレッドで同期I/Oを代行する方式で、I/O発行のシステムコール自体は依然ブロックします。「標準API=高性能」ではない典型例です。

Linux native AIO(libaio)と O_DIRECT の制約

そこでカーネルが本物の非同期I/Oを提供したのが native AIO(システムコール io_submit/io_getevents、ライブラリは libaio)です。要求をカーネルに提出(submit)して即座に戻り、完了は別途まとめて回収します。発行スレッドはブロックしません。

しかしこの非同期化には大きな但し書きがありました——実質 O_DIRECT(ダイレクトI/O)でしか非同期にならないのです。理由はページキャッシュにあります。

  • 通常のバッファドI/Oは、読むデータがページキャッシュに無ければページ確保・ブロックI/O層への発行・ページのロック待ちなどが絡む。
  • これらの経路はカーネル内部でブロックし得る箇所を含み、native AIOの枠組みでは非同期にできず、io_submit がその場で同期的にブロックしてしまう。
  • 確実に非同期になるのは、ページキャッシュを迂回しユーザーバッファとデバイスを直結する O_DIRECT のとき。

その O_DIRECT にも厳しい制約があります。バッファのアドレス・長さ・ファイルオフセットをデバイスのブロックサイズ(通常512Bや4KB)にアラインしなければならず、ページキャッシュの恩恵(先読み・書き込みの集約)も失います。結果、libaio が活きるのは自前でキャッシュを持つデータベースなど一部に限られ、「汎用の非同期I/O」にはなり得ませんでした。

観点POSIX AIO(glibc)native AIO(libaio)
非同期化の主体ユーザースレッドプールカーネル
並行I/OのコストI/O数ぶんのスレッドスレッド非依存
バッファドI/O(同期を代行するので)一応動くブロックし得る=実質非対応
O_DIRECT不要実質必須・アライン制約あり
対象操作read/write中心read/write中心
スケール性低い中(用途限定)

io_uring:提出と完了をリングで分離する

io_uring はこの行き詰まりを、カーネルとユーザー空間で共有する2本のリングバッファで打開しました。要点は「I/Oの提出(submission)と完了(completion)を別々のキューに分離し、両者を共有メモリ上に置く」ことです。

  • SQ(Submission Queue):アプリがやってほしいI/Oを SQE(Submission Queue Entry) として書き込むリング。
  • CQ(Completion Queue):カーネルが結果を CQE(Completion Queue Entry) として書き込むリング。
ユーザー空間              共有メモリ              カーネル
   │ ①SQEを書く ──────→ [ SQ ring ] ─────→ ②I/Oを実行
   │                                          │
   │ ④CQEを読む ←────── [ CQ ring ] ←───── ③完了を書き込む

提出と完了が分離しているため、多数のI/OをまとめてSQに積み、1回の io_submit 相当のシステムコール(io_uring_enter)で投入できます。各SQEには user_data を添えられ、完了したCQEに同じ値が載るので、どの要求の結果かを照合できます。さらに O_DIRECT でなくても——カーネル内部でブロックし得るバッファドI/Oでも——カーネルワーカーに肩代わりさせることで非同期に回せるため、libaioの「O_DIRECT縛り」が外れ、ようやく汎用の非同期I/Oになりました。

提出と完了が独立キューなので、提出側と完了側のポインタ更新が干渉しません。これがロックを避けた単一生産者・単一消費者リングを成り立たせ、システムコールの固定コストをバッチ化で薄める土台になります。

リングのポインタ更新は順序が命

SQ/CQはロックフリーのリングで、tail/head の更新とメモリバリアの順序を誤ると、書き終えていないSQEをカーネルが読む・完了を取りこぼす競合が起きます。通常は liburing が正しいバリアを隠蔽するので、自前でリングを直接叩くのは避けるのが安全です。

登録バッファ・登録ファイルによる最適化

io_uring は提出経路をさらに削るために、I/Oに使う資源を事前に一度だけ登録する仕組みを持ちます。

  • 登録バッファ(IORING_REGISTER_BUFFERS:I/Oに使う固定バッファをあらかじめカーネルにピン留め登録する。通常の read/write は毎回ユーザーバッファのページを参照・固定(get_user_pages)する必要があるが、登録済みなら毎回のページピン留めを省けるIORING_OP_READ_FIXED などで使う。
  • 登録ファイル(IORING_REGISTER_FILES:使うファイルディスクリプタを配列で登録する。通常は提出のたびに fd から struct file を引き、参照カウントの増減(fget/fput)が走るが、登録済みなら毎回の参照カウント操作を省略でき、配列添字でファイルを指定できる。

どちらも「I/O1回ごとの固定コスト」を提出ループの外へ追い出す最適化です。短いI/Oを高頻度で回すワークロードほど、このオーバーヘッド削減が効きます。加えて SQPOLL モードではカーネル専用スレッドがSQリングを自分でポーリングするため、定常状態では io_uring_enter すら不要になり、提出・回収ともシステムコールゼロに近づきます。

試験・面接の頻出ポイント

「なぜ io_uring が libaio を置き換えたか」は頻出です。核心は2点——(1) 提出と完了をリングで分離しバッファドI/Oも含めて非同期化できたこと(O_DIRECT縛りの解消)、(2) 登録バッファ/登録ファイルとSQPOLLでI/Oごとの固定コストとシステムコールを削ったこと。POSIX AIOが「スレッドプールの擬態」、libaioが「O_DIRECT限定」だった点と対比して語れると強いです。

どれを選ぶか

新規の高性能ストレージI/Oなら io_uring(カーネル5.5以降で実用機能が揃う)が第一候補です。既存のDBエンジン互換や限定環境では libaio + O_DIRECT が今も現役。POSIX AIO は移植性目的の最後の手段で、性能を期待する場面では選びません。

まとめ

まとめ

非同期I/Oの実装史は「どこのブロックを誰が肩代わりするか」の探求です。POSIX AIOはユーザースレッドに同期I/Oを代行させる擬態でスケールせず、native AIO(libaio)はカーネル非同期化を果たしたもののページキャッシュ経路のブロックを避けられず実質 O_DIRECT 限定でした。io_uringはSQ/CQの共有リングで提出と完了を分離し、バッファドI/Oもワーカーに回して汎用の非同期I/Oを実現。さらに登録バッファ・登録ファイルでI/Oごとの固定コストを、SQPOLLでシステムコールそのものを削りました。ソケット側の多重化(epoll・io_uring)と合わせて理解すると、現代Linux I/Oの全体像がつながります。

OS Article

非同期I/Oの実装(POSIX AIO・io_uring)を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

非同期I/O

比較で見る軸

難易度: advanced / カテゴリ: OS / タグ数: 6

導入後に効く点

Linux native AIO(libaio)はカーネルで非同期化するが、O_DIRECT以外ではブロックし得る「半端な非同期」で、用途がDB等に限られた。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
OS
タグ数
6

判断チェックリスト

  • 自社の用途が「非同期I/O / io_uring」に近いか確認する。
  • 強みである「POSIX AIO(glibc実装)は内部でユーザースレッドプールが同期I/Oを代行するだけで、真のカーネル非同期ではなくスケールしない。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

非同期I/Oio_uringPOSIX AIOlibaioLinux非同期I/Oio_uringPOSIX AIO
参考: 公式情報