Chapter 03: io_uring Deep-dive
学習目標
このチャプターを完了すると、以下のことができるようになります:
- io_uringの基本アーキテクチャを理解する
- Submission Queue (SQ) と Completion Queue (CQ) の動作原理を説明できる
- io_uring_setup と io_uring_enter システムコールを使用できる
- Zero-copy I/O とRegistered Buffersの利点を理解する
- epollとio_uringの性能比較ができる
io_uringとは
io_uringは、Linuxカーネル5.1で導入された新しい非同期I/Oインターフェースです。従来のepoll/select/pollやLinux AIOの欠点を克服し、高性能かつ柔軟な非同期I/Oを実現します。
従来のI/O手法の課題
epoll/select/pollの問題点:
- システムコールのオーバーヘッドが大きい
- ファイルディスクリプタごとにepoll_ctl呼び出しが必要
- データコピーのオーバーヘッド
- ネットワークI/Oに特化、ファイルI/Oには不向き
Linux AIOの問題点:
- ダイレクトI/O (O_DIRECT) でのみ真の非同期性を発揮
- バッファドI/Oでは同期的にブロックする可能性
- API が複雑で使いにくい
- 対応している操作が限定的
io_uringの利点:
- システムコール回数を最小化(場合によってはゼロ)
- ユーザー空間とカーネル空間でメモリを共有
- あらゆる種類のI/O操作をサポート
- チェイン操作、ポーリングモードなど高度な機能
- epollより2-3倍高速(ベンチマーク結果による)
io_uringのアーキテクチャ
io_uringは2つの環状バッファ(リングバッファ)を使用してユーザー空間とカーネル空間間で通信します。
ユーザー空間 カーネル空間
┌─────────────────┐ ┌─────────────────┐
│ │ │ │
│ Application │ │ Kernel I/O │
│ │ │ Worker │
│ ┌──────────┐ │ │ ┌──────────┐ │
│ │Submission│───┼──────────┼──→│Submission│ │
│ │ Queue │ │ Submit │ │ Queue │ │
│ │ (SQ) │ │ │ │ (SQ) │ │
│ └──────────┘ │ │ └──────────┘ │
│ │ │ │ │
│ ┌──────────┐ │ │ ↓ │
│ │Completion│←──┼──────────┼───┐ Process │
│ │ Queue │ │ Complete │ │ │
│ │ (CQ) │ │ │ ↓ │
│ └──────────┘ │ │ ┌──────────┐ │
│ │ │ │Completion│ │
│ │ │ │ Queue │ │
│ │ │ │ (CQ) │ │
└─────────────────┘ │ └──────────┘ │
│ │
└─────────────────┘
Submission Queue (SQ)
アプリケーションがカーネルに対してI/O要求を送信するためのリングバッファです。
構成要素:
- SQ Ring: インデックスを保持する環状バッファ
- SQE Array: 実際のSubmission Queue Entry(SQE)を格納する配列
- head: カーネルが次に処理するエントリのインデックス
- tail: アプリケーションが次に書き込むエントリのインデックス
SQ Ring構造:
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ ← インデックス配列
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
↑ ↑
head tail
(読み取り位置) (書き込み位置)
各インデックスはSQE Arrayの実際のエントリを参照:
SQE Array:
┌─────────────────────┐
│ SQE[0]: read(fd=3) │
├─────────────────────┤
│ SQE[1]: write(fd=4) │
├─────────────────────┤
│ SQE[2]: accept() │
├─────────────────────┤
│ ... │
└─────────────────────┘
Completion Queue (CQ)
カーネルがアプリケーションに対してI/O完了を通知するためのリングバッファです。
構成要素:
- CQ Ring: Completion Queue Entry(CQE)を直接格納する環状バッファ
- head: アプリケーションが次に読み取るエントリのインデックス
- tail: カーネルが次に書き込むエントリのインデックス
CQ Ring構造:
┌───────────────┬───────────────┬───────────────┬───────────────┐
│ CQE[0] │ CQE[1] │ CQE[2] │ CQE[3] │
│ user_data: 42 │ user_data: 43 │ user_data: 44 │ user_data: 45 │
│ res: 1024 │ res: 512 │ res: -EAGAIN │ res: 2048 │
│ flags: 0 │ flags: 0 │ flags: 0 │ flags: 0 │
└───────────────┴───────────────┴───────────────┴───────────────┘
↑ ↑
head tail
(読み取り位置) (書き込み位置)
メモリマッピング
io_uringはmmap()を使用してユーザー空間とカーネル空間でメモリを共有します。
┌──────────────────────────────────────────┐
│ io_uring File Descriptor │
└──────────────────────────────────────────┘
│
┌───────────┴───────────┐
↓ ↓
┌──────────────┐ ┌──────────────┐
│ SQ mmap │ │ CQ mmap │
│ (offset: 0) │ │ (offset: ...) │
└──────────────┘ └──────────────┘
│ │
↓ ↓
Shared Memory Shared Memory
(User + Kernel) (User + Kernel)
Submission Queue Entry (SQE)
SQEは、実行するI/O操作の詳細を記述する構造体です。
SQE構造体
struct io_uring_sqe {
__u8 opcode; // 操作タイプ (IORING_OP_READ, WRITE, etc.)
__u8 flags; // フラグ (IOSQE_FIXED_FILE, etc.)
__u16 ioprio; // I/O優先度
__s32 fd; // ファイルディスクリプタ
union {
__u64 off; // オフセット (read/write用)
__u64 addr2;
};
union {
__u64 addr; // バッファアドレス
__u64 splice_off_in;
};
__u32 len; // バッファ長
union {
__kernel_rwf_t rw_flags;
__u32 fsync_flags;
__u16 poll_events;
__u32 sync_range_flags;
__u32 msg_flags;
__u32 timeout_flags;
__u32 accept_flags;
__u32 cancel_flags;
__u32 open_flags;
__u32 statx_flags;
__u32 fadvise_advice;
__u32 splice_flags;
};
__u64 user_data; // ユーザー定義データ (CQEで返される)
union {
__u16 buf_index; // Registered buffer index
__u16 buf_group;
__u16 personality;
__s32 splice_fd_in;
};
union {
__u16 addr_len;
__u16 __pad3[1];
};
__u64 __pad2[3];
};
主要な操作タイプ (opcode)
// io_uring operation codes
pub const IORING_OP_NOP = 0; // No operation
pub const IORING_OP_READV = 1; // readv()
pub const IORING_OP_WRITEV = 2; // writev()
pub const IORING_OP_FSYNC = 3; // fsync()
pub const IORING_OP_READ_FIXED = 4; // Fixed buffer read
pub const IORING_OP_WRITE_FIXED = 5; // Fixed buffer write
pub const IORING_OP_POLL_ADD = 6; // Poll for events
pub const IORING_OP_POLL_REMOVE = 7; // Remove poll
pub const IORING_OP_SYNC_FILE_RANGE = 8;
pub const IORING_OP_SENDMSG = 9; // sendmsg()
pub const IORING_OP_RECVMSG = 10; // recvmsg()
pub const IORING_OP_TIMEOUT = 11; // Timeout operation
pub const IORING_OP_TIMEOUT_REMOVE = 12;
pub const IORING_OP_ACCEPT = 13; // accept()
pub const IORING_OP_ASYNC_CANCEL = 14;
pub const IORING_OP_LINK_TIMEOUT = 15;
pub const IORING_OP_CONNECT = 16; // connect()
pub const IORING_OP_FALLOCATE = 17;
pub const IORING_OP_OPENAT = 18; // openat()
pub const IORING_OP_CLOSE = 19; // close()
pub const IORING_OP_READ = 22; // read()
pub const IORING_OP_WRITE = 23; // write()
pub const IORING_OP_SEND = 24; // send()
pub const IORING_OP_RECV = 25; // recv()
Zigでの基本的なSQEの作成
const std = @import("std");
const linux = std.os.linux;
// SQEを準備する関数
fn prepareSQE(
sqe: *linux.io_uring_sqe,
opcode: u8,
fd: i32,
addr: u64,
len: u32,
offset: u64,
user_data: u64,
) void {
sqe.* = std.mem.zeroes(linux.io_uring_sqe);
sqe.opcode = opcode;
sqe.fd = fd;
sqe.addr = addr;
sqe.len = len;
sqe.off = offset;
sqe.user_data = user_data;
}
// Read操作のSQE作成例
fn prepareRead(
sqe: *linux.io_uring_sqe,
fd: i32,
buffer: []u8,
offset: u64,
user_data: u64,
) void {
prepareSQE(
sqe,
linux.IORING_OP_READ,
fd,
@intFromPtr(buffer.ptr),
@intCast(buffer.len),
offset,
user_data,
);
}
Completion Queue Entry (CQE)
CQEは、完了したI/O操作の結果を記述する構造体です。
CQE構造体
struct io_uring_cqe {
__u64 user_data; // SQEで設定したuser_data
__s32 res; // 操作の結果 (成功時: バイト数, 失敗時: 負のエラーコード)
__u32 flags; // フラグ
};
Zigでの基本的なCQEの処理
// CQEを処理する関数
fn processCQE(cqe: *const linux.io_uring_cqe) !void {
const user_data = cqe.user_data;
const result = cqe.res;
if (result < 0) {
// エラーが発生
const err = @as(linux.E, @enumFromInt(-result));
std.log.err("I/O operation failed: user_data={}, error={s}",
.{ user_data, @tagName(err) });
return error.IOError;
}
// 成功: resultはバイト数
std.log.info("I/O operation completed: user_data={}, bytes={}",
.{ user_data, result });
}
io_uringシステムコール
io_uringを使用するには、主に2つのシステムコールを使用します。
io_uring_setup
io_uringインスタンスを作成し、ファイルディスクリプタを返します。
シグネチャ:
int io_uring_setup(u32 entries, struct io_uring_params *params);
パラメータ:
entries: SQのエントリ数(2の累乗が推奨)params: io_uringの設定パラメータ
io_uring_params構造体:
struct io_uring_params {
__u32 sq_entries; // SQエントリ数(出力)
__u32 cq_entries; // CQエントリ数(出力)
__u32 flags; // フラグ
__u32 sq_thread_cpu; // SQ polling用CPU
__u32 sq_thread_idle; // SQ thread idle時間
__u32 features; // カーネル機能フラグ(出力)
__u32 wq_fd; // ワークキューFD
__u32 resv[3];
struct io_sqring_offsets sq_off; // SQメモリオフセット
struct io_cqring_offsets cq_off; // CQメモリオフセット
};
主要なフラグ:
pub const IORING_SETUP_IOPOLL = 1 << 0; // Busy-wait for I/O completion
pub const IORING_SETUP_SQPOLL = 1 << 1; // SQ polling thread
pub const IORING_SETUP_SQ_AFF = 1 << 2; // sq_thread_cpu is valid
pub const IORING_SETUP_CQSIZE = 1 << 3; // cq_entries is valid
pub const IORING_SETUP_CLAMP = 1 << 4; // Clamp SQ/CQ sizes
pub const IORING_SETUP_ATTACH_WQ = 1 << 5; // Attach to existing wq
Zigでの使用例:
const std = @import("std");
const linux = std.os.linux;
fn setupIoUring(entries: u32) !i32 {
var params = std.mem.zeroes(linux.io_uring_params);
const fd = linux.io_uring_setup(entries, ¶ms);
if (fd < 0) {
return error.IoUringSetupFailed;
}
std.log.info("io_uring setup: fd={}, sq_entries={}, cq_entries={}",
.{ fd, params.sq_entries, params.cq_entries });
return @intCast(fd);
}
io_uring_enter
SQEを送信し、CQEを取得します。
シグネチャ:
int io_uring_enter(
int fd,
u32 to_submit,
u32 min_complete,
u32 flags,
sigset_t *sig
);
パラメータ:
fd: io_uring_setupで取得したファイルディスクリプタto_submit: 送信するSQEの数min_complete: 待機する最小CQE数flags: 動作フラグsig: シグナルマスク(通常はNULL)
主要なフラグ:
pub const IORING_ENTER_GETEVENTS = 1 << 0; // CQEを取得
pub const IORING_ENTER_SQ_WAKEUP = 1 << 1; // SQ polling threadを起動
pub const IORING_ENTER_SQ_WAIT = 1 << 2; // SQに空きができるまで待機
Zigでの使用例:
fn submitAndWait(
ring_fd: i32,
to_submit: u32,
min_complete: u32,
) !u32 {
const ret = linux.io_uring_enter(
@intCast(ring_fd),
to_submit,
min_complete,
linux.IORING_ENTER_GETEVENTS,
null,
);
if (ret < 0) {
return error.IoUringEnterFailed;
}
return @intCast(ret);
}
io_uring_register
高度な機能(固定バッファ、固定ファイルなど)を登録します。
シグネチャ:
int io_uring_register(
int fd,
unsigned int opcode,
void *arg,
unsigned int nr_args
);
主要な操作コード:
pub const IORING_REGISTER_BUFFERS = 0; // バッファを登録
pub const IORING_UNREGISTER_BUFFERS = 1; // バッファ登録を解除
pub const IORING_REGISTER_FILES = 2; // ファイルを登録
pub const IORING_UNREGISTER_FILES = 3; // ファイル登録を解除
pub const IORING_REGISTER_EVENTFD = 4; // eventfdを登録
pub const IORING_UNREGISTER_EVENTFD = 5; // eventfd登録を解除
基本的な使用フロー
io_uringの典型的な使用パターンを示します。
1. Setup Phase:
┌──────────────────────┐
│ io_uring_setup() │
│ - Create ring │
│ - Get params │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ mmap() SQ/CQ rings │
│ - Map shared memory │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ Optional: │
│ io_uring_register() │
│ - Register buffers │
│ - Register files │
└──────────┬───────────┘
↓
2. Operation Phase (loop):
┌──────────────────────┐
│ Get SQE │
│ - Find free slot │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ Prepare SQE │
│ - Set opcode │
│ - Set parameters │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ io_uring_enter() │
│ - Submit SQEs │
│ - Wait for CQEs │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ Process CQEs │
│ - Check results │
│ - Handle completions│
└──────────┬───────────┘
↓
(repeat)
↓
3. Cleanup Phase:
┌──────────────────────┐
│ munmap() rings │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ close(ring_fd) │
└──────────────────────┘
まとめ
このチャプターでは、io_uringの基本的なアーキテクチャとコンポーネントについて学びました:
- 2つのリングバッファ: Submission Queue (SQ) と Completion Queue (CQ)
- 共有メモリ: ユーザー空間とカーネル空間でメモリを共有し、コピーを削減
- SQE: I/O操作のリクエストを記述
- CQE: I/O操作の完了結果を取得
- システムコール: io_uring_setup, io_uring_enter, io_uring_register
次の説明編では、これらの概念をより深く掘り下げ、実際のZigコードでの実装方法を学びます。