Exercise 03: io_uring Deep-dive

エクササイズの目的

このエクササイズでは、io_uringの知識を実践的なプロジェクトで活用します。3つの段階的なプロジェクトを通じて、io_uringの基礎から高度な機能までをマスターします。

プロジェクト構成

exercise-03-io-uring/
├── src/
│   ├── io_uring.zig          # IoUringラッパー (mandatory)
│   ├── buffer_pool.zig       # バッファプール (mandatory)
│   ├── file_copy.zig         # ファイルコピーツール (mandatory)
│   ├── echo_server.zig       # Echoサーバー (bonus)
│   └── http_server.zig       # HTTPサーバー (bonus)
├── tests/
│   ├── io_uring_test.zig
│   ├── buffer_pool_test.zig
│   └── integration_test.zig
├── benchmark/
│   └── perf_comparison.zig   # epollとの性能比較
└── build.zig

Part 1: IoUringラッパーの実装 (30点)

要件

基本的なio_uringラッパーを実装してください。

マンダトリー機能:

  • Ring初期化 (10点)
- init(): io_uring_setupとmmap - SQ/CQ/SQEのメモリマッピング - エラーハンドリング

  • SQE管理 (10点)
- getSQE(): 空きSQEの取得 - キューフル検出 - Tail pointer管理

  • Submit & Wait (10点)
- submit(): SQEの送信 - submitAndWait(): 送信と待機 - waitCQE(): CQEの取得 - cqeSeen(): CQEの消費

実装テンプレート

const std = @import("std");
const linux = std.os.linux;
const os = std.os;

pub const IoUring = struct {
    fd: i32,
    params: linux.io_uring_params,
    sq: SubmissionQueue,
    cq: CompletionQueue,
    sqes: []linux.io_uring_sqe,

    pub const SubmissionQueue = struct {
        // TODO: 必要なフィールドを追加
        head: *u32,
        tail: *u32,
        // ...
    };

    pub const CompletionQueue = struct {
        // TODO: 必要なフィールドを追加
        head: *u32,
        tail: *u32,
        // ...
    };

    const Self = @This();

    pub fn init(entries: u32, flags: u32) !Self {
        // TODO: io_uring_setupを実装
        var params = std.mem.zeroes(linux.io_uring_params);
        params.flags = flags;

        const fd = linux.io_uring_setup(entries, &params);
        if (fd < 0) {
            return error.SetupFailed;
        }

        var self = Self{
            .fd = @intCast(fd),
            .params = params,
            .sq = undefined,
            .cq = undefined,
            .sqes = undefined,
        };

        // TODO: SQ/CQ/SQEをmmap
        try self.mapSubmissionQueue();
        try self.mapCompletionQueue();
        try self.mapSQEs();

        return self;
    }

    fn mapSubmissionQueue(self: *Self) !void {
        // TODO: 実装
    }

    fn mapCompletionQueue(self: *Self) !void {
        // TODO: 実装
    }

    fn mapSQEs(self: *Self) !void {
        // TODO: 実装
    }

    pub fn getSQE(self: *Self) ?*linux.io_uring_sqe {
        // TODO: 実装
        // ヒント: tail - headでキューの使用量を計算
        _ = self;
        return null;
    }

    pub fn submit(self: *Self) !u32 {
        // TODO: 実装
        _ = self;
        return 0;
    }

    pub fn submitAndWait(self: *Self, wait_nr: u32) !u32 {
        // TODO: io_uring_enterを実装
        _ = self;
        _ = wait_nr;
        return 0;
    }

    pub fn waitCQE(self: *Self) !*linux.io_uring_cqe {
        // TODO: 実装
        _ = self;
        return error.NotImplemented;
    }

    pub fn cqeSeen(self: *Self) void {
        // TODO: headポインタを更新
        _ = self;
    }

    pub fn deinit(self: *Self) void {
        // TODO: munmapとclose
        _ = self;
    }
};

テストケース

const std = @import("std");
const testing = std.testing;
const IoUring = @import("io_uring.zig").IoUring;

test "IoUring initialization" {
    var ring = try IoUring.init(32, 0);
    defer ring.deinit();

    try testing.expect(ring.params.sq_entries >= 32);
    try testing.expect(ring.params.cq_entries >= 32);
}

test "SQE management" {
    var ring = try IoUring.init(4, 0);
    defer ring.deinit();

    // Get all available SQEs
    var count: usize = 0;
    while (ring.getSQE() != null) {
        count += 1;
    }

    try testing.expect(count == 4);

    // Queue should be full now
    try testing.expect(ring.getSQE() == null);
}

test "Basic read operation" {
    var ring = try IoUring.init(32, 0);
    defer ring.deinit();

    // Create test file
    const file = try std.fs.cwd().createFile("test_io_uring.txt", .{
        .read = true,
        .truncate = true,
    });
    defer {
        file.close();
        std.fs.cwd().deleteFile("test_io_uring.txt") catch {};
    }

    const content = "Hello, io_uring!";
    try file.writeAll(content);
    try file.seekTo(0);

    // Prepare read
    var buffer: [1024]u8 = undefined;
    const sqe = ring.getSQE() orelse return error.SQEUnavailable;
    sqe.* = std.mem.zeroes(std.os.linux.io_uring_sqe);
    sqe.opcode = std.os.linux.IORING_OP_READ;
    sqe.fd = file.handle;
    sqe.addr = @intFromPtr(&buffer);
    sqe.len = buffer.len;
    sqe.off = 0;
    sqe.user_data = 42;

    // Submit and wait
    _ = try ring.submit();
    const cqe = try ring.waitCQE();
    defer ring.cqeSeen();

    try testing.expect(cqe.user_data == 42);
    try testing.expect(cqe.res == content.len);
    try testing.expectEqualStrings(content, buffer[0..@intCast(cqe.res)]);
}

Part 2: ファイルコピーツール (30点)

要件

io_uringを使用した高速ファイルコピーツールを実装してください。

マンダトリー機能:

  • 基本的なコピー (15点)
- ソースファイルからデスティネーションへコピー - 非同期読み書きのパイプライン化 - 進捗表示

  • バッファプール (15点)
- 複数バッファでのパイプライン処理 - Registered buffersの使用 - メモリ効率的な実装

実装例

const std = @import("std");
const IoUring = @import("io_uring.zig").IoUring;

const CopyBuffer = struct {
    data: []align(std.mem.page_size) u8,
    in_use: bool,
};

pub const FileCopier = struct {
    ring: IoUring,
    buffers: []CopyBuffer,
    buffer_size: usize,

    const Self = @This();

    pub fn init(
        allocator: std.mem.Allocator,
        ring_entries: u32,
        num_buffers: usize,
        buffer_size: usize,
    ) !Self {
        var ring = try IoUring.init(ring_entries, 0);
        errdefer ring.deinit();

        // Allocate buffers
        var buffers = try allocator.alloc(CopyBuffer, num_buffers);
        errdefer allocator.free(buffers);

        for (buffers) |*buf| {
            buf.data = try allocator.alignedAlloc(
                u8,
                std.mem.page_size,
                buffer_size,
            );
            buf.in_use = false;
        }

        // TODO: Register buffers with io_uring

        return Self{
            .ring = ring,
            .buffers = buffers,
            .buffer_size = buffer_size,
        };
    }

    pub fn copy(
        self: *Self,
        src_path: []const u8,
        dst_path: []const u8,
    ) !void {
        // TODO: ファイルコピーの実装

        // 1. ファイルを開く
        const src_file = try std.fs.cwd().openFile(src_path, .{});
        defer src_file.close();

        const dst_file = try std.fs.cwd().createFile(dst_path, .{
            .truncate = true,
        });
        defer dst_file.close();

        // 2. ファイルサイズを取得
        const file_size = try src_file.getEndPos();

        // 3. パイプライン処理
        //    - 複数バッファを使って同時に読み書き
        //    - 読み込み完了したバッファは即座に書き込み開始
        //    - 書き込み完了したバッファは再利用

        // TODO: 実装してください
        _ = self;
        _ = file_size;
    }

    pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {
        for (self.buffers) |*buf| {
            allocator.free(buf.data);
        }
        allocator.free(self.buffers);
        self.ring.deinit();
    }
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(allocator, args);

    if (args.len != 3) {
        std.log.err("Usage: {} <source> <destination>", .{args[0]});
        return error.InvalidArgs;
    }

    var copier = try FileCopier.init(allocator, 128, 8, 1024 * 1024);
    defer copier.deinit(allocator);

    const start = std.time.milliTimestamp();
    try copier.copy(args[1], args[2]);
    const end = std.time.milliTimestamp();

    std.log.info("Copy completed in {}ms", .{end - start});
}

パフォーマンス目標

テストファイルサイズ: 1GB
バッファ数: 8
バッファサイズ: 1MB

期待される性能:
- 標準的なread/write: ~800MB/s
- io_uring (基本): ~1.5GB/s
- io_uring (registered buffers): ~2.0GB/s

Part 3: Bonus Projects (40点)

Bonus 1: Echo Server with io_uring (20点)

高性能なechoサーバーを実装してください。

要件:

  • 複数クライアントの同時接続対応
  • ゼロコピーでのデータエコー
  • Graceful shutdown
  • 接続統計の表示

性能目標:

  • 10,000同時接続
  • 100,000 req/s以上のスループット

// エコーサーバーのスケルトン
pub const EchoServer = struct {
    ring: IoUring,
    listener: std.net.Server,
    connections: std.AutoHashMap(u64, Connection),

    pub fn init(allocator: std.mem.Allocator, port: u16) !EchoServer {
        // TODO: 実装
    }

    pub fn run(self: *EchoServer) !void {
        // TODO: イベントループを実装
        // 1. ACCEPT操作を送信
        // 2. CQEを処理
        //    - ACCEPT完了 -> READ送信
        //    - READ完了 -> WRITE送信
        //    - WRITE完了 -> 次のREAD送信
    }

    pub fn deinit(self: *EchoServer) void {
        // TODO: クリーンアップ
    }
};

Bonus 2: Simple HTTP Server (20点)

io_uringを使用したシンプルなHTTPサーバーを実装してください。

要件:

  • HTTP/1.1の基本的なリクエスト処理
  • 静的ファイルの配信
  • Keep-Alive対応
  • Registered buffersの活用

エンドポイント:

  • GET /: index.htmlを返す
  • GET /static/*: 静的ファイルを返す
  • GET /stats: サーバー統計をJSONで返す

pub const HttpServer = struct {
    ring: IoUring,
    listener: std.net.Server,
    buffer_pool: BufferPool,

    pub fn init(
        allocator: std.mem.Allocator,
        port: u16,
        static_dir: []const u8,
    ) !HttpServer {
        // TODO: 実装
    }

    fn handleRequest(
        self: *HttpServer,
        conn: *Connection,
        request: []const u8,
    ) ![]const u8 {
        // TODO: HTTPリクエストをパースして応答を生成
        // 1. リクエストラインをパース (GET /path HTTP/1.1)
        // 2. ヘッダーをパース
        // 3. ルーティング
        // 4. レスポンスを生成
    }

    pub fn run(self: *HttpServer) !void {
        // TODO: イベントループ
    }

    pub fn deinit(self: *HttpServer) void {
        // TODO: クリーンアップ
    }
};

ベンチマークツール

epollとの性能比較

const std = @import("std");

pub fn benchmark() !void {
    const iterations = 100_000;
    const buffer_size = 4096;

    std.log.info("Running benchmark: {} iterations", .{iterations});

    // 1. epoll版
    const epoll_time = try benchmarkEpoll(iterations, buffer_size);
    std.log.info("epoll: {}ms", .{epoll_time});

    // 2. io_uring版 (basic)
    const uring_time = try benchmarkIoUring(iterations, buffer_size, false);
    std.log.info("io_uring (basic): {}ms", .{uring_time});

    // 3. io_uring版 (registered buffers)
    const uring_fixed_time = try benchmarkIoUring(iterations, buffer_size, true);
    std.log.info("io_uring (fixed): {}ms", .{uring_fixed_time});

    // 結果の比較
    const speedup_basic = @as(f64, @floatFromInt(epoll_time)) /
        @as(f64, @floatFromInt(uring_time));
    const speedup_fixed = @as(f64, @floatFromInt(epoll_time)) /
        @as(f64, @floatFromInt(uring_fixed_time));

    std.log.info("\nSpeedup:", .{});
    std.log.info("  io_uring (basic): {d:.2}x", .{speedup_basic});
    std.log.info("  io_uring (fixed): {d:.2}x", .{speedup_fixed});
}

fn benchmarkEpoll(iterations: usize, buffer_size: usize) !i64 {
    // TODO: epollを使ったベンチマーク
    _ = iterations;
    _ = buffer_size;
    return 0;
}

fn benchmarkIoUring(
    iterations: usize,
    buffer_size: usize,
    use_fixed: bool,
) !i64 {
    // TODO: io_uringを使ったベンチマーク
    _ = iterations;
    _ = buffer_size;
    _ = use_fixed;
    return 0;
}

評価基準

マンダトリー要件 (80点)

項目 配点 評価基準
IoUringラッパー 30点 - 正しいメモリマッピング
- エラーハンドリング
- テスト通過
ファイルコピー 30点 - パイプライン処理
- バッファプール
- 性能目標達成
テストカバレッジ 20点 - ユニットテスト
- 統合テスト
- エッジケース

必須:

  • すべてのテストが通過すること
  • メモリリークがないこと
  • エラーハンドリングが適切であること
  • ボーナス要件 (20点)

    項目 配点 評価基準
    Echo Server 10点 - 10K同時接続
    - 正確なエコー
    - 統計機能
    HTTP Server 10点 - HTTP/1.1準拠
    - Keep-Alive
    - 静的ファイル配信

    提出方法

    ディレクトリ構造

    exercise-03-io-uring/
    ├── README.md                  # プロジェクト説明、ビルド手順
    ├── build.zig
    ├── src/
    │   ├── io_uring.zig
    │   ├── buffer_pool.zig
    │   ├── file_copy.zig
    │   ├── echo_server.zig       # (bonus)
    │   └── http_server.zig       # (bonus)
    ├── tests/
    │   └── *.zig
    ├── benchmark/
    │   └── perf_comparison.zig
    └── BENCHMARK.md              # ベンチマーク結果
    

    README.md に含めること

    # Exercise 03: io_uring Implementation
    
    ## Author
    - Name: [あなたの名前]
    - Date: [提出日]
    
    ## Build Instructions
    \`\`\`bash
    zig build
    \`\`\`
    
    ## Run Tests
    \`\`\`bash
    zig build test
    \`\`\`
    
    ## Run Benchmark
    \`\`\`bash
    zig build benchmark
    \`\`\`
    
    ## Implementation Notes
    [実装の工夫点、苦労した点など]
    
    ## Performance Results
    [ベンチマーク結果のサマリー]
    
    ## Bonus Features
    - [ ] Echo Server
    - [ ] HTTP Server
    

    BENCHMARK.md フォーマット

    # Benchmark Results
    
    ## Environment
    - CPU: [CPU情報]
    - RAM: [メモリ容量]
    - Kernel: [Linuxカーネルバージョン]
    - Zig: [Zigバージョン]
    
    ## File Copy Performance
    
    | Implementation | Speed | Speedup |
    |----------------|-------|---------|
    | std.fs.copy    | XXX MB/s | 1.0x |
    | io_uring       | XXX MB/s | X.Xx |
    | io_uring+fixed | XXX MB/s | X.Xx |
    
    ## Echo Server Performance
    
    | Metric | Value |
    |--------|-------|
    | Max concurrent connections | XXXX |
    | Throughput | XXX req/s |
    | Average latency | XX ms |
    | P99 latency | XX ms |
    
    ## Analysis
    [結果の分析、考察]
    

    ヒント

    デバッグ方法

    // io_uringのデバッグログ
    fn debugRing(ring: *IoUring) void {
        std.log.debug("SQ: head={}, tail={}, mask={}", .{
            ring.sq.head.*,
            ring.sq.tail.*,
            ring.sq.ring_mask,
        });
        std.log.debug("CQ: head={}, tail={}, mask={}", .{
            ring.cq.head.*,
            ring.cq.tail.*,
            ring.cq.ring_mask,
        });
    }
    
    // CQEのエラーチェック
    fn checkCQE(cqe: *const linux.io_uring_cqe) !i32 {
        if (cqe.res < 0) {
            const err = @as(linux.E, @enumFromInt(-cqe.res));
            std.log.err("Operation failed: {s} (user_data={})",
                .{ @tagName(err), cqe.user_data });
            return error.OperationFailed;
        }
        return cqe.res;
    }
    

    よくあるエラー

  • EFAULT (Bad address)
- バッファアドレスが無効 - メモリアライメントの問題

  • EINVAL (Invalid argument)
- SQEのパラメータが不正 - opcodeがサポートされていない

  • ENOMEM (Out of memory)
- CQEリングがフル - カーネルバッファ不足

参考資料

期限

提出期限: [指定された日付]

頑張ってください!