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, &params);
    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コードでの実装方法を学びます。