Processos e Signals em Zig: Fork, Exec, IPC e Tratamento de Sinais

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:

SignalNumeroDescricao
SIGINT2Ctrl+C no terminal
SIGTERM15Pedido de terminacao
SIGKILL9Terminacao forcada (nao pode ser tratado)
SIGHUP1Terminal desconectado
SIGUSR110Sinal definido pelo usuario
SIGCHLD17Processo 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

  1. Gerenciador de tarefas: Crie um programa que execute multiplos comandos em paralelo (como processos filhos) e exiba o status de cada um ao terminar.

  2. Daemon simples: Implemente um daemon que rode em background, escreva logs periodicamente e responda a SIGUSR1 recarregando configuracao.

  3. 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


Duvidas sobre processos e signals em Zig? Junte-se a comunidade Zig Brasil!

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.