DevOps & CI/CD

Como Usar Shell Scripting Avançado: Processos, Sinais, Trap e Scripts Robustos em Produção

15 min de leitura

Como Usar Shell Scripting Avançado: Processos, Sinais, Trap e Scripts Robustos em Produção

Entendendo Processos em Shell Um processo em Unix/Linux é uma instância de um programa em execução. Quando você executa um comando no shell, o sistema operacional cria um novo processo com um identificador único chamado PID (Process ID). Compreender como os processos funcionam é fundamental para criar scripts robustos, pois você precisa saber como iniciar, monitorar e finalizar processos corretamente. Cada processo possui um processo pai (PPID) e pode gerar processos filhos. Quando um script shell é executado, ele se torna o processo pai de todos os comandos executados dentro dele. É crucial entender essa hierarquia porque quando o processo pai é finalizado, o comportamento dos filhos depende de como foram inicializados — alguns podem se tornar órfãos, outros podem ser automaticamente encerrados. Executando Processos em Primeiro e Segundo Plano No shell, você pode executar um comando em primeiro plano (foreground) ou segundo plano (background). Quando um processo roda em primeiro plano, ele bloqueia a entrada do terminal até sua

<h2>Entendendo Processos em Shell</h2>

<p>Um processo em Unix/Linux é uma instância de um programa em execução. Quando você executa um comando no shell, o sistema operacional cria um novo processo com um identificador único chamado PID (Process ID). Compreender como os processos funcionam é fundamental para criar scripts robustos, pois você precisa saber como iniciar, monitorar e finalizar processos corretamente.</p>

<p>Cada processo possui um processo pai (PPID) e pode gerar processos filhos. Quando um script shell é executado, ele se torna o processo pai de todos os comandos executados dentro dele. É crucial entender essa hierarquia porque quando o processo pai é finalizado, o comportamento dos filhos depende de como foram inicializados — alguns podem se tornar órfãos, outros podem ser automaticamente encerrados.</p>

<h3>Executando Processos em Primeiro e Segundo Plano</h3>

<p>No shell, você pode executar um comando em primeiro plano (foreground) ou segundo plano (background). Quando um processo roda em primeiro plano, ele bloqueia a entrada do terminal até sua conclusão. Em segundo plano, o comando executa enquanto você pode continuar digitando novos comandos. Isso é controlado pelo operador <code>&amp;</code>.</p>

<pre><code class="language-bash">#!/bin/bash

Processo em primeiro plano (bloqueante)

echo &quot;Iniciando backup...&quot;

tar -czf backup.tar.gz /home/usuario/dados

echo &quot;Backup concluído&quot;

Processo em segundo plano (não-bloqueante)

echo &quot;Iniciando sincronização em background...&quot;

rsync -av /local/ /remoto/ &amp;

SYNC_PID=$!

echo &quot;Sincronização rodando com PID: $SYNC_PID&quot;

Continua executando sem esperar

echo &quot;Script pode fazer outras tarefas...&quot;

wait $SYNC_PID

echo &quot;Sincronização finalizada&quot;</code></pre>

<p>O comando <code>wait</code> pausa a execução do script até que o processo especificado termine. Se você usar <code>wait</code> sem argumentos, aguarda todos os processos em background.</p>

<h3>Monitorando e Controlando Processos</h3>

<p>Para criar scripts robustos, você precisa monitorar o status dos processos filhos. O comando <code>jobs</code> lista processos em background no contexto atual, enquanto <code>ps</code> fornece uma visão mais detalhada de todos os processos do sistema.</p>

<pre><code class="language-bash">#!/bin/bash

Iniciando múltiplos processos em background

for i in {1..3}; do

sleep 30 &amp;

PIDS[$i]=$!

echo &quot;Processo $i iniciado com PID: ${PIDS[$i]}&quot;

done

Monitorando todos os processos

echo &quot;Aguardando conclusão de todos os processos...&quot;

for pid in &quot;${PIDS[@]}&quot;; do

if wait $pid; then

echo &quot;Processo $pid finalizou com sucesso&quot;

else

echo &quot;Processo $pid retornou erro: $?&quot;

fi

done

echo &quot;Todos os processos concluídos&quot;</code></pre>

<h2>Sinais: Comunicação entre Processos</h2>

<p>Sinais são mecanismos de comunicação interprocessual (IPC) que permitem ao sistema operacional notificar um processo sobre eventos. Existem dezenas de sinais em Unix/Linux, sendo alguns dos mais importantes: SIGTERM (encerramento graciosa), SIGKILL (encerramento forçado), SIGSTOP (pausar), SIGCONT (retomar) e SIGUSR1/SIGUSR2 (sinais personalizados definidos pelo usuário).</p>

<p>Quando você pressiona Ctrl+C no terminal, o sistema envia um sinal SIGINT (signal interrupt) ao processo. Ctrl+Z envia SIGSTOP. Diferentemente de SIGKILL, que não pode ser capturado, a maioria dos sinais pode ser tratada por um script através de handlers personalizados, permitindo limpeza adequada de recursos.</p>

<h3>Sinais Comuns em Scripts Shell</h3>

<p>Os sinais mais relevantes para scripting são SIGTERM, SIGINT, SIGHUP (hangup), SIGALRM (alarme), e os sinais do usuário. Cada sinal possui um número associado — SIGTERM é 15, SIGKILL é 9, SIGINT é 2. Você pode enviar sinais usando o comando <code>kill -SINAL PID</code>.</p>

<pre><code class="language-bash">#!/bin/bash

Enviando sinais para um processo

sleep 100 &amp;

SLEEP_PID=$!

echo &quot;Processo iniciado com PID: $SLEEP_PID&quot;

sleep 2

echo &quot;Enviando SIGTERM (encerramento graciosa)...&quot;

kill -TERM $SLEEP_PID

Verifica se o processo ainda está rodando

sleep 1

if kill -0 $SLEEP_PID 2&gt;/dev/null; then

echo &quot;Processo ainda ativo, enviando SIGKILL...&quot;

kill -KILL $SLEEP_PID

else

echo &quot;Processo finalizou com SIGTERM&quot;

fi

Aguarda a conclusão

wait $SLEEP_PID 2&gt;/dev/null

echo &quot;Status final: $?&quot;</code></pre>

<p>A opção <code>-0</code> do comando <code>kill</code> testa se um processo existe sem enviar sinal algum — útil para verificar se um PID ainda está ativo.</p>

<h2>Trap: Capturando e Tratando Sinais</h2>

<p>O comando <code>trap</code> é o mecanismo principal para capturar sinais e executar ações customizadas. Quando um script recebe um sinal, ao invés de ser encerrado abruptamente, ele pode executar uma função ou comando definido no <code>trap</code>. Isso permite limpeza de arquivos temporários, fechamento de conexões, e outras operações críticas.</p>

<p>A sintaxe básica é <code>trap &#039;comando&#039; SINAL</code>. Você pode definir múltiplos traps para diferentes sinais no mesmo script. O trap é herdado por processos filhos, mas pode ser redefinido ou removido com <code>trap - SINAL</code>.</p>

<h3>Implementando Limpeza Segura com Trap</h3>

<p>Um padrão essencial em scripting robusto é definir uma função de limpeza e associá-la aos sinais SIGTERM e SIGINT. Isso garante que recursos sejam liberados mesmo se o script for interrompido.</p>

<pre><code class="language-bash">#!/bin/bash

Variáveis globais

TEMP_DIR=$(mktemp -d)

LOCK_FILE=&quot;/tmp/meu_script.lock&quot;

CHILD_PIDS=()

Função de limpeza

cleanup() {

local exit_code=$?

echo &quot;Executando limpeza...&quot;

Finaliza processos filhos

for pid in &quot;${CHILD_PIDS[@]}&quot;; do

if kill -0 $pid 2&gt;/dev/null; then

echo &quot;Finalizando processo filho: $pid&quot;

kill -TERM $pid

Aguarda com timeout

local count=0

while kill -0 $pid 2&gt;/dev/null &amp;&amp; [ $count -lt 5 ]; do

sleep 1

((count++))

done

Força se ainda estiver ativo

if kill -0 $pid 2&gt;/dev/null; then

kill -KILL $pid

fi

fi

done

Remove arquivos temporários

if [ -d &quot;$TEMP_DIR&quot; ]; then

rm -rf &quot;$TEMP_DIR&quot;

echo &quot;Diretório temporário removido&quot;

fi

Remove lock file

rm -f &quot;$LOCK_FILE&quot;

echo &quot;Limpeza concluída&quot;

exit $exit_code

}

Registra a função de limpeza

trap cleanup SIGTERM SIGINT EXIT

Verifica lock file para evitar múltiplas execuções

if [ -f &quot;$LOCK_FILE&quot; ]; then

echo &quot;Outro processo já está em execução&quot;

exit 1

fi

echo $$ &gt; &quot;$LOCK_FILE&quot;

Executa trabalho principal

echo &quot;Iniciando processamento...&quot;

for i in {1..5}; do

(

echo &quot;Tarefa $i em execução (PID: $$)&quot;

sleep 10

echo &quot;Tarefa $i concluída&quot;

) &amp;

CHILD_PIDS+=($!)

done

Aguarda todos os filhos

wait

echo &quot;Processamento finalizado&quot;</code></pre>

<p>Este exemplo demonstra um padrão robusto: define uma função <code>cleanup</code> que trata múltiplos aspectos (finalização de filhos com timeout, remoção de temporários, lock file), registra essa função para SIGTERM, SIGINT e EXIT. O sinal EXIT é especial — dispara ao final normal do script também, garantindo limpeza em todos os cenários.</p>

<h3>Tratando Sinais Específicos Diferentemente</h3>

<p>Às vezes você quer comportamentos diferentes para sinais distintos. Por exemplo, SIGTERM pode ser uma &quot;solicitação educada&quot;, enquanto SIGINT (Ctrl+C) deveria ser imediato.</p>

<pre><code class="language-bash">#!/bin/bash

Estado global

GRACEFUL_SHUTDOWN=false

Processa SIGTERM com encerramento graciosa

on_sigterm() {

echo &quot;SIGTERM recebido - iniciando encerramento graciosa&quot;

GRACEFUL_SHUTDOWN=true

}

Processa SIGINT com encerramento imediata

on_sigint() {

echo &quot;SIGINT recebido - encerrando imediatamente&quot;

exit 130 # Código padrão para SIGINT

}

trap on_sigterm SIGTERM

trap on_sigint SIGINT

echo &quot;Processando 100 itens...&quot;

for i in {1..100}; do

if [ &quot;$GRACEFUL_SHUTDOWN&quot; = true ]; then

echo &quot;Encerramento graciosa: parando após item $i&quot;

break

fi

echo &quot;Processando item $i&quot;

sleep 1

done

echo &quot;Script finalizado normalmente&quot;</code></pre>

<h2>Scripts Robustos: Padrões e Boas Práticas</h2>

<p>Um script robusto não é apenas um que funciona — é aquele que falha de forma previsível, fornece feedback adequado, lida com erros e se recupera quando possível. Robustez envolve tratamento de erros, validação de entrada, logging, e tratamento de sinais que já discutimos. Vamos integrar todos esses conceitos em um exemplo real.</p>

<h3>Estrutura Recomendada para Scripts Produção</h3>

<p>Scripts destinados a ambiente de produção devem seguir um padrão consistente. Começa com shebang e declaração de opções do shell, prossegue para configuração e funções, depois lógica principal. Cada função deve ter responsabilidade clara e o script deve sair com código apropriado.</p>

<pre><code class="language-bash">#!/bin/bash

set -euo pipefail # Exit on error, undefined vars, pipe failures

IFS=$&#039;\n\t&#039; # Safer field separator

Configuração

readonly SCRIPT_DIR=&quot;$(cd &quot;$(dirname &quot;${BASH_SOURCE[0]}&quot;)&quot; &amp;&amp; pwd)&quot;

readonly SCRIPT_NAME=&quot;$(basename &quot;$0&quot;)&quot;

readonly LOG_FILE=&quot;${LOG_FILE:-/var/log/${SCRIPT_NAME}.log}&quot;

readonly LOCK_FILE=&quot;/tmp/${SCRIPT_NAME}.lock&quot;

readonly PID_FILE=&quot;/var/run/${SCRIPT_NAME}.pid&quot;

Variáveis de estado

declare -a CHILD_PIDS=()

CURRENT_TASK=&quot;&quot;

Funções de logging

log() {

echo &quot;[$(date &#039;+%Y-%m-%d %H:%M:%S&#039;)] [INFO] $*&quot; | tee -a &quot;$LOG_FILE&quot;

}

error() {

echo &quot;[$(date &#039;+%Y-%m-%d %H:%M:%S&#039;)] [ERROR] $*&quot; | tee -a &quot;$LOG_FILE&quot; &gt;&amp;2

}

Função de limpeza

cleanup() {

local exit_code=$?

if [ -n &quot;$CURRENT_TASK&quot; ]; then

error &quot;Interrompido durante: $CURRENT_TASK&quot;

fi

Finaliza filhos

for pid in &quot;${CHILD_PIDS[@]}&quot;; do

if kill -0 &quot;$pid&quot; 2&gt;/dev/null; then

log &quot;Finalizando processo $pid&quot;

if ! kill -TERM &quot;$pid&quot; 2&gt;/dev/null; then

kill -KILL &quot;$pid&quot; 2&gt;/dev/null || true

fi

fi

done

wait &quot;${CHILD_PIDS[@]}&quot; 2&gt;/dev/null || true

Remove lock

rm -f &quot;$LOCK_FILE&quot; &quot;$PID_FILE&quot;

log &quot;Script finalizado com código: $exit_code&quot;

exit &quot;$exit_code&quot;

}

Handlers de sinal

on_sigterm() {

log &quot;SIGTERM recebido&quot;

cleanup

}

on_sigint() {

log &quot;SIGINT recebido (Ctrl+C)&quot;

cleanup

}

Validação de pré-requisitos

check_requirements() {

local required_cmds=(&quot;curl&quot; &quot;jq&quot; &quot;openssl&quot;)

for cmd in &quot;${required_cmds[@]}&quot;; do

if ! command -v &quot;$cmd&quot; &amp;&gt; /dev/null; then

error &quot;Comando obrigatório não encontrado: $cmd&quot;

exit 1

fi

done

if [ ! -w &quot;$(dirname &quot;$LOG_FILE&quot;)&quot; ]; then

error &quot;Sem permissão de escrita em $(dirname &quot;$LOG_FILE&quot;)&quot;

exit 1

fi

}

Verificação de lock

acquire_lock() {

if [ -f &quot;$LOCK_FILE&quot; ]; then

local old_pid

old_pid=$(cat &quot;$LOCK_FILE&quot; 2&gt;/dev/null || echo &quot;&quot;)

if [ -n &quot;$old_pid&quot; ] &amp;&amp; kill -0 &quot;$old_pid&quot; 2&gt;/dev/null; then

error &quot;Outro processo rodando (PID: $old_pid)&quot;

exit 1

fi

rm -f &quot;$LOCK_FILE&quot;

fi

echo $$ &gt; &quot;$LOCK_FILE&quot;

echo $$ &gt; &quot;$PID_FILE&quot;

}

Funções de negócio

process_data() {

local input_file=&quot;$1&quot;

CURRENT_TASK=&quot;processar dados de $input_file&quot;

log &quot;Iniciando: $CURRENT_TASK&quot;

Validação

if [ ! -f &quot;$input_file&quot; ]; then

error &quot;Arquivo não encontrado: $input_file&quot;

return 1

fi

if [ ! -r &quot;$input_file&quot; ]; then

error &quot;Sem permissão de leitura: $input_file&quot;

return 1

fi

Processamento

local line_count=0

while IFS= read -r line; do

((line_count++))

Simula processamento

sleep 0.1

if [ $((line_count % 10)) -eq 0 ]; then

log &quot;Processadas $line_count linhas&quot;

fi

done &lt; &quot;$input_file&quot;

log &quot;Processamento concluído: $line_count linhas&quot;

return 0

}

fetch_remote_data() {

local url=&quot;$1&quot;

local output_file=&quot;$2&quot;

CURRENT_TASK=&quot;baixar dados de $url&quot;

log &quot;Iniciando: $CURRENT_TASK&quot;

if ! curl -sSf -m 30 -o &quot;$output_file&quot; &quot;$url&quot;; then

error &quot;Falha ao baixar de $url&quot;

return 1

fi

log &quot;Download concluído: $output_file&quot;

return 0

}

Programa principal

main() {

log &quot;Iniciando $SCRIPT_NAME (PID: $$)&quot;

check_requirements

acquire_lock

Registra handlers

trap on_sigterm SIGTERM

trap on_sigint SIGINT

trap cleanup EXIT

Trabalho em paralelo

log &quot;Iniciando processamento paralelo&quot;

fetch_remote_data &quot;https://example.com/data.json&quot; &quot;/tmp/data.json&quot; &amp;

CHILD_PIDS+=($!)

process_data &quot;/etc/hostname&quot; &amp;

CHILD_PIDS+=($!)

Aguarda conclusão

local failed=0

for pid in &quot;${CHILD_PIDS[@]}&quot;; do

if ! wait &quot;$pid&quot;; then

((failed++))

fi

done

CURRENT_TASK=&quot;&quot;

if [ $failed -gt 0 ]; then

error &quot;$failed tarefas falharam&quot;

return 1

fi

log &quot;Todas as tarefas completadas com sucesso&quot;

return 0

}

Executa

main &quot;$@&quot;</code></pre>

<p>Este exemplo consolida tudo que vimos: usa <code>set -euo pipefail</code> para falhar rápido e seguro, implementa logging estruturado, trata sinais adequadamente, valida pré-requisitos, gerencia lock files, processa múltiplas tarefas em paralelo, e fornece limpeza garantida.</p>

<h2>Conclusão</h2>

<p>Dominar shell scripting avançado exige compreensão profunda de três pilares: <strong>processos e seu ciclo de vida</strong> — você agora sabe como iniciar, monitorar e finalizar processos corretamente, utilizando <code>&amp;</code>, <code>wait</code> e <code>jobs</code> para coordenação; <strong>sinais como mecanismo de comunicação</strong> — compreende que sinais como SIGTERM, SIGINT e SIGKILL são notificações que permitem ao sistema se comunicar com processos, e que a maioria pode ser capturada para ações customizadas; <strong>trap para tratamento robusto</strong> — implementa handlers que garantem limpeza de recursos mesmo em cenários de interrupção, transformando scripts frágeis em soluções confiáveis para produção.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://www.gnu.org/software/bash/manual/html_node/Signals.html" target="_blank" rel="noopener noreferrer">GNU Bash Manual - Signals</a></li>

<li><a href="https://man7.org/linux/man-pages/man7/signal.7.html" target="_blank" rel="noopener noreferrer">Linux man-pages: signal(7)</a></li>

<li><a href="https://man7.org/linux/man-pages/man1/trap.1p.html" target="_blank" rel="noopener noreferrer">Linux man-pages: trap(1p)</a></li>

<li><a href="https://www.tldp.org/LDP/abs/html/index.html" target="_blank" rel="noopener noreferrer">Advanced Bash-Scripting Guide - Signals and Traps</a></li>

<li><a href="https://linuxcommand.org/" target="_blank" rel="noopener noreferrer">The Linux Command Line by William Shotts - Chapter on Job Control</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em DevOps & CI/CD

Estratégias de Testes em Pipelines CI: Unit, Integration e Smoke Tests na Prática
Estratégias de Testes em Pipelines CI: Unit, Integration e Smoke Tests na Prática

Fundamentos de Estratégias de Testes em Pipelines CI A integração contínua (C...

O que Todo Dev Deve Saber sobre ELK Stack: Elasticsearch, Logstash e Kibana para Logs Centralizados
O que Todo Dev Deve Saber sobre ELK Stack: Elasticsearch, Logstash e Kibana para Logs Centralizados

Introdução ao ELK Stack O ELK Stack é uma solução de código aberto composta p...

Container Registry: Docker Hub, GHCR e Registry Privado com Harbor: Do Básico ao Avançado
Container Registry: Docker Hub, GHCR e Registry Privado com Harbor: Do Básico ao Avançado

O que é um Container Registry Um Container Registry é um repositório centrali...