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:
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:
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_SHAREDexige 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:
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:
zig env
rg "pub fn mmap|pub fn munmap|pub const MAP" /caminho/da/lib/std
O contrato importante permanece:
- o tamanho passado a
mmapdeve ser representável emusize; - offset normalmente precisa respeitar o alinhamento de página;
- permissões do mapeamento devem ser compatíveis com o descritor;
- nenhum slice derivado pode sobreviver a
munmap; - 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.
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:
- escrever uma nova versão em arquivo temporário no mesmo filesystem;
- validar tamanho e checksum;
- sincronizar quando durabilidade for exigida;
- fechar o temporário;
- renomear atomicamente sobre o caminho final;
- 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:
4 bytes magic "ZIDX"
2 bytes versão little-endian
2 bytes flags
8 bytes quantidade de registros
Você pode validar por slices:
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:
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 e ao tratamento de erros em Zig.
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:
- arredonde o offset desejado para baixo até a fronteira de página;
- calcule o deslocamento interno;
- mapeie
deslocamento_interno + tamanho_desejado; - exponha apenas o subslice solicitado;
- faça
munmapusando a região original, não o subslice.
Mantenha numa struct tanto o mapeamento completo quanto a visão útil:
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.
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:
/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 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 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.mmapestão fixadas; - arquivo vazio possui semântica explícita;
- tamanho cabe em
usizeno target; - offsets e somas usam checagem de overflow;
- magic bytes, versão, flags e checksum são validados;
-
MAP_PRIVATEouMAP_SHAREDfoi 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
mmapcom 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.