Apos entender como syscalls funcionam no artigo anterior, e hora de mergulhar nas operacoes de file system. O Linux segue a filosofia “tudo e um arquivo”, e dominar operacoes de I/O em arquivos e a base para qualquer programacao de sistemas seria. Neste artigo, exploramos como Zig torna essas operacoes seguras, eficientes e elegantes.
Leitura de Arquivos: Multiplas Abordagens
Zig oferece varias formas de ler arquivos, cada uma adequada para diferentes cenarios.
Leitura Completa em Memoria
Para arquivos pequenos a medios, a forma mais simples e carregar todo o conteudo na memoria:
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// readToEndAlloc le todo o arquivo de uma vez
// O segundo parametro e o limite maximo de bytes
const conteudo = try std.fs.cwd().readFileAlloc(
allocator,
"config.txt",
1024 * 1024, // max 1MB
);
defer allocator.free(conteudo);
std.debug.print("Conteudo ({d} bytes):\n{s}\n", .{
conteudo.len,
conteudo,
});
}
Leitura Linha por Linha
Para arquivos grandes ou quando voce precisa processar linha por linha:
const std = @import("std");
pub fn main() !void {
const arquivo = try std.fs.cwd().openFile("dados.csv", .{});
defer arquivo.close();
var buf_reader = std.io.bufferedReader(arquivo.reader());
var reader = buf_reader.reader();
var linha_buf: [4096]u8 = undefined;
var numero_linha: usize = 0;
while (reader.readUntilDelimiterOrEof(&linha_buf, '\n')) |linha_opt| {
if (linha_opt) |linha| {
numero_linha += 1;
std.debug.print("Linha {d}: {s}\n", .{ numero_linha, linha });
} else {
break; // EOF
}
} else |err| {
std.debug.print("Erro na leitura: {}\n", .{err});
}
std.debug.print("\nTotal: {d} linhas\n", .{numero_linha});
}
Leitura com Buffer Fixo
Para controle maximo sobre a leitura, use buffers fixos:
const std = @import("std");
pub fn main() !void {
const arquivo = try std.fs.cwd().openFile("grande.bin", .{});
defer arquivo.close();
var buffer: [4096]u8 = undefined;
var total_bytes: usize = 0;
while (true) {
const bytes_lidos = try arquivo.read(&buffer);
if (bytes_lidos == 0) break; // EOF
total_bytes += bytes_lidos;
// Processar buffer[0..bytes_lidos]
}
std.debug.print("Total lido: {d} bytes\n", .{total_bytes});
}
Escrita de Arquivos
Escrita Simples
const std = @import("std");
pub fn main() !void {
// createFile cria ou trunca o arquivo
const arquivo = try std.fs.cwd().createFile("relatorio.txt", .{});
defer arquivo.close();
const writer = arquivo.writer();
try writer.writeAll("=== Relatorio de Sistema ===\n\n");
try writer.print("Data: {s}\n", .{"2026-02-21"});
try writer.print("Processos ativos: {d}\n", .{142});
try writer.print("Memoria livre: {d} MB\n", .{3847});
// Escrever bytes diretamente
const dados_binarios = [_]u8{ 0xDE, 0xAD, 0xBE, 0xEF };
try arquivo.writeAll(&dados_binarios);
}
Escrita com Buffer (Buffered Writer)
Para escrita frequente de pequenas quantidades de dados, use um buffered writer para reduzir o numero de syscalls:
const std = @import("std");
pub fn main() !void {
const arquivo = try std.fs.cwd().createFile("log.txt", .{});
defer arquivo.close();
var buf_writer = std.io.bufferedWriter(arquivo.writer());
var writer = buf_writer.writer();
var i: usize = 0;
while (i < 10000) : (i += 1) {
try writer.print("[{d}] Entrada de log numero {d}\n", .{ i, i * 7 });
}
// Importante: flush para garantir que tudo foi escrito
try buf_writer.flush();
}
Append em Arquivo Existente
const std = @import("std");
pub fn main() !void {
// Abrir para escrita no final
const arquivo = try std.fs.cwd().openFile("log.txt", .{
.mode = .write_only,
});
defer arquivo.close();
// Posicionar no final
try arquivo.seekFromEnd(0);
const writer = arquivo.writer();
try writer.print("Nova entrada adicionada\n", .{});
}
Manipulacao de Diretorios
Listando Conteudo de um Diretorio
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var dir = try std.fs.cwd().openDir(".", .{ .iterate = true });
defer dir.close();
var iterator = dir.iterate();
std.debug.print("{s:<30} {s:<10} {s}\n", .{ "Nome", "Tipo", "Tamanho" });
std.debug.print("{s}\n", .{"-" ** 50});
while (try iterator.next()) |entrada| {
const tipo_str = switch (entrada.kind) {
.file => "arquivo",
.directory => "diretorio",
.sym_link => "symlink",
else => "outro",
};
if (entrada.kind == .file) {
const stat = dir.statFile(entrada.name) catch continue;
std.debug.print("{s:<30} {s:<10} {d} bytes\n", .{
entrada.name,
tipo_str,
stat.size,
});
} else {
std.debug.print("{s:<30} {s:<10}\n", .{ entrada.name, tipo_str });
}
}
}
Travessia Recursiva de Diretorios
const std = @import("std");
fn percorrerRecursivo(
allocator: std.mem.Allocator,
dir_path: []const u8,
profundidade: usize,
) !void {
var dir = try std.fs.cwd().openDir(dir_path, .{ .iterate = true });
defer dir.close();
var iterator = dir.iterate();
while (try iterator.next()) |entrada| {
// Indentacao visual
var i: usize = 0;
while (i < profundidade) : (i += 1) {
std.debug.print(" ", .{});
}
std.debug.print("{s}\n", .{entrada.name});
if (entrada.kind == .directory) {
const sub_path = try std.fs.path.join(allocator, &.{
dir_path,
entrada.name,
});
defer allocator.free(sub_path);
percorrerRecursivo(allocator, sub_path, profundidade + 1) catch |err| {
std.debug.print(" [Erro ao acessar: {}]\n", .{err});
};
}
}
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
try percorrerRecursivo(allocator, ".", 0);
}
Criacao e Remocao de Diretorios
const std = @import("std");
pub fn main() !void {
const cwd = std.fs.cwd();
// Criar diretorio simples
try cwd.makeDir("novo_diretorio");
// Criar diretorios aninhados (como mkdir -p)
try cwd.makePath("caminho/completo/aninhado");
// Remover diretorio vazio
try cwd.deleteDir("novo_diretorio");
// Remover arvore de diretorios (como rm -rf)
try cwd.deleteTree("caminho");
}
Permissoes e Metadados
Obtendo Metadados de Arquivo
const std = @import("std");
pub fn main() !void {
const stat = try std.fs.cwd().statFile("exemplo.txt");
std.debug.print("Tamanho: {d} bytes\n", .{stat.size});
std.debug.print("Inode: {d}\n", .{stat.inode});
// Permissoes
const mode = stat.mode;
std.debug.print("Permissoes: {o}\n", .{mode & 0o777});
// Verificar tipos de permissao
const owner_read = (mode & 0o400) != 0;
const owner_write = (mode & 0o200) != 0;
const owner_exec = (mode & 0o100) != 0;
std.debug.print("Dono: r={} w={} x={}\n", .{
owner_read,
owner_write,
owner_exec,
});
}
Manipulando Caminhos
Zig fornece funcoes utilitarias para trabalhar com caminhos de forma portavel:
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const caminho = "/home/usuario/projetos/zig/main.zig";
// Extrair componentes
std.debug.print("Diretorio: {s}\n", .{std.fs.path.dirname(caminho) orelse "."});
std.debug.print("Nome base: {s}\n", .{std.fs.path.basename(caminho)});
std.debug.print("Extensao: {s}\n", .{std.fs.path.extension(caminho)});
// Juntar caminhos
const junto = try std.fs.path.join(allocator, &.{ "/home", "usuario", "docs" });
defer allocator.free(junto);
std.debug.print("Junto: {s}\n", .{junto});
// Caminho absoluto
const absoluto = try std.fs.cwd().realpathAlloc(allocator, ".");
defer allocator.free(absoluto);
std.debug.print("CWD absoluto: {s}\n", .{absoluto});
}
File Locking
File locking e essencial quando multiplos processos precisam acessar o mesmo arquivo de forma coordenada.
const std = @import("std");
pub fn main() !void {
const arquivo = try std.fs.cwd().openFile("recurso_compartilhado.dat", .{
.mode = .read_write,
});
defer arquivo.close();
// Adquirir lock exclusivo (bloqueante)
try arquivo.lock(.exclusive);
defer arquivo.unlock();
std.debug.print("Lock adquirido! Processando...\n", .{});
// Operacoes seguras no arquivo
try arquivo.seekTo(0);
const writer = arquivo.writer();
try writer.print("Escrita segura pelo processo {d}\n", .{
std.os.linux.getpid(),
});
// Simular processamento
std.time.sleep(2 * std.time.ns_per_s);
std.debug.print("Processamento concluido. Lock sera liberado.\n", .{});
// O defer cuida de desbloquear
}
Memory-Mapped Files (mmap)
Memory-mapped files permitem mapear um arquivo diretamente na memoria virtual do processo, oferecendo acesso eficiente especialmente para arquivos grandes.
const std = @import("std");
const posix = std.posix;
pub fn contarOcorrencias(dados: []const u8, byte_alvo: u8) usize {
var contagem: usize = 0;
for (dados) |b| {
if (b == byte_alvo) contagem += 1;
}
return contagem;
}
pub fn main() !void {
const arquivo = try std.fs.cwd().openFile("texto_grande.txt", .{});
defer arquivo.close();
const stat = try arquivo.stat();
if (stat.size == 0) {
std.debug.print("Arquivo vazio\n", .{});
return;
}
// Mapear arquivo na memoria
const mapeado = try posix.mmap(
null,
stat.size,
posix.PROT.READ,
.{ .TYPE = .PRIVATE },
arquivo.handle,
0,
);
defer posix.munmap(mapeado);
const dados: []const u8 = @as([*]const u8, @ptrCast(mapeado))[0..stat.size];
// Agora podemos acessar como um slice normal
const linhas = contarOcorrencias(dados, '\n');
std.debug.print("Arquivo: {d} bytes, {d} linhas\n", .{ stat.size, linhas });
}
Quando Usar mmap vs read
| Cenario | mmap | read |
|---|---|---|
| Arquivos grandes (>100MB) | Excelente | Consome muita RAM |
| Acesso aleatorio | Otimo | Precisa de seek |
| Acesso sequencial unico | Igual | Ligeiramente melhor |
| Multiplos processos lendo | Memoria compartilhada | Cada um tem copia |
| Arquivos pequenos (<4KB) | Overhead desnecessario | Ideal |
Operacoes Atomicas com Arquivos
Para garantir que uma escrita em arquivo seja atomica (tudo ou nada), use o padrao write-rename:
const std = @import("std");
pub fn escritaAtomica(
allocator: std.mem.Allocator,
caminho: []const u8,
conteudo: []const u8,
) !void {
// 1. Escrever em arquivo temporario
const tmp_path = try std.fmt.allocPrint(allocator, "{s}.tmp", .{caminho});
defer allocator.free(tmp_path);
const tmp_file = try std.fs.cwd().createFile(tmp_path, .{});
try tmp_file.writeAll(conteudo);
try tmp_file.sync(); // fsync para garantir persistencia
tmp_file.close();
// 2. Renomear atomicamente (rename e atomico no Linux)
try std.fs.cwd().rename(tmp_path, caminho);
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
try escritaAtomica(allocator, "config.json", "{\"versao\": 2}\n");
std.debug.print("Escrita atomica concluida\n", .{});
}
Exercicios
Monitor de diretorio: Crie um programa que monitore um diretorio e exiba uma mensagem quando novos arquivos forem criados (polling a cada segundo).
Busca de arquivos: Implemente uma funcao que busque recursivamente arquivos por extensao (similar a
find . -name "*.zig").Concatenador: Crie um programa que concatene multiplos arquivos em um unico arquivo de saida, adicionando cabecalhos entre eles.
Proximo Artigo
No proximo artigo, exploraremos processos e signals, aprendendo a criar processos filhos, comunicacao entre processos e tratamento de sinais.
Conteudo Relacionado
- Artigo anterior: Syscalls Linux
- File I/O em Zig — Tutorial introdutorio
- Gerenciamento de Memoria em Zig — Allocators
- Zig Build System — Organizacao de projetos
Duvidas sobre operacoes de file system? Deixe um comentario ou participe da comunidade Zig Brasil.