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, ¶ms);
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点)
実装例
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;
}
よくあるエラー
- EINVAL (Invalid argument)
- ENOMEM (Out of memory)
参考資料
期限
提出期限: [指定された日付]
頑張ってください!