---
title: "mmap em Zig: Arquivos Mapeados em Memória com Segurança"
url: "https://ziglang.com.br/artigos/zig-mmap-arquivos-memory-mapped/"
markdown_url: "https://ziglang.com.br/artigos/zig-mmap-arquivos-memory-mapped.MD"
description: "Guia prático de mmap em Zig para mapear arquivos, processar dados grandes, escolher MAP_PRIVATE ou MAP_SHARED, sincronizar, medir e evitar SIGBUS."
date: "2026-08-29"
author: ""
---

# mmap em Zig: Arquivos Mapeados em Memória com Segurança

Guia prático de mmap em Zig para mapear arquivos, processar dados grandes, escolher MAP_PRIVATE ou MAP_SHARED, sincronizar, medir e evitar SIGBUS.


Para **mapear um arquivo em memória com Zig**, abra o arquivo, obtenha e valide seu tamanho, crie o mapeamento com `std.posix.mmap`, use o slice retornado somente enquanto o arquivo e o mapeamento estiverem válidos e finalize com `std.posix.munmap`. Escolha `MAP.PRIVATE` para leitura ou alterações descartáveis e `MAP.SHARED` apenas quando realmente quiser compartilhar modificações com o arquivo ou outros processos.

`mmap` não é automaticamente mais rápido que `read`. Ele é especialmente útil para acesso aleatório, índices, formatos binários e arquivos grandes reutilizados. Para percorrer um arquivo uma única vez, um buffer de 64 KiB pode ser mais simples, previsível e igualmente eficiente. A decisão correta vem do padrão de acesso e de medições, não do tamanho isolado do arquivo.

Este guia mostra o modelo mental, um exemplo de leitura, os riscos de `SIGBUS`, o comportamento de `MAP_PRIVATE` e `MAP_SHARED`, sincronização, concorrência e uma estratégia segura para produção. Os nomes e tipos da API podem mudar antes do Zig 1.0; confirme `std.posix.mmap`, `munmap` e as flags na versão fixada pelo seu projeto.

## Resposta rápida

| Cenário | Abordagem recomendada |
|---|---|
| Ler arquivo pequeno uma vez | `readToEndAlloc` ou buffer simples |
| Ler arquivo grande sequencialmente | loop com buffer explícito |
| Consultar regiões aleatórias muitas vezes | `mmap` somente leitura |
| Carregar índice binário no startup | `mmap` + validação completa do formato |
| Alterar dados sem persistir | `MAP_PRIVATE` |
| Compartilhar alterações entre processos | `MAP_SHARED` + protocolo de sincronização |
| Atualizar arquivo com segurança | gravar temporário, sincronizar e renomear |
| Suportar Windows e POSIX | abstração por plataforma + fallback |
| Investigar performance | page faults, RSS, throughput e latência |
| Evitar corrupção | não exponha structs nativas como formato de disco |

## O que mmap realmente faz

Uma leitura tradicional pede bytes ao kernel e os copia para um buffer da aplicação:

```text
arquivo -> page cache do kernel -> buffer do processo
```

Com `mmap`, uma faixa do espaço de endereçamento virtual do processo passa a representar páginas do arquivo:

```text
endereço virtual do processo -> páginas do arquivo no page cache
```

A chamada inicial não precisa carregar imediatamente todos os bytes. Quando o programa toca uma página ainda ausente, ocorre um **page fault**; o kernel localiza ou lê a página e atualiza o mapeamento. Depois disso, o código acessa os dados como um slice.

Isso traz vantagens:

- evita um buffer do tamanho total do arquivo no heap da aplicação;
- permite acesso aleatório com indexação normal;
- deixa o kernel carregar e expulsar páginas sob demanda;
- pode compartilhar páginas limpas entre processos;
- reduz cópias em alguns fluxos.

Também transfere complexidade para outro lugar:

- erros de I/O podem aparecer durante o acesso, não apenas na abertura;
- truncar o arquivo pode invalidar partes do mapeamento;
- o consumo não aparece todo como heap;
- a política de persistência de `MAP_SHARED` exige cuidado;
- ponteiros ou slices ficam inválidos após `munmap`;
- formatos binários precisam tratar alinhamento, endianness e versão.

Portanto, “parece um array” não significa “tem o contrato de um array comum”.

## Exemplo: mapear um arquivo somente para leitura

O padrão abaixo é intencionalmente pequeno. Ele rejeita arquivo vazio, converte o tamanho com segurança e garante `munmap` com `defer`:

```zig
const std = @import("std");

pub fn main() !void {
    const file = try std.fs.cwd().openFile("dados.bin", .{});
    defer file.close();

    const stat = try file.stat();
    if (stat.size == 0) return error.ArquivoVazio;

    const length = std.math.cast(usize, stat.size) orelse
        return error.ArquivoGrandeDemais;

    const mapped = try std.posix.mmap(
        null,
        length,
        std.posix.PROT.READ,
        .{ .TYPE = .PRIVATE },
        file.handle,
        0,
    );
    defer std.posix.munmap(mapped);

    const prefix_len = @min(mapped.len, 16);
    std.debug.print("primeiros bytes: {any}\n", .{mapped[0..prefix_len]});
}
```

A forma das flags variou entre versões de Zig. Em algumas releases, `MAP.PRIVATE` aparece como bit flag; em outras, o argumento é uma struct de flags. Consulte a stdlib instalada:

```bash
zig env
rg "pub fn mmap|pub fn munmap|pub const MAP" /caminho/da/lib/std
```

O contrato importante permanece:

1. o tamanho passado a `mmap` deve ser representável em `usize`;
2. offset normalmente precisa respeitar o alinhamento de página;
3. permissões do mapeamento devem ser compatíveis com o descritor;
4. nenhum slice derivado pode sobreviver a `munmap`;
5. o conteúdo do arquivo continua sendo input não confiável.

Para parsing mais amplo, conecte esse padrão ao guia de [processamento e serialização de dados em Zig](/artigos/zig-processamento-dados-parsing-serializacao/).

## Não mapeie arquivo vazio

Arquivos de tamanho zero merecem um caminho explícito. Sistemas POSIX normalmente rejeitam mapeamento de comprimento zero. Além disso, um arquivo vazio pode representar estados muito diferentes:

- dado válido sem registros;
- arquivo ainda sendo produzido;
- corrupção ou interrupção de escrita;
- arquivo temporário criado, mas não preenchido;
- rotação em andamento.

Não transforme todos em `error.ArquivoVazio` sem considerar o domínio. Um importador pode aceitar zero registros; um índice binário obrigatório deve falhar no startup; um tailer pode aguardar crescimento.

## MAP_PRIVATE versus MAP_SHARED

Essa escolha define o significado das escritas.

### MAP_PRIVATE

`MAP_PRIVATE` usa copy-on-write. O processo inicialmente enxerga as páginas do arquivo. Quando escreve numa página, recebe uma cópia privada. A alteração não é um mecanismo de atualização do arquivo.

Use para:

- leitura somente leitura, combinada com `PROT.READ`;
- aplicar patches temporários durante parsing;
- carregar uma imagem base compartilhável e manter estado privado;
- testar transformações sem persistência.

Mesmo se o mapeamento tiver `PROT.WRITE`, não prometa que o arquivo será alterado. A semântica é privada.

### MAP_SHARED

`MAP_SHARED` permite que modificações sejam refletidas no objeto mapeado e vistas por outros processos que compartilham essas páginas. Isso não fornece, por si só:

- transação;
- lock;
- ordem de publicação;
- checksum;
- recuperação após crash;
- compatibilidade entre versões;
- garantia de que os bytes já chegaram ao armazenamento físico.

Se dois processos escrevem na mesma região sem protocolo, você criou uma corrida sobre dados persistentes. Use atomics apenas para tipos e alinhamentos apropriados; para estruturas maiores, defina lock, geração, estado de commit e recuperação.

Na maioria dos arquivos de configuração, manifests e resultados de jobs, a estratégia mais segura continua sendo:

1. escrever uma nova versão em arquivo temporário no mesmo filesystem;
2. validar tamanho e checksum;
3. sincronizar quando durabilidade for exigida;
4. fechar o temporário;
5. renomear atomicamente sobre o caminho final;
6. manter leitores antigos usando o inode já aberto até terminarem.

## O risco mais importante: truncamento e SIGBUS

Imagine que o processo A mapeou um arquivo de 100 MiB. O processo B abre o mesmo caminho e chama `truncate` para reduzi-lo a 10 MiB. Quando A acessa uma página além do novo fim, o kernel não possui mais backing válido para aquela região. Em sistemas Unix, o processo pode receber `SIGBUS`.

Isso é diferente de um erro Zig retornado por uma função. A falha acontece durante um load comum de memória, possivelmente longe da rotina que abriu o arquivo.

Por isso, não atualize arquivos mapeados com “truncar e reescrever no mesmo inode”. Prefira **criar e renomear**. O leitor que já abriu a versão antiga continua ligado ao objeto antigo; novos leitores abrem a versão nova.

Ainda assim, defina limites:

- valide o tamanho antes de mapear;
- valide magic bytes, versão e offsets antes de seguir ponteiros lógicos;
- trate overflow em `offset + length`;
- nunca confie em contagens vindas do próprio arquivo;
- mantenha checksum por bloco quando corrupção parcial for relevante;
- monitore sinais e crashes no ambiente de produção.

## Parsing seguro de um formato binário

Não faça cast direto do início do arquivo para uma `struct` e considere o trabalho concluído. Layout de struct pode envolver padding, alinhamento e endianness. Um formato de disco deve ser deliberado.

Suponha um cabeçalho com:

```text
4 bytes  magic "ZIDX"
2 bytes  versão little-endian
2 bytes  flags
8 bytes  quantidade de registros
```

Você pode validar por slices:

```zig
const std = @import("std");

const Header = struct {
    version: u16,
    flags: u16,
    record_count: u64,
};

fn parseHeader(bytes: []const u8) !Header {
    const header_len = 16;
    if (bytes.len < header_len) return error.CabecalhoTruncado;
    if (!std.mem.eql(u8, bytes[0..4], "ZIDX")) return error.MagicInvalido;

    return .{
        .version = std.mem.readInt(u16, bytes[4..6], .little),
        .flags = std.mem.readInt(u16, bytes[6..8], .little),
        .record_count = std.mem.readInt(u64, bytes[8..16], .little),
    };
}
```

Depois do cabeçalho, valide cada região antes de criar subslices:

```zig
fn checkedRegion(bytes: []const u8, offset: usize, length: usize) ![]const u8 {
    const end = std.math.add(usize, offset, length) catch
        return error.OffsetInvalido;
    if (end > bytes.len) return error.ArquivoTruncado;
    return bytes[offset..end];
}
```

Esse helper evita o overflow clássico em `offset + length`. Para arquivos controlados por usuários, downloads ou rede, use a mesma disciplina aplicada a [checksums em downloads](/artigos/zig-download-arquivos-http-checksum/) e ao [tratamento de erros em Zig](/artigos/zig-error-handling-boas-praticas/).

## Acesso parcial com offset

Você não precisa mapear o arquivo inteiro. Mapear janelas pode ser útil quando:

- o arquivo é maior que o espaço de endereçamento disponível;
- apenas um segmento é consultado;
- a aplicação processa blocos independentes;
- você quer limitar a região virtual por worker.

O offset passado ao `mmap` deve normalmente ser múltiplo do tamanho de página. Para acessar uma região arbitrária:

1. arredonde o offset desejado para baixo até a fronteira de página;
2. calcule o deslocamento interno;
3. mapeie `deslocamento_interno + tamanho_desejado`;
4. exponha apenas o subslice solicitado;
5. faça `munmap` usando a região original, não o subslice.

Mantenha numa struct tanto o mapeamento completo quanto a visão útil:

```zig
const MappedRegion = struct {
    mapping: []align(std.mem.page_size) u8,
    view: []u8,

    pub fn deinit(self: *MappedRegion) void {
        std.posix.munmap(self.mapping);
        self.* = undefined;
    }
};
```

A assinatura exata e o alinhamento retornado dependem da versão. O princípio de ownership não depende: a função que destrói precisa receber a região original.

## msync, fsync e durabilidade

Com `MAP_SHARED`, páginas sujas podem ser gravadas pelo kernel posteriormente. Se a aplicação precisa solicitar sincronização, POSIX oferece `msync`. Mas há três conceitos diferentes:

- **visibilidade:** outro processo enxerga a alteração;
- **writeback:** dados foram enviados ao filesystem;
- **durabilidade:** dados e metadados sobreviverão a uma queda conforme o contrato do storage.

Uma chamada isolada não transforma atualização in-place em transação. Se a estrutura possui cabeçalho, corpo e checksum, um crash entre essas escritas pode deixar uma mistura de versões.

Para estado crítico, use protocolo de commit:

- duas áreas ou gerações;
- journal ou write-ahead log;
- checksum e número de sequência;
- marcador de commit escrito por último;
- recuperação testada após interrupção em cada etapa.

Para estado não crítico — cache reconstruível, índice derivado, artefato temporário — documente que a aplicação pode descartar e reconstruir o arquivo.

## Concorrência dentro do processo

Um slice mapeado segue as mesmas regras de data race de qualquer memória. Várias threads podem ler páginas imutáveis. Se alguma thread escreve enquanto outra lê a mesma região sem sincronização, o comportamento do programa não se torna seguro porque os bytes vieram de um arquivo.

Padrões mais simples:

- mapa somente leitura depois da inicialização;
- publique a referência somente após validar todo o arquivo;
- troque a versão inteira do índice, não campos individuais;
- mantenha contagem de leitores antes de desmontar a versão antiga;
- use uma arena ou allocator separado para metadados derivados;
- não guarde ponteiros para o mapeamento em filas que sobrevivem ao owner.

Para padrões de locks, atomics e filas, consulte [concorrência avançada em Zig](/artigos/zig-concorrencia-padroes-avancados/).

## mmap usa memória, só não é heap comum

É comum observar um heap pequeno e concluir que `mmap` “não usa RAM”. As páginas acessadas podem se tornar residentes e competir por memória física. O kernel pode descartar páginas limpas e recarregá-las; páginas privadas modificadas precisam de swap ou permanecem residentes; páginas compartilhadas sujas exigem writeback.

Meça no Linux com:

```bash
/usr/bin/time -v ./zig-out/bin/indexador
perf stat -e page-faults,minor-faults,major-faults ./zig-out/bin/indexador
cat /proc/$PID/smaps_rollup
```

Métricas úteis:

- major e minor page faults;
- RSS e PSS;
- bytes lidos do disco;
- latência p50, p95 e p99;
- throughput de registros;
- tempo de startup frio e quente;
- CPU por gigabyte processado;
- page cache antes e depois do teste.

Um benchmark repetido logo após a primeira execução mede cache quente. Se o objetivo é startup após reboot ou primeiro acesso a um dataset, teste também o cenário frio de maneira controlada. O guia de [profiling com perf e FlameGraph](/artigos/zig-profiling-perf-flamegraph/) ajuda a separar page faults, parsing e cópia.

## Quando read com buffer é melhor

Prefira leitura explícita quando:

- o arquivo será consumido uma vez do início ao fim;
- você precisa de backpressure claro;
- o input pode ser pipe, socket ou stdin;
- o software precisa rodar em Windows sem implementação separada;
- erros de I/O devem aparecer em pontos controlados;
- o arquivo muda durante a leitura e snapshots não existem;
- a memória virtual é limitada;
- o parsing já trabalha naturalmente em chunks.

Um ETL de CSV ou JSONL normalmente combina melhor com um reader bufferizado do que com acesso aleatório. Veja o guia de [ETL com CSV e JSONL em Zig](/artigos/zig-etl-csv-jsonl-migracao-dados/) para esse caso.

Prefira `mmap` quando:

- o mesmo índice recebe muitas consultas aleatórias;
- múltiplos processos leem o mesmo dataset imutável;
- o formato possui offsets e regiões independentes;
- o custo de copiar o arquivo inteiro é relevante;
- o sistema operacional e filesystem são conhecidos;
- a equipe consegue operar atualização por geração e renomeação.

## Checklist de produção

Antes de usar arquivo mapeado em produção:

- [ ] a versão do Zig e a assinatura de `std.posix.mmap` estão fixadas;
- [ ] arquivo vazio possui semântica explícita;
- [ ] tamanho cabe em `usize` no target;
- [ ] offsets e somas usam checagem de overflow;
- [ ] magic bytes, versão, flags e checksum são validados;
- [ ] `MAP_PRIVATE` ou `MAP_SHARED` foi escolhido por contrato;
- [ ] nenhum slice sobrevive a `munmap`;
- [ ] atualização usa arquivo temporário e rename quando possível;
- [ ] nenhum produtor trunca um inode ainda mapeado;
- [ ] concorrência entre readers e writers tem protocolo;
- [ ] durabilidade não depende apenas de `munmap`;
- [ ] formato de disco não depende do layout nativo de uma struct;
- [ ] testes cobrem truncamento, versão inválida e offsets maliciosos;
- [ ] benchmark compara `mmap` com leitura bufferizada;
- [ ] métricas incluem page faults, RSS e latência de cauda;
- [ ] existe fallback ou implementação específica para plataformas não POSIX.

## Perguntas frequentes

### Quando vale a pena usar mmap em Zig?

Quando o programa consulta regiões aleatórias, reutiliza um índice grande, compartilha páginas imutáveis entre processos ou quer evitar uma cópia integral para o heap. Para streaming sequencial, comece com `read` bufferizado.

### MAP_PRIVATE altera o arquivo original?

Não. Escritas são privadas por copy-on-write e não formam um mecanismo de persistência. Use `MAP_SHARED` apenas com um protocolo explícito ou grave uma nova versão do arquivo.

### Por que mmap pode causar SIGBUS?

O caso mais comum é acessar uma região cujo backing desapareceu após truncamento do arquivo. Evite reescrever no mesmo inode; produza uma nova versão e faça rename.

### Preciso chamar msync antes de munmap?

Se você depende de controlar o writeback de um mapeamento compartilhado, avalie `msync`, mas não confunda isso com transação ou durabilidade completa. O protocolo pode exigir também `fsync`, checksum, ordem de escrita e sincronização do diretório.

### mmap usa menos memória do que readAllAlloc?

Ele evita um buffer integral no heap e carrega páginas sob demanda, mas páginas residentes ainda usam RAM. Observe RSS, PSS, page faults e I/O real.

### mmap funciona igual em Linux, macOS e Windows?

Não. Linux, BSD e macOS oferecem a família POSIX com diferenças; Windows usa file mapping próprio. Isole a plataforma e preserve um fallback com leitura explícita.

## Conclusão

`mmap` em Zig é uma ferramenta poderosa quando o arquivo precisa se comportar como uma coleção consultável, não apenas como um fluxo de bytes. O caminho seguro é manter o mapeamento somente leitura, validar o formato antes de expor dados, controlar o lifetime com `defer` e atualizar datasets por nova versão mais rename.

Não escolha `mmap` apenas para evitar uma alocação visível. Compare com um reader bufferizado sob carga real, incluindo cache frio, page faults e latência p99. Quando o padrão de acesso favorece páginas sob demanda, a combinação de Zig, slices e memória mapeada pode produzir um design enxuto. Quando o fluxo é sequencial ou multiplataforma, a solução simples continua sendo a melhor.
