6月20日、sibexi.coが「epoll vs io_uring in Linux」と題した記事を公開した。LinuxにおけるI/O多重化の2大メカニズム、epollとio_uringの設計思想・実装・性能の違いを、実際にリバースプロキシを開発した経験をもとに詳しく解説している。
記事の結論は明快だ。カーネル5.1以降が動く環境であれば、新規プロジェクトでepollを選ぶ理由はほとんどない。2019年に登場したio_uringは、単なる性能改善にとどまらず、I/O処理の「設計思想そのもの」を塗り替えた。CloudflareやNode.js、Rustの非同期ランタイム(Tokio)がio_uringのバックエンドを採用し始めており、このテーマを改めて整理する意義は大きい。なお、カーネル5.1以降はUbuntu 20.04 LTS(カーネル5.4)やDebian 11以降でデフォルトで利用可能であり、2026年現在、多くの本番環境でio_uringを選択できる状況にある。
epollの仕組みと限界
epollはI/Oが「可能になった」タイミングを通知する、いわゆる「readinessモデル」だ。2002年にLinuxカーネルへ取り込まれて以来、長らく高性能サーバーの標準的な選択肢だった。
開発者はepollからの通知を受けてから、自前でread()やwrite()を呼び出す必要がある。つまり1回のI/Oイベントにつき、epoll_wait + read という最低2回のシステムコールが発生する(epoll_ctlでの登録を含めると3回)。システムコールのたびにユーザーモードとカーネルモードの間でコンテキストスイッチが生じ、接続数が増えるほどこのオーバーヘッドが膨れ上がる。
記事の筆者はリバースプロキシ「TinyGate」を学生と共同開発する中でこの限界に直面した。epollで書き直した第2版では劇的な性能向上を得たものの、さらなるボトルネックを突き詰めた末にio_uringへの全面移行を決断。プロジェクトをゼロから書き直すことになったという。
io_uringが変えた設計思想
io_uringはI/Oが「完了した」タイミングを通知する、「completionモデル」だ。2019年、Linux カーネル5.1でJens Axboeによって導入され、liburingという公式ヘルパーライブラリも整備されている。設計の詳細はAxboe自身が執筆したio_uring解説PDFに詳しい。
仕組みの核心は、アプリケーションとカーネルが共有メモリ上のリングバッファを介してやりとりする点にある。アプリは操作をSubmission Queue(SQ)に積み、カーネルは完了結果をCompletion Queue(CQ)に書き戻す。データのやり取りがメモリ経由で完結するため、システムコールの回数を大幅に削減できる。
デフォルトでもio_uring_enter()1回でバッチ単位の投入・回収が可能だ。さらにIORING_SETUP_SQPOLLを使うと、カーネルスレッドがSQをポーリングし続けるため、定常状態でのシステムコールはほぼゼロになる。ただしこのモードはキューが空のときもCPUを消費し続ける点に注意が必要だ。アイドルタイムアウトsq_thread_idleで緩和できるが、コストはゼロにはならない。高スループットが常時求められる用途向けのオプションと捉えるのが現実的だ。
コードで見る差異
記事ではC言語による実装例が示されている。epollでは次の3ステップが必要だ。
epoll_create1()でインスタンス作成epoll_ctl()でファイルディスクリプタを登録epoll_wait()で待機 →read()でデータ取得
一方io_uring(liburingを使用)では、登録ステップが不要で、read()の個別呼び出しも不要になる。完了通知の中に結果が含まれているからだ。
エラーハンドリングの流儀が変わる点は要注意だ。エラーも非同期で返ってくるため、cqe->resフィールドを確認する必要があり、通常の同期的なシステムコールとは作法が異なる。epollに慣れた開発者が最初に戸惑うポイントの一つだ。
io_uring固有の機能
completionモデルの恩恵はシステムコール削減だけではない。記事では以下の機能も紹介されている。
ゼロコピーI/Oは特に帯域を使い切りたいシナリオで有効だ。io_uring_register_buffers()でバッファを事前登録しておくと、毎回のカーネルメモリ再マッピングを省ける。ネットワーク送信ではカーネル6.0以降で使えるIORING_OP_SEND_ZCにより、バッファコピー自体を省略できる。
epollの時代は終わりを迎えつつある
筆者は「7年以上前にリリースされたカーネルをまだ使っているなら、それは良い状態とは言えない」と率直に述べ、古い環境へのサポートを早めに切ることを支持している。readinessモデルからcompletionモデルへの移行は、I/O処理の大部分をアプリからカーネルに委譲するアーキテクチャ上の転換であり、高接続数を扱うサーバーほどその恩恵は大きくなる。epollが20年以上にわたって担ってきた役割は、io_uringへと着実に引き継がれつつある。
詳細はepoll vs io_uring in Linuxを参照していただきたい。