io_uring e I/O Assincrono com Zig: Performance Maxima no Linux

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:

ModeloProblema
Blocking I/OThread fica bloqueada esperando. Nao escala.
Non-blocking + poll/selectMuitas syscalls. Overhead de copia de dados.
epollMelhor que poll, mas ainda 1 syscall por evento.
AIO (POSIX)Implementacao ruim no Linux. Limitado a direct I/O.
io_uringZero 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:

  1. Submission Queue (SQ): Voce coloca requisicoes aqui
  2. 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:

Metricaepollio_uringio_uring + SQPOLL
Requisicoes/s150K280K320K
Latencia p992.1ms0.8ms0.5ms
Syscalls/req3-40-10
CPU usage65%45%40%

Os numeros variam conforme hardware e carga, mas io_uring consistentemente supera epoll, especialmente sob alta concorrencia.

Boas Praticas com io_uring

  1. Dimensione o ring adequadamente: Comece com 256-1024 entradas e ajuste conforme a carga.

  2. Use user_data para rastrear operacoes: Codifique o tipo de operacao e contexto no campo user_data.

  3. Batching sempre que possivel: Submeta multiplas operacoes antes de chamar submit().

  4. SQPOLL para latencia minima: Use em cenarios onde microsegundos importam (trading, games).

  5. Fixed buffers e files: Para performance maxima, pre-registre buffers e file descriptors com o kernel.

Exercicios

  1. File copy assincrono: Implemente uma copia de arquivo grande usando io_uring com buffers duplos (leitura e escrita simultaneous).

  2. Proxy TCP: Crie um proxy TCP simples que use io_uring para gerenciar conexoes de entrada e saida simultaneamente.

  3. 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:

  1. Syscalls Linux — A interface com o kernel
  2. File System Operations — Arquivos e diretorios
  3. Processos e Signals — Gerenciamento de processos
  4. Networking com Sockets Raw — Comunicacao de rede
  5. io_uring e I/O Assincrono — Performance maxima (este artigo)

Conteudo Relacionado


Concluiu a serie? Parabens! Voce agora tem uma base solida em programacao de sistemas com Zig. Continue explorando com nossas outras series e tutoriais.

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.