Processos sao a unidade fundamental de execucao em sistemas Unix/Linux. Entender como cria-los, gerencia-los e coordena-los e essencial para qualquer programador de sistemas. Neste terceiro artigo da serie, exploramos como Zig facilita o trabalho com processos, pipes e signals, trazendo seguranca e clareza para operacoes que em C seriam complexas e propensas a erros.
Pre-requisito: Leia os artigos anteriores sobre syscalls e file system.
Executando Processos Filhos
A forma mais comum e segura de executar processos externos em Zig e atraves de std.process.Child. Esta API abstrai as complexidades de fork/exec e gerenciamento de pipes.
Execucao Simples de Comandos
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Executar um comando e capturar a saida
const resultado = try std.process.Child.run(.{
.allocator = allocator,
.argv = &.{ "uname", "-a" },
});
defer {
allocator.free(resultado.stdout);
allocator.free(resultado.stderr);
}
std.debug.print("Saida: {s}\n", .{resultado.stdout});
if (resultado.stderr.len > 0) {
std.debug.print("Erros: {s}\n", .{resultado.stderr});
}
std.debug.print("Codigo de saida: {}\n", .{resultado.term});
}
Processo com Stdin Interativo
const std = @import("std");
pub fn main() !void {
var child = std.process.Child.init(.{
.argv = &.{ "cat", "-n" },
.stdin_behavior = .pipe,
.stdout_behavior = .pipe,
}, std.heap.page_allocator);
try child.spawn();
// Escrever no stdin do processo filho
if (child.stdin) |stdin| {
try stdin.writeAll("Linha 1\n");
try stdin.writeAll("Linha 2\n");
try stdin.writeAll("Linha 3\n");
stdin.close();
child.stdin = null;
}
// Aguardar o processo terminar e capturar saida
const resultado = try child.wait();
std.debug.print("Processo terminou com: {}\n", .{resultado});
}
Pipeline de Processos
Podemos encadear processos como pipes no shell (equivalente a ls -la | grep zig | wc -l):
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Primeiro comando: ls -la
const ls_result = try std.process.Child.run(.{
.allocator = allocator,
.argv = &.{ "ls", "-la" },
});
defer allocator.free(ls_result.stdout);
defer allocator.free(ls_result.stderr);
// Processar a saida em Zig (em vez de chamar grep)
var linhas_zig: usize = 0;
var iter = std.mem.splitScalar(u8, ls_result.stdout, '\n');
while (iter.next()) |linha| {
if (std.mem.indexOf(u8, linha, "zig") != null) {
linhas_zig += 1;
std.debug.print("{s}\n", .{linha});
}
}
std.debug.print("\nTotal de linhas contendo 'zig': {d}\n", .{linhas_zig});
}
Fork e Exec Diretos
Para casos que exigem controle total, Zig permite usar fork e exec diretamente atraves das APIs POSIX.
Fork Basico
const std = @import("std");
const posix = std.posix;
pub fn main() !void {
const pid = try posix.fork();
if (pid == 0) {
// Processo filho
std.debug.print("[Filho PID {d}] Executando tarefa...\n", .{
std.os.linux.getpid(),
});
std.time.sleep(1 * std.time.ns_per_s);
std.debug.print("[Filho] Tarefa concluida\n", .{});
posix.exit(0);
} else {
// Processo pai
std.debug.print("[Pai PID {d}] Filho criado com PID {d}\n", .{
std.os.linux.getpid(),
pid,
});
// Aguardar o filho terminar
const resultado = posix.waitpid(pid, 0);
std.debug.print("[Pai] Filho terminou com status: {}\n", .{resultado});
}
}
Fork com Exec
const std = @import("std");
const posix = std.posix;
pub fn main() !void {
const pid = try posix.fork();
if (pid == 0) {
// No processo filho, substituir com novo programa
const err = posix.execvpeZ(
"/usr/bin/python3",
&.{ "/usr/bin/python3", "-c", "print('Ola do Python!')" },
std.c.environ,
);
// Se exec retornar, houve erro
std.debug.print("Erro no exec: {}\n", .{err});
posix.exit(1);
} else {
// Pai aguarda
_ = posix.waitpid(pid, 0);
std.debug.print("Processo filho concluiu\n", .{});
}
}
Comunicacao Entre Processos (IPC)
Pipes
Pipes sao a forma mais simples de IPC no Unix. Eles criam um canal unidirecional entre dois processos.
const std = @import("std");
const posix = std.posix;
pub fn main() !void {
// Criar pipe: [0] = leitura, [1] = escrita
const pipe_fds = try posix.pipe();
const pid = try posix.fork();
if (pid == 0) {
// Filho: escreve no pipe
posix.close(pipe_fds[0]); // Fechar lado de leitura
const mensagem = "Mensagem do filho para o pai!";
_ = try posix.write(pipe_fds[1], mensagem);
posix.close(pipe_fds[1]);
posix.exit(0);
} else {
// Pai: le do pipe
posix.close(pipe_fds[1]); // Fechar lado de escrita
var buffer: [256]u8 = undefined;
const bytes_lidos = try posix.read(pipe_fds[0], &buffer);
posix.close(pipe_fds[0]);
std.debug.print("Pai recebeu: {s}\n", .{buffer[0..bytes_lidos]});
_ = posix.waitpid(pid, 0);
}
}
Variaveis de Ambiente
Variaveis de ambiente sao outra forma de comunicacao (unidirecional, de pai para filho):
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Ler variavel de ambiente
const home = std.posix.getenv("HOME") orelse "/tmp";
std.debug.print("HOME: {s}\n", .{home});
// Executar processo com variaveis de ambiente customizadas
var env_map = std.process.EnvMap.init(allocator);
defer env_map.deinit();
try env_map.put("MEU_APP_CONFIG", "/etc/meuapp.conf");
try env_map.put("MEU_APP_DEBUG", "true");
try env_map.put("PATH", std.posix.getenv("PATH") orelse "/usr/bin");
const resultado = try std.process.Child.run(.{
.allocator = allocator,
.argv = &.{ "env" },
.env_map = &env_map,
});
defer allocator.free(resultado.stdout);
defer allocator.free(resultado.stderr);
std.debug.print("Ambiente do filho:\n{s}\n", .{resultado.stdout});
}
Tratamento de Signals
Signals sao notificacoes assincronas enviadas a processos pelo kernel ou por outros processos. Os mais comuns sao:
| Signal | Numero | Descricao |
|---|---|---|
| SIGINT | 2 | Ctrl+C no terminal |
| SIGTERM | 15 | Pedido de terminacao |
| SIGKILL | 9 | Terminacao forcada (nao pode ser tratado) |
| SIGHUP | 1 | Terminal desconectado |
| SIGUSR1 | 10 | Sinal definido pelo usuario |
| SIGCHLD | 17 | Processo filho terminou |
Configurando um Signal Handler
const std = @import("std");
const posix = std.posix;
var executando: bool = true;
fn handleSignal(sig: c_int) callconv(.C) void {
if (sig == posix.SIG.INT) {
std.debug.print("\nRecebido SIGINT (Ctrl+C). Encerrando graciosamente...\n", .{});
executando = false;
} else if (sig == posix.SIG.TERM) {
std.debug.print("\nRecebido SIGTERM. Encerrando...\n", .{});
executando = false;
}
}
pub fn main() !void {
// Instalar handler para SIGINT
const act = posix.Sigaction{
.handler = .{ .handler = handleSignal },
.mask = posix.empty_sigset,
.flags = 0,
};
try posix.sigaction(posix.SIG.INT, &act, null);
try posix.sigaction(posix.SIG.TERM, &act, null);
std.debug.print("Programa rodando. Pressione Ctrl+C para encerrar.\n", .{});
std.debug.print("PID: {d}\n", .{std.os.linux.getpid()});
// Loop principal
while (executando) {
std.debug.print(".", .{});
std.time.sleep(500 * std.time.ns_per_ms);
}
// Limpeza graceful
std.debug.print("Limpeza concluida. Ate logo!\n", .{});
}
Shutdown Gracioso
Um padrao muito comum em servidores e daemons:
const std = @import("std");
const posix = std.posix;
const Estado = struct {
rodando: std.atomic.Value(bool),
conexoes_ativas: std.atomic.Value(u32),
};
var estado_global = Estado{
.rodando = std.atomic.Value(bool).init(true),
.conexoes_ativas = std.atomic.Value(u32).init(0),
};
fn signalHandler(_: c_int) callconv(.C) void {
estado_global.rodando.store(false, .release);
}
pub fn main() !void {
// Instalar handlers
const act = posix.Sigaction{
.handler = .{ .handler = signalHandler },
.mask = posix.empty_sigset,
.flags = 0,
};
try posix.sigaction(posix.SIG.INT, &act, null);
try posix.sigaction(posix.SIG.TERM, &act, null);
std.debug.print("Servidor iniciado. PID: {d}\n", .{std.os.linux.getpid()});
while (estado_global.rodando.load(.acquire)) {
// Simular processamento
_ = estado_global.conexoes_ativas.fetchAdd(1, .monotonic);
std.time.sleep(100 * std.time.ns_per_ms);
_ = estado_global.conexoes_ativas.fetchSub(1, .monotonic);
}
// Aguardar conexoes ativas terminarem
std.debug.print("Aguardando conexoes ativas...\n", .{});
while (estado_global.conexoes_ativas.load(.acquire) > 0) {
std.time.sleep(100 * std.time.ns_per_ms);
}
std.debug.print("Shutdown completo.\n", .{});
}
Evitando Processos Zumbis
Um processo zumbi e um processo filho que terminou mas cujo pai ainda nao chamou wait(). Eles consomem entradas na tabela de processos do kernel.
const std = @import("std");
const posix = std.posix;
fn ignorarSIGCHLD() !void {
const act = posix.Sigaction{
.handler = .{ .handler = posix.SIG.DFL },
.mask = posix.empty_sigset,
.flags = posix.SA.NOCLDWAIT, // Evita zumbis automaticamente
};
try posix.sigaction(posix.SIG.CHLD, &act, null);
}
pub fn main() !void {
try ignorarSIGCHLD();
// Agora podemos criar filhos sem preocupacao com zumbis
var i: usize = 0;
while (i < 5) : (i += 1) {
const pid = try posix.fork();
if (pid == 0) {
// Filho executa e termina
std.debug.print("Filho {d} executando\n", .{i});
posix.exit(0);
}
}
std.debug.print("Pai: criou 5 filhos, nenhum sera zumbi\n", .{});
std.time.sleep(2 * std.time.ns_per_s);
}
Exercicios
Gerenciador de tarefas: Crie um programa que execute multiplos comandos em paralelo (como processos filhos) e exiba o status de cada um ao terminar.
Daemon simples: Implemente um daemon que rode em background, escreva logs periodicamente e responda a SIGUSR1 recarregando configuracao.
Watchdog: Crie um processo pai que monitore um processo filho e o reinicie automaticamente se ele falhar.
Proximo Artigo
No proximo artigo, mergulhamos em networking com sockets raw, construindo servidores TCP, trabalhando com protocolos customizados e explorando sockets raw.
Conteudo Relacionado
- Artigo anterior: File System Operations
- Concorrencia em Zig — Threads e paralelismo
- Servidor HTTP em Zig — Aplicacao pratica de processos e I/O
- Zig Debugging — Depuracao de programas
Duvidas sobre processos e signals em Zig? Junte-se a comunidade Zig Brasil!