io_uring e a interface de I/O assincrono mais moderna e performatica do kernel Linux, introduzida na versao 5.1 (2019). Ela revoluciona a forma como programas interagem com o kernel, eliminando overheads de syscalls tradicionais e permitindo batching de operacoes. Neste artigo final da serie de Sistemas Operacionais, exploramos como Zig se integra com io_uring para criar aplicacoes de performance maxima.
Pre-requisitos: Este artigo assume familiaridade com os conceitos dos artigos anteriores, especialmente syscalls e networking. Para uma introducao ao io_uring em Zig, veja tambem Zig Async com io_uring.
Por Que io_uring?
O modelo tradicional de I/O no Linux tem limitacoes fundamentais:
| Modelo | Problema |
|---|---|
| Blocking I/O | Thread fica bloqueada esperando. Nao escala. |
| Non-blocking + poll/select | Muitas syscalls. Overhead de copia de dados. |
| epoll | Melhor que poll, mas ainda 1 syscall por evento. |
| AIO (POSIX) | Implementacao ruim no Linux. Limitado a direct I/O. |
| io_uring | Zero syscalls no caminho quente. Batching nativo. Suporta tudo. |
io_uring funciona atraves de dois buffers circulares (ring buffers) compartilhados entre user space e kernel space:
- Submission Queue (SQ): Voce coloca requisicoes aqui
- Completion Queue (CQ): O kernel coloca resultados aqui
Como ambos sao memoria compartilhada, nao ha copia de dados entre user space e kernel space — a comunicacao e feita por escrita direta na memoria.
Configurando io_uring em Zig
A standard library do Zig inclui bindings para io_uring:
const std = @import("std");
const linux = std.os.linux;
const IoUring = linux.IoUring;
pub fn main() !void {
// Criar io_uring com 256 entradas
var ring = try IoUring.init(256, 0);
defer ring.deinit();
std.debug.print("io_uring inicializado com sucesso!\n", .{});
std.debug.print("SQ entries: {d}\n", .{ring.sq.sqes.len});
std.debug.print("CQ entries: {d}\n", .{ring.cq.cqes.len});
}
Operacoes Basicas com io_uring
Leitura Assincrona de Arquivo
const std = @import("std");
const linux = std.os.linux;
const IoUring = linux.IoUring;
pub fn main() !void {
var ring = try IoUring.init(32, 0);
defer ring.deinit();
// Abrir arquivo
const arquivo = try std.fs.cwd().openFile("/etc/hostname", .{});
defer arquivo.close();
// Preparar buffer
var buffer: [1024]u8 = undefined;
// Submeter operacao de leitura
const sqe = try ring.get_sqe();
sqe.prep_read(arquivo.handle, &buffer, 0);
sqe.user_data = 42; // Identificador customizado
// Submeter para o kernel
_ = try ring.submit();
// Aguardar conclusao
const cqe = try ring.copy_cqe();
if (cqe.res > 0) {
const bytes_lidos: usize = @intCast(cqe.res);
std.debug.print("Lido ({d} bytes): {s}\n", .{
bytes_lidos,
buffer[0..bytes_lidos],
});
} else {
std.debug.print("Erro na leitura: {d}\n", .{cqe.res});
}
}
Escrita Assincrona
const std = @import("std");
const linux = std.os.linux;
const IoUring = linux.IoUring;
pub fn main() !void {
var ring = try IoUring.init(32, 0);
defer ring.deinit();
const arquivo = try std.fs.cwd().createFile("saida_async.txt", .{});
defer arquivo.close();
const dados = "Escrito de forma assincrona via io_uring!\n";
// Submeter escrita
const sqe = try ring.get_sqe();
sqe.prep_write(arquivo.handle, dados, 0);
sqe.user_data = 1;
_ = try ring.submit();
// Aguardar conclusao
const cqe = try ring.copy_cqe();
if (cqe.res >= 0) {
std.debug.print("Escrita concluida: {d} bytes\n", .{cqe.res});
} else {
std.debug.print("Erro na escrita: {d}\n", .{cqe.res});
}
}
Batching: Multiplas Operacoes Simultaneas
O verdadeiro poder do io_uring esta no batching — submeter multiplas operacoes com uma unica syscall (ou nenhuma, no modo SQPOLL).
const std = @import("std");
const linux = std.os.linux;
const IoUring = linux.IoUring;
pub fn main() !void {
var ring = try IoUring.init(64, 0);
defer ring.deinit();
const arquivos = [_][]const u8{
"/etc/hostname",
"/etc/os-release",
"/proc/uptime",
"/proc/loadavg",
};
var buffers: [arquivos.len][1024]u8 = undefined;
var fds: [arquivos.len]std.posix.fd_t = undefined;
// Abrir todos os arquivos
for (arquivos, 0..) |nome, i| {
const f = try std.fs.openFileAbsolute(nome, .{});
fds[i] = f.handle;
}
defer for (fds) |fd| std.posix.close(fd);
// Submeter TODAS as leituras de uma vez
for (0..arquivos.len) |i| {
const sqe = try ring.get_sqe();
sqe.prep_read(fds[i], &buffers[i], 0);
sqe.user_data = @intCast(i);
}
// Uma unica syscall para submeter tudo
const submetidos = try ring.submit();
std.debug.print("Submetidas {d} operacoes com 1 syscall\n", .{submetidos});
// Coletar resultados
var completados: usize = 0;
while (completados < arquivos.len) {
const cqe = try ring.copy_cqe();
const idx: usize = @intCast(cqe.user_data);
if (cqe.res > 0) {
const bytes: usize = @intCast(cqe.res);
std.debug.print("\n=== {s} ({d} bytes) ===\n{s}\n", .{
arquivos[idx],
bytes,
buffers[idx][0..bytes],
});
}
completados += 1;
}
}
Servidor de Alta Performance com io_uring
Vamos construir um servidor HTTP minimalista usando io_uring para I/O assincrono:
const std = @import("std");
const linux = std.os.linux;
const posix = std.posix;
const IoUring = linux.IoUring;
const net = std.net;
const RESPOSTA_HTTP =
"HTTP/1.1 200 OK\r\n" ++
"Content-Type: text/plain\r\n" ++
"Content-Length: 19\r\n" ++
"Connection: close\r\n" ++
"\r\n" ++
"Ola via io_uring!\n";
const OpType = enum(u8) {
accept,
read,
write,
close,
};
const UserData = packed struct {
fd: i32,
op: OpType,
};
fn encodeUserData(fd: i32, op: OpType) u64 {
const ud = UserData{ .fd = fd, .op = op };
return @bitCast(ud);
}
fn decodeUserData(data: u64) UserData {
return @bitCast(data);
}
pub fn main() !void {
var ring = try IoUring.init(256, 0);
defer ring.deinit();
// Criar socket de escuta
const listen_fd = try posix.socket(posix.AF.INET, posix.SOCK.STREAM, 0);
defer posix.close(listen_fd);
try posix.setsockopt(listen_fd, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
const addr = net.Address.initIp4(.{ 0, 0, 0, 0 }, 8080);
try posix.bind(listen_fd, &addr.any, addr.getOsSockLen());
try posix.listen(listen_fd, 128);
std.debug.print("Servidor io_uring escutando em 0.0.0.0:8080\n", .{});
// Submeter primeiro accept
var client_addr: posix.sockaddr = undefined;
var addr_len: posix.socklen_t = @sizeOf(posix.sockaddr);
{
const sqe = try ring.get_sqe();
sqe.prep_accept(listen_fd, &client_addr, &addr_len, 0);
sqe.user_data = encodeUserData(listen_fd, .accept);
}
_ = try ring.submit();
var read_buffers: [1024][1024]u8 = undefined;
var buffer_idx: usize = 0;
// Event loop
while (true) {
const cqe = try ring.copy_cqe();
const ud = decodeUserData(cqe.user_data);
switch (ud.op) {
.accept => {
if (cqe.res >= 0) {
const client_fd: i32 = cqe.res;
// Submeter leitura do cliente
const idx = buffer_idx % read_buffers.len;
buffer_idx += 1;
const read_sqe = try ring.get_sqe();
read_sqe.prep_read(client_fd, &read_buffers[idx], 0);
read_sqe.user_data = encodeUserData(client_fd, .read);
}
// Submeter proximo accept
const accept_sqe = try ring.get_sqe();
accept_sqe.prep_accept(listen_fd, &client_addr, &addr_len, 0);
accept_sqe.user_data = encodeUserData(listen_fd, .accept);
_ = try ring.submit();
},
.read => {
if (cqe.res > 0) {
// Submeter resposta
const write_sqe = try ring.get_sqe();
write_sqe.prep_write(ud.fd, RESPOSTA_HTTP, 0);
write_sqe.user_data = encodeUserData(ud.fd, .write);
_ = try ring.submit();
} else {
posix.close(ud.fd);
}
},
.write => {
// Fechar conexao apos resposta
posix.close(ud.fd);
},
.close => {},
}
}
}
SQPOLL: Zero Syscalls
O modo SQPOLL cria uma thread do kernel que monitora a submission queue. Isso elimina completamente a necessidade de syscalls para submeter operacoes:
const std = @import("std");
const linux = std.os.linux;
const IoUring = linux.IoUring;
pub fn main() !void {
// SQPOLL: kernel thread monitora a SQ
// Requer CAP_SYS_NICE ou root
var ring = try IoUring.init(256, linux.IORING_SETUP_SQPOLL);
defer ring.deinit();
std.debug.print("io_uring com SQPOLL ativo!\n", .{});
std.debug.print("Nenhuma syscall necessaria para submeter operacoes.\n", .{});
// Operacoes submetidas aqui sao processadas
// pela kernel thread automaticamente
const arquivo = try std.fs.cwd().openFile("/proc/uptime", .{});
defer arquivo.close();
var buffer: [256]u8 = undefined;
const sqe = try ring.get_sqe();
sqe.prep_read(arquivo.handle, &buffer, 0);
sqe.user_data = 1;
// Nao precisa chamar submit() - a kernel thread vai detectar
// Mas podemos forcar para nao esperar
_ = try ring.submit();
const cqe = try ring.copy_cqe();
if (cqe.res > 0) {
const n: usize = @intCast(cqe.res);
std.debug.print("Uptime: {s}\n", .{buffer[0..n]});
}
}
Benchmarks: io_uring vs Alternativas
Resultados tipicos em um servidor com muitas conexoes simultaneas:
| Metrica | epoll | io_uring | io_uring + SQPOLL |
|---|---|---|---|
| Requisicoes/s | 150K | 280K | 320K |
| Latencia p99 | 2.1ms | 0.8ms | 0.5ms |
| Syscalls/req | 3-4 | 0-1 | 0 |
| CPU usage | 65% | 45% | 40% |
Os numeros variam conforme hardware e carga, mas io_uring consistentemente supera epoll, especialmente sob alta concorrencia.
Boas Praticas com io_uring
Dimensione o ring adequadamente: Comece com 256-1024 entradas e ajuste conforme a carga.
Use user_data para rastrear operacoes: Codifique o tipo de operacao e contexto no campo
user_data.Batching sempre que possivel: Submeta multiplas operacoes antes de chamar
submit().SQPOLL para latencia minima: Use em cenarios onde microsegundos importam (trading, games).
Fixed buffers e files: Para performance maxima, pre-registre buffers e file descriptors com o kernel.
Exercicios
File copy assincrono: Implemente uma copia de arquivo grande usando io_uring com buffers duplos (leitura e escrita simultaneous).
Proxy TCP: Crie um proxy TCP simples que use io_uring para gerenciar conexoes de entrada e saida simultaneamente.
Benchmark comparativo: Compare a performance de leitura de muitos arquivos pequenos usando read() sincrono vs io_uring batched.
Conclusao da Serie
Esta serie cobriu os fundamentos da programacao de sistemas com Zig:
- Syscalls Linux — A interface com o kernel
- File System Operations — Arquivos e diretorios
- Processos e Signals — Gerenciamento de processos
- Networking com Sockets Raw — Comunicacao de rede
- io_uring e I/O Assincrono — Performance maxima (este artigo)
Conteudo Relacionado
- Zig Async com io_uring — Introducao ao io_uring
- Otimizacao de Performance em Zig — Serie de performance
- Profiling e Benchmarks em Zig — Ferramentas de medicao
- Zig em Producao: Case Studies — Exemplos reais
Concluiu a serie? Parabens! Voce agora tem uma base solida em programacao de sistemas com Zig. Continue explorando com nossas outras series e tutoriais.