この記事のポイント
- io_uring は Linux カーネル 5.1 以降に実装された非同期 I/O API。従来の
epoll+read/writeより大幅に少ないシステムコールで高スループットを実現します - Ubuntu 24.04(kernel 6.8)では
io_uring_disabled=0でデフォルト有効。liburing-dev 2.5を apt で入れれば C 言語からすぐ使えます(実測) - liburing の4ステップ(init → SQE セット → submit → wait_cqe)を押さえれば、ファイル・ソケット・タイムアウト・キャンセルを同じインターフェースで扱えます
- Ubuntu 22.04 では liburing 2.1、Ubuntu 24.04 では liburing 2.5 が apt で入ります。サポートされる IORING_OP は kernel 6.8 で 55 種(実測)
「非同期 I/O をちゃんと使いたいんだけど、epoll の書き方がわかりづらい」「Rust の Tokio や Python の asyncio が内部で使っているという io_uring、実際どういう仕組みなの?」
io_uring は 2019 年に Linux カーネルに加わった非同期 I/O の仕組みです。カーネルとユーザー空間のあいだに共有のリングバッファを置き、システムコールの回数を最小にしながら大量の I/O 操作をキューに積んでまとめて処理できます。本記事では Ubuntu 24.04 の実環境で 実際に C プログラムを動かして、io_uring がどう機能するかを確かめます。
動作確認環境
本記事のコマンドはすべて docker run –rm ubuntu:24.04(一部 –privileged)で実行しています。カーネル 6.8.0-83-generic、liburing 2.5-1build1 で動作確認済みです(2026-06-20)。
io_uring が登場した背景
Linux の I/O には長い歴史があります。同期 I/O(read/write)は呼ぶたびに処理を止めてカーネルに飛びます。非同期版として aio(POSIX AIO、io_submit)も存在しますが、API が複雑なうえ、ファイル操作に限定されていてソケットには使えませんでした。
Jens Axboe(Linux のブロックレイヤーを長年担当している開発者)が 2019 年に提案した io_uring は、この問題をシンプルなリングバッファ設計で解決しました。
従来の epoll + read/write との違い
| 方式 | システムコール数(1操作あたり) | ファイル対応 | ソケット対応 | バッファゼロコピー |
|---|---|---|---|---|
read / write(同期) |
操作ごとに 1 回 | ◎ | ◎ | × |
epoll + read |
epoll_wait + read = 2 回 |
△(pipe のみ) | ◎ | × |
POSIX AIO (io_submit) |
操作ごとに 1 回 | ◎ | × | × |
| io_uring | バッチまとめて 1 回(or 0 回) | ◎ | ◎ | ◎(fixed buffer) |
io_uring の最大の特徴は、複数の I/O 操作をキューに積んでから io_uring_enter() 一発でカーネルに渡せる点です。SQPOLL モードを使えばシステムコールが 0 回になることもあります(カーネルスレッドがリングを監視するため)。
io_uring の仕組み(リングバッファ構造)

上の図は Ubuntu 24.04(kernel 6.8.0-83-generic)で io_uring の状態を確認したものです。/proc/sys/kernel/io_uring_disabled が 0 なので、デフォルトで有効になっています。
SQ(Submission Queue)と CQ(Completion Queue)
io_uring の核心は、カーネルとユーザー空間が mmap で共有する 2 つのリングバッファです。
- SQ(Submission Queue):ユーザーが「やってほしい操作」を書き込む。SQE(Submission Queue Entry)1 個 = 操作 1 件
- CQ(Completion Queue):カーネルが「終わった操作」の結果を書き込む。CQE(Completion Queue Entry)で結果コードと共に返ってくる
ユーザーはこの流れを繰り返すだけです。
# SQ・CQ のリングバッファをカーネルに確保させる
② sqe = io_uring_get_sqe(&ring)
# 空の SQE スロットを取得
io_uring_prep_read(sqe, fd, buf, len, offset)
# 操作の種類と引数を SQE に書き込む
③ io_uring_submit(&ring)
# SQ に積んだ操作をカーネルへ一括投入(io_uring_enter syscall)
④ io_uring_wait_cqe(&ring, &cqe)
# CQ に完了イベントが来るまで待つ
# cqe->res に戻り値(読み取りバイト数など)が入る
io_uring_cqe_seen(&ring, cqe) # CQE を消費したとマーク
⑤ io_uring_queue_exit(&ring)
# リングを解放
システムコール番号(kernel 6.8 実測)
io_uring が使うシステムコールは 3 つだけです。Docker コンテナ内で実際に確認しました。
#include <stdio.h>
#include <asm/unistd.h>
int main() {
printf(“io_uring_setup = %d\n”, __NR_io_uring_setup);
printf(“io_uring_enter = %d\n”, __NR_io_uring_enter);
printf(“io_uring_register= %d\n”, __NR_io_uring_register);
}
$ gcc -o syscall_num syscall_num.c && ./syscall_num
io_uring_setup = 425
io_uring_enter = 426
io_uring_register= 427
io_uring_setup(425)がリング作成、io_uring_enter(426)が投入と待機、io_uring_register(427)がバッファ固定登録です。liburing を使うとこれらは隠蔽され、開発者は直接 syscall を呼ぶ必要がありません。
Ubuntu への liburing インストール

Ubuntu 24.04 では apt で liburing が入ります。実際にインストールしたログが上の図です。
$ sudo apt install -y liburing-dev
Setting up liburing2:amd64 (2.5-1build1) …
Setting up liburing-dev:amd64 (2.5-1build1) …
$ dpkg -l liburing-dev liburing2
ii liburing-dev:amd64 2.5-1build1 Linux kernel io_uring access library – development files
ii liburing2:amd64 2.5-1build1 Linux kernel io_uring access library – shared library
$ ls /usr/include/liburing.h
/usr/include/liburing.h
Ubuntu 22.04 と 24.04 の違い

Ubuntu のバージョンで入る liburing が変わります。実測結果が上の表です。22.04 では liburing 2.1、24.04 では 2.5 が apt の候補になっていました。liburing 2.5 では io_uring_prep_read_fixed など固定バッファ関連の API が使いやすくなっています。
最小サンプル:C 言語で io_uring を使う
実際に C プログラムで io_uring を動かしてみます。コンパイルには -luring フラグが必要です。
#include <string.h>
#include <liburing.h>
#include <fcntl.h>
#include <unistd.h>
#define QUEUE_DEPTH 1
#define BUF_SIZE 128
int main() {
struct io_uring ring;
char buf[BUF_SIZE + 1];
memset(buf, 0, sizeof(buf));
/* ① リング初期化(キュー深さ 1) */
io_uring_queue_init(QUEUE_DEPTH, &ring, 0);
/* ② SQE を取得して READ 操作をセット */
int fd = open(“/proc/version”, O_RDONLY);
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, BUF_SIZE, 0);
/* ③ カーネルへ投入 */
io_uring_submit(&ring);
/* ④ 完了を待って結果を取り出す */
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
printf(“Read %d bytes: %.*s\n”, cqe->res, cqe->res, buf);
io_uring_cqe_seen(&ring, cqe);
/* ⑤ クリーンアップ */
io_uring_queue_exit(&ring);
close(fd);
return 0;
}
$ ./iouring_demo
io_uring initialized OK (queue depth=1)
Async read: 128 bytes
Content: Linux version 6.8.0-83-generic (buildd@lcy02-amd64-026) …
io_uring cleanup OK

上の図が実際のビルド・実行結果です。/proc/version の先頭 128 バイトを io_uring 経由で非同期読み取りできました。kernel 6.8.0-83-generic、ubuntu:24.04 の Docker コンテナで確認しています。
サポートされる操作一覧(Ubuntu 24.04 実測)

kernel 6.8 で io_uring が対応している操作数を io_uring_get_probe_ring() で調べると、55 種の IORING_OP が利用可能でした。ファイル操作だけでなく、ソケットの accept/send/recv、タイムアウト制御、操作キャンセルまでカバーしています。
Supported IORING_OP count: 55
NOP supported=1
READV supported=1
WRITEV supported=1
READ supported=1
WRITE supported=1
SEND supported=1
RECV supported=1
ACCEPT supported=1
CONNECT supported=1
TIMEOUT supported=1
ASYNC_CANCEL supported=1
io_uring の高度な使い方
①複数操作をバッチで投入する
io_uring の威力が発揮されるのは、複数の I/O をまとめて投入するときです。
for (int i = 0; i < 10; i++) {
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fds[i], bufs[i], BUF_SIZE, 0);
io_uring_sqe_set_data(sqe, (void *)(uintptr_t)i); /* タグ付け */
}
io_uring_submit(&ring); /* syscall 1 回で 10 件まとめて投入 */
/* 完了をひとつずつ回収 */
for (int i = 0; i < 10; i++) {
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
int idx = (int)(uintptr_t)io_uring_cqe_get_data(cqe);
process(idx, cqe->res);
io_uring_cqe_seen(&ring, cqe);
}
②固定バッファ(fixed buffers)で性能を上げる
io_uring_register_buffers() でバッファをあらかじめカーネルに登録しておくと、毎回の操作でページのピン止め処理を省けます。大量の小さな I/O を繰り返すサーバーで効果があります。
io_uring_register_buffers(&ring, &iov, 1); /* バッファをカーネルに固定登録 */
/* 通常の prep_read の代わりに prep_read_fixed を使う */
io_uring_prep_read_fixed(sqe, fd, buf, BUF_SIZE, 0, 0 /* buf_index */);
③Python / Rust / Go から io_uring を使う
C 以外の言語からも io_uring を利用できます。
| 言語 | ライブラリ / ランタイム | io_uring 利用方法 |
|---|---|---|
| Rust | tokio |
tokio-uring クレートまたは Tokio が内部で使用 |
| Python | asyncio |
Python 3.12 以降の asyncio が Linux 上で io_uring を自動使用 |
| Go | net パッケージ | Go runtime が epoll を使用(io_uring 対応は実験段階) |
| C / C++ | liburing |
直接 API 呼び出し(本記事の方法) |
セキュリティ設定:io_uring_disabled
io_uring はカーネルの機能を直接扱うため、コンテナ環境では制限されることがあります。/proc/sys/kernel/io_uring_disabled で動作を制御できます。
| io_uring_disabled の値 | 意味 | 用途 |
|---|---|---|
0(デフォルト) |
全ユーザーが使用可 | 通常の開発・本番環境 |
1 |
特権ユーザーのみ使用可 | セキュリティを強めたい場合 |
2 |
完全無効 | セキュリティポリシーで禁止する場合 |
0
# Ubuntu 24.04 のデフォルトは 0(全ユーザーが使用可)
$ cat /proc/sys/kernel/io_uring_group
-1
# -1 = グループ制限なし(全員使用可)
# 制限する場合(一時的):
$ sudo sysctl -w kernel.io_uring_disabled=1
kernel.io_uring_disabled = 1
Docker コンテナでの注意点
デフォルトの Docker seccomp プロファイルは io_uring の syscall を制限します。コンテナ内で io_uring を使う場合は --privileged または --security-opt seccomp=unconfined が必要です(本記事の C デモは --privileged で実行)。
よくある疑問と実測で分かったこと
Q1. epoll との使い分けは?
epoll はソケットの「状態監視」に特化しています(読み取り可能になったら通知する)。一方 io_uring は「操作そのもの」を非同期に投入します。
- ソケット多重化だけなら epoll でも十分
- ファイル・ソケット両方を同じ API で扱いたい、またはシステムコール回数を削りたいなら io_uring
Q2. カーネルが古いと使えない?
io_uring は Linux 5.1 で導入されました。Ubuntu 20.04(kernel 5.4)以降なら基本機能は使えます。ただし機能追加はカーネルごとに進んでいて、固定バッファのゼロコピーや SQPOLL モードの安定化は 5.5 以降、多くの高度な OP は 5.7〜5.11 で揃ってきました。本記事で確認した 55 ops は kernel 6.8 での数値です。
Q3. Rust の tokio-uring と通常の tokio はどう違う?
通常の tokio は Linux では epoll ベースで動きます。tokio-uring は tokio の上に io_uring を直接使う実験的クレートです。本番で使える安定性は 2026 年時点ではまだ tokio 本体のほうが高いです。
まとめ
- Ubuntu 24.04(kernel 6.8)では io_uring がデフォルトで有効(
/proc/sys/kernel/io_uring_disabled=0・実測) sudo apt install liburing-devでバージョン 2.5-1build1 が入り、C から即座に使える(実測)- 4 ステップ(init → SQE セット → submit → wait_cqe)で非同期 I/O を実装できる。実際に
/proc/versionを 128 バイト読み取ることを確認した - kernel 6.8 では 55 種の IORING_OP をサポート。ファイル・ソケット・タイムアウト・キャンセルをすべて同じインターフェースで扱える(実測)
- 高スループットが必要なサーバーを自分の VPS で動かすなら、io_uring は選択肢として押さえておく価値があります
io_uring は「非同期 I/O をちゃんと使いたい」と思ったときに、最もストレートに答えてくれる現代的な Linux の機能です。C から入るのが一番仕組みが見えやすいですが、Python や Rust からも使えるので、まずは小さなプログラムで動かしてみるのがいいと思います。
VPS でサーバーを動かしながら io_uring のパフォーマンスを実測してみたい方は、 も合わせてどうぞ。



コメント