---
title: "Zig e io_uring: I/O Assíncrono de Alta Performance no Linux"
url: "https://ziglang.com.br/artigos/zig-io-uring-async/"
markdown_url: "https://ziglang.com.br/artigos/zig-io-uring-async.MD"
description: "Aprenda a usar io_uring com Zig para I/O assíncrono de alta performance. Guia com exemplos de servidor TCP, SQE/CQE, buffer pools e comparação com epoll."
date: "2026-03-25"
author: ""
---

# Zig e io_uring: I/O Assíncrono de Alta Performance no Linux

Aprenda a usar io_uring com Zig para I/O assíncrono de alta performance. Guia com exemplos de servidor TCP, SQE/CQE, buffer pools e comparação com epoll.


Se você trabalha com servidores de alta performance no Linux, provavelmente já ouviu falar do **io_uring**. Introduzido no kernel 5.1 (2019), o io_uring revolucionou o I/O assíncrono no Linux ao permitir operações sem syscalls bloqueantes, usando filas compartilhadas entre userspace e kernel. E a **linguagem Zig**, com seu controle manual de memória e execução em [comptime](/glossario/comptime/), é uma parceira ideal para extrair o máximo dessa interface.

Neste artigo, vamos explorar como usar io_uring com Zig para construir aplicações de I/O de alta performance — desde os conceitos fundamentais até um servidor TCP funcional.

## O que é io_uring

O io_uring é uma interface de I/O assíncrono do kernel Linux baseada em duas filas circulares (ring buffers) compartilhadas entre o espaço do usuário e o kernel:

- **SQE (Submission Queue Entry)** — onde a aplicação submete requisições de I/O
- **CQE (Completion Queue Entry)** — onde o kernel entrega os resultados

O fluxo básico é:

1. A aplicação preenche um SQE com a operação desejada (read, write, accept, etc.)
2. Submete a fila para o kernel
3. O kernel processa as operações de forma assíncrona
4. Os resultados aparecem na CQE sem necessidade de syscall adicional

Essa arquitetura elimina a principal fonte de overhead do I/O tradicional: o custo de transição entre userspace e kernel. Com io_uring, centenas de operações podem ser submetidas e completadas com **uma única syscall** — ou até zero, usando o modo SQPOLL.

### Por que io_uring é Superior ao epoll

O `epoll` foi o padrão-ouro do I/O assíncrono no Linux por quase duas décadas. Mas ele tem limitações fundamentais:

| Aspecto | epoll | io_uring |
|---------|-------|----------|
| Modelo | Notificação de prontidão | Conclusão de operação |
| Syscalls por operação | 2+ (epoll_wait + read/write) | 0-1 (batch submit) |
| Operações suportadas | Apenas socket/fd readiness | Read, write, accept, connect, send, recv, fsync, fallocate, etc. |
| Buffer management | Manual por operação | Buffer pools compartilhados |
| Overhead por operação | ~1-2us | ~0.1-0.5us |
| Batching nativo | Não | Sim |

Com epoll, você descobre que um fd está "pronto" e então faz a syscall de read/write. Com io_uring, você diz "leia esses dados" e o kernel faz tudo — incluindo a transferência de dados — antes de notificá-lo. Menos context switches, menos cópias, mais throughput.

## Por que Zig é Ideal para io_uring

A combinação Zig + io_uring é poderosa por vários motivos:

**Controle de memória** — io_uring exige buffers pré-alocados com endereços fixos. Zig oferece [alocadores explícitos](/artigos/zig-alocacao-memoria-estrategias/) e controle total sobre layout de memória, perfeito para criar buffer pools alinhados e pinados.

**Comptime** — Muitos parâmetros de io_uring são constantes conhecidas em tempo de compilação (tamanho das filas, flags, opcodes). Com comptime, essas configurações são resolvidas sem custo em runtime.

**Sem runtime oculto** — Diferente de Go ou Rust (com tokio), Zig não tem runtime de async escondido. Você controla exatamente quando e como as operações são submetidas e processadas.

**Bindings nativos** — A stdlib de Zig inclui `std.os.linux.io_uring`, bindings diretos para a interface do kernel sem wrappers adicionais.

## Configurando io_uring em Zig

Vamos começar com a configuração básica de uma instância io_uring:

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

pub fn main() !void {
    // Criar instância com 256 entradas na submission queue
    var ring = try IoUring.init(256, .{});
    defer ring.deinit();

    std.debug.print("io_uring inicializado com sucesso!\n", .{});
    std.debug.print("SQ entries: {}\n", .{ring.sq.sqes.len});
    std.debug.print("CQ entries: {}\n", .{ring.cq.cqes.len});
}
```

O parâmetro `256` define o tamanho da submission queue. O kernel pode arredondar para a próxima potência de 2. A completion queue geralmente tem o dobro do tamanho.

### Flags de Inicialização

O segundo parâmetro aceita flags que controlam o comportamento da instância:

```zig
var ring = try IoUring.init(256, .{
    .flags = linux.IORING_SETUP_SQPOLL |  // Kernel poll mode
             linux.IORING_SETUP_IOPOLL,    // Hardware polling
    .sq_thread_idle = 2000,                // Timeout do thread SQPOLL em ms
});
```

Com `IORING_SETUP_SQPOLL`, o kernel cria um thread dedicado que monitora a submission queue continuamente. Isso elimina até a syscall de submit — a aplicação só precisa escrever no ring buffer e o kernel processa automaticamente. Ideal para cenários de latência ultra-baixa.

## Operações Básicas: Read e Write

Vamos ver como realizar leitura e escrita assíncrona em arquivos:

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

pub fn main() !void {
    var ring = try IoUring.init(64, .{});
    defer ring.deinit();

    // Abrir arquivo para leitura
    const fd = try std.posix.open("/etc/hostname", .{ .ACCMODE = .RDONLY }, 0);
    defer std.posix.close(fd);

    // Preparar buffer de leitura
    var buffer: [4096]u8 = undefined;

    // Submeter operação de leitura
    _ = ring.read(
        0xDEAD,    // user_data: identificador para correlacionar com CQE
        fd,
        .{ .buffer = &buffer },
        0,         // offset no arquivo
    );

    // Submeter para o kernel
    _ = try ring.submit();

    // Esperar resultado
    const cqe = try ring.copy_cqe();

    if (cqe.res >= 0) {
        const bytes_lidos: usize = @intCast(cqe.res);
        std.debug.print("Lidos {} bytes: {s}\n", .{
            bytes_lidos,
            buffer[0..bytes_lidos],
        });
    } else {
        std.debug.print("Erro: {}\n", .{cqe.res});
    }
}
```

Cada SQE contém um campo `user_data` de 64 bits que permite identificar qual operação gerou cada CQE. Isso é essencial quando você submete muitas operações simultâneas.

## Servidor TCP com io_uring

Agora vamos ao exemplo prático mais completo — um servidor TCP echo de alta performance:

```zig
const std = @import("std");
const linux = std.os.linux;
const IoUring = linux.IoUring;
const posix = std.posix;
const net = std.net;

const BUFFER_SIZE = 4096;
const MAX_CONNECTIONS = 1024;
const RING_ENTRIES = 512;

const EventType = enum(u8) {
    accept,
    read,
    write,
};

const UserData = packed struct {
    fd: i32,
    event_type: EventType,
};

fn encodeUserData(fd: i32, event_type: EventType) u64 {
    const ud = UserData{ .fd = fd, .event_type = event_type };
    return @bitCast(ud);
}

fn decodeUserData(data: u64) UserData {
    return @bitCast(data);
}

pub fn main() !void {
    // Inicializar io_uring
    var ring = try IoUring.init(RING_ENTRIES, .{});
    defer ring.deinit();

    // Criar socket do servidor
    const server_fd = try posix.socket(
        posix.AF.INET,
        posix.SOCK.STREAM | posix.SOCK.NONBLOCK,
        0,
    );
    defer posix.close(server_fd);

    // Configurar SO_REUSEADDR
    try posix.setsockopt(server_fd, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));

    // Bind e listen
    const addr = net.Address.initIp4(.{ 0, 0, 0, 0 }, 8080);
    try posix.bind(server_fd, &addr.any, addr.getOsSockLen());
    try posix.listen(server_fd, 128);

    std.debug.print("Servidor TCP rodando na porta 8080\n", .{});

    // Buffers por conexão
    var buffers: [MAX_CONNECTIONS][BUFFER_SIZE]u8 = undefined;

    // Submeter primeiro accept
    _ = ring.accept(
        encodeUserData(server_fd, .accept),
        server_fd,
        null,
        null,
        0,
    );
    _ = try ring.submit();

    // Event loop principal
    while (true) {
        const cqe = try ring.copy_cqe();
        const ud = decodeUserData(cqe.user_data);

        switch (ud.event_type) {
            .accept => {
                if (cqe.res >= 0) {
                    const client_fd = cqe.res;
                    const idx: usize = @intCast(client_fd % MAX_CONNECTIONS);

                    // Submeter leitura para o novo cliente
                    _ = ring.read(
                        encodeUserData(client_fd, .read),
                        client_fd,
                        .{ .buffer = &buffers[idx] },
                        0,
                    );
                }

                // Sempre resubmeter accept para novos clientes
                _ = ring.accept(
                    encodeUserData(server_fd, .accept),
                    server_fd,
                    null,
                    null,
                    0,
                );
            },

            .read => {
                if (cqe.res > 0) {
                    const bytes: usize = @intCast(cqe.res);
                    const idx: usize = @intCast(ud.fd % MAX_CONNECTIONS);

                    // Echo: escrever de volta o que foi lido
                    _ = ring.write(
                        encodeUserData(ud.fd, .write),
                        ud.fd,
                        buffers[idx][0..bytes],
                        0,
                    );
                } else {
                    // Conexão fechada ou erro
                    posix.close(@intCast(ud.fd));
                }
            },

            .write => {
                if (cqe.res >= 0) {
                    const idx: usize = @intCast(ud.fd % MAX_CONNECTIONS);

                    // Submeter próxima leitura
                    _ = ring.read(
                        encodeUserData(ud.fd, .read),
                        ud.fd,
                        .{ .buffer = &buffers[idx] },
                        0,
                    );
                } else {
                    posix.close(@intCast(ud.fd));
                }
            },
        }

        _ = try ring.submit();
    }
}
```

Esse servidor usa io_uring para **todas** as operações de I/O — accept, read e write — sem nenhuma syscall bloqueante no hot path. Em benchmarks típicos, um servidor io_uring em Zig facilmente ultrapassa 100K requisições/segundo em hardware modesto.

## Buffer Pools e Operações Multishot

Para ainda mais performance, io_uring suporta **buffer pools registrados** que eliminam a cópia de dados entre kernel e userspace:

```zig
// Registrar buffers fixos com o kernel
var fixed_buffers: [16][4096]u8 = undefined;
var iovecs: [16]posix.iovec = undefined;

for (&iovecs, &fixed_buffers) |*iov, *buf| {
    iov.* = .{
        .base = buf,
        .len = buf.len,
    };
}

try ring.register_buffers(&iovecs);
```

Com buffers registrados, o kernel mapeia a memória diretamente, evitando cópias em cada operação. Para servidores que processam milhões de requisições, essa otimização faz diferença mensurável.

### Multishot Accept

A partir do kernel 5.19, o io_uring suporta **multishot accept** — uma única submissão que aceita múltiplas conexões sem resubmissão:

```zig
// Em vez de resubmeter accept a cada conexão:
_ = ring.accept_multishot(
    encodeUserData(server_fd, .accept),
    server_fd,
    null,
    null,
    0,
);
```

Com multishot, o kernel continua preenchendo CQEs para cada nova conexão aceita até que você cancele a operação. Menos submissões, menos overhead, mais throughput.

## Comparação com Abordagens Tradicionais

### epoll + threads

A abordagem clássica de servidores Linux usa epoll com pool de threads. Funciona bem, mas cada operação exige pelo menos duas syscalls (epoll_wait + read/write) e o gerenciamento de threads adiciona complexidade e overhead de context switch.

### async/await (Rust + tokio)

Rust com tokio oferece uma excelente experiência de desenvolvimento com async/await. O runtime Tokio também suporta io_uring via tokio-uring — veja o <a href="https://rustlang.com.br" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'rustlang.com.br' })">estado do ecossistema async em Rust no Rust Brasil</a>. Porém, o runtime do tokio adiciona overhead e complexidade — scheduler de tasks, alocações internas, e um modelo de execução que pode ser difícil de raciocinar. Zig com io_uring é mais explícito e previsível.

Go, por sua vez, usa epoll/kqueue internamente no seu runtime de goroutines — para entender o <a href="https://golang.com.br/aprenda/concorrencia-go/" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">modelo de concorrência do Go</a>, confira o Golang Brasil.

### O Futuro: std.Io.Evented em Zig

A equipe de Zig está trabalhando em uma abstração de [I/O assíncrono na stdlib](/artigos/zig-2026-estado-atual-roadmap/) que usará io_uring no Linux, kqueue no macOS e IOCP no Windows. Quando pronta, essa API oferecerá a mesma performance do io_uring direto com portabilidade automática. Até lá, usar os bindings de io_uring diretamente é a melhor opção para performance máxima no Linux.

## Considerações de Deploy

Se você está desenvolvendo um servidor io_uring em Zig, considere que io_uring é específico do Linux. Para deploy em servidores ARM (como AWS Graviton), a [cross-compilation do Zig](/artigos/zig-cross-compilation-guia/) permite compilar de qualquer máquina para o alvo Linux ARM sem toolchains extras:

```bash
zig build -Dtarget=aarch64-linux-gnu -Doptimize=ReleaseFast
```

O binário resultante inclui tudo que precisa — sem dependências externas, sem runtime, pronto para deploy.

### Requisitos de Kernel

Certifique-se de que o servidor de produção roda um kernel recente:

- **5.1+** — io_uring básico
- **5.6+** — fixed buffers, timeout, cancel
- **5.19+** — multishot accept
- **6.0+** — multishot receive, zero-copy send

Em distribuições modernas como Ubuntu 22.04+ ou Amazon Linux 2023, o kernel já suporta todas essas funcionalidades.

## Performance: O que Esperar

Em benchmarks típicos comparando um echo server Zig + io_uring com alternativas:

- **2-3x mais throughput** que epoll + threads para cargas I/O-bound
- **Latência p99 50-70% menor** que abordagens baseadas em async/await com runtime
- **Uso de memória 5-10x menor** que servidores Node.js ou Go para a mesma carga

Esses números variam conforme hardware, kernel e carga de trabalho. O ponto chave é que io_uring elimina overhead sistemático que outras abordagens carregam por design.

Para entender melhor como Zig se compara a C em cenários de alta performance, confira nosso artigo sobre [Zig vs C moderno](/artigos/zig-vs-c-moderno/). E para ver padrões de [concorrência](/cheatsheets/concorrencia/) e [operações de I/O](/cheatsheets/io-operacoes/) em Zig, acesse nossos cheatsheets. Também temos conteúdo detalhado sobre o [ecossistema de networking em Zig](/ecossistema/zig-network/).

## Conclusão

A combinação de Zig com io_uring representa o estado da arte em I/O de alta performance no Linux. Zig oferece o controle de baixo nível necessário para usar io_uring de forma ótima, sem a complexidade de C e sem o overhead de runtimes de alto nível.

Se você está construindo servidores, proxies, bancos de dados ou qualquer software que precisa extrair o máximo de performance de I/O no Linux, Zig com io_uring é uma combinação que merece sua atenção séria.
