Como usar sinais do Linux em scripts Bash

Publicados: 2022-08-09
Laptop Linux mostrando um prompt bash
fatmawati achmad zaenuri/Shutterstock.com

O kernel do Linux envia sinais aos processos sobre eventos aos quais eles precisam reagir. Scripts bem comportados lidam com sinais de forma elegante e robusta e podem limpar atrás de si mesmos, mesmo se você pressionar Ctrl+C. Aqui está como.

Sinais e Processos

Sinais são mensagens curtas, rápidas e unidirecionais enviadas para processos como scripts, programas e daemons. Eles deixam o processo saber sobre algo que aconteceu. O usuário pode ter pressionado Ctrl+C ou o aplicativo pode ter tentado gravar na memória à qual não tem acesso.

Se o autor do processo previu que um determinado sinal pode ser enviado a ele, ele pode escrever uma rotina no programa ou script para lidar com esse sinal. Essa rotina é chamada de manipulador de sinal . Ele captura ou aprisiona o sinal e executa alguma ação em resposta a ele.

Como gerenciar processos do terminal Linux: 10 comandos que você precisa saber
RELACIONADO Como gerenciar processos do terminal Linux: 10 comandos que você precisa saber

O Linux usa muitos sinais, como veremos, mas do ponto de vista do script, há apenas um pequeno subconjunto de sinais nos quais você provavelmente estará interessado. Em particular, em scripts não triviais, sinais que informam o script para desligar deve ser interceptado (quando possível) e um desligamento normal deve ser executado.

Por exemplo, scripts que criam arquivos temporários ou abrem portas de firewall podem ter a chance de excluir os arquivos temporários ou fechar as portas antes de serem encerradas. Se o script simplesmente morrer no instante em que receber o sinal, seu computador poderá ficar em um estado imprevisível.

Veja como você pode lidar com sinais em seus próprios scripts.

Conheça os sinais

Alguns comandos do Linux têm nomes enigmáticos. Não é assim com o comando que captura sinais. Chama-se trap . Também podemos usar trap com a opção -l (list) para nos mostrar toda a lista de sinais que o Linux usa.

 armadilha -l 

Listando os sinais no Ubuntu com trap -l

Embora nossa lista numerada termine em 64, na verdade existem 62 sinais. Os sinais 32 e 33 estão faltando. Eles não são implementados no Linux. Eles foram substituídos por funcionalidades no compilador gcc para lidar com threads em tempo real. Tudo, desde o sinal 34, SIGRTMIN , até o sinal 64, SIGRTMAX , são sinais em tempo real.

Você verá listas diferentes em diferentes sistemas operacionais do tipo Unix. No OpenIndiana, por exemplo, os sinais 32 e 33 estão presentes, juntamente com vários sinais extras, levando a contagem total para 73.

Listando os sinais no OpenIndiana com trap -l

Os sinais podem ser referenciados por nome, número ou pelo nome abreviado. Seu nome abreviado é simplesmente seu nome com o “SIG” principal removido.

Os sinais são gerados por muitas razões diferentes. Se você pode decifrá-los, seu propósito está contido em seu nome. O impacto de um sinal se enquadra em uma das poucas categorias:

  • Finalizar: O processo é finalizado.
  • Ignore: O sinal não afeta o processo. Este é um sinal apenas de informação.
  • Core: Um arquivo dump-core é criado. Isso geralmente é feito porque o processo transgrediu de alguma forma, como uma violação de memória.
  • Parar: O processo é interrompido. Ou seja, é pausado , não encerrado.
  • Continue: Diz a um processo parado para continuar a execução.

Estes são os sinais que você encontrará com mais frequência.

  • SIGHUP : Sinal 1. A conexão com um host remoto — como um servidor SSH — caiu inesperadamente ou o usuário fez logout. Um script que recebe esse sinal pode terminar normalmente ou pode optar por tentar se reconectar ao host remoto.
  • SIGINT : Sinal 2. O usuário pressionou a combinação Ctrl+C para forçar um processo a fechar, ou o comando kill foi usado com o sinal 2. Tecnicamente, este é um sinal de interrupção, não um sinal de término, mas um script interrompido sem um manipulador de sinal geralmente terminará.
  • SIGQUIT : Sinal 3. O usuário pressionou a combinação Ctrl+D para forçar o encerramento de um processo, ou o comando kill foi usado com o sinal 3.
  • SIGFPE : Sinal 8. O processo tentou realizar uma operação matemática ilegal (impossível), como divisão por zero.
  • SIGKILL : Sinal 9. Este é o sinal equivalente a uma guilhotina. Você não pode pegá-lo ou ignorá-lo, e isso acontece instantaneamente. O processo é encerrado imediatamente.
  • SIGTERM : Sinal 15. Esta é a versão mais atenciosa do SIGKILL . SIGTERM também diz a um processo para terminar, mas pode ficar preso e o processo pode executar seus processos de limpeza antes de fechar. Isso permite um desligamento normal. Este é o sinal padrão gerado pelo comando kill .

Sinais na linha de comando

Uma maneira de capturar um sinal é usar trap com o número ou nome do sinal e uma resposta que você deseja que aconteça se o sinal for recebido. Podemos demonstrar isso em uma janela de terminal.

Este comando intercepta o sinal SIGINT . A resposta é imprimir uma linha de texto na janela do terminal. Estamos usando a opção -e (habilitar escapes) com echo para que possamos usar o especificador de formato “ \n ”.

 trap 'echo -e "\nCtrl+c Detectado."' SIGINT 

Capturando Ctrl+C na linha de comando

Nossa linha de texto é impressa cada vez que pressionamos a combinação Ctrl+C.

Para ver se um trap está definido em um sinal, use a opção -p (print trap).

 armadilha -p SIGINT 

Verificando se uma armadilha está definida em um sinal

Usar trap sem opções faz a mesma coisa.

Para redefinir o sinal para seu estado normal, não capturado, use um hífen “ - ” e o nome do sinal capturado.

 armadilha - SIGINT
 armadilha -p SIGINT 

Removendo uma armadilha de um sinal

Nenhuma saída do comando trap -p indica que não há trap definido nesse sinal.

Capturando Sinais em Scripts

Podemos usar o mesmo comando trap de formato geral dentro de um script. Este script captura três sinais diferentes, SIGINT , SIGQUIT e SIGTERM .

 #!/bin/bash

trap "echo I foi encerrado SIGINT; sair" SIGINT
trap "echo eu fui encerrado pelo SIGQUIT; saia" SIGQUIT
trap "echo I foi encerrado SIGTERM; sair" SIGTERM

eco $$
contador=0

enquanto verdadeiro
Faz 
  echo "Número do loop:" $((++contador))
  dormir 1
feito

As três instruções trap estão na parte superior do script. Observe que incluímos o comando exit dentro da resposta a cada um dos sinais. Isso significa que o script reage ao sinal e sai.

Copie o texto em seu editor e salve-o em um arquivo chamado “simple-loop.sh”, e torne-o executável usando o comando chmod . Você precisará fazer isso em todos os scripts deste artigo se quiser acompanhar em seu próprio computador. Basta usar o nome do script apropriado em cada caso.

 chmod +x simple-loop.sh 

Fazendo um script executável com chmod

O resto do script é muito simples. Precisamos saber o ID do processo do script, para que o script faça eco disso para nós. A variável $$ contém o ID do processo do script.

Criamos uma variável chamada counter e a definimos como zero.

O loop while será executado para sempre, a menos que seja parado à força. Ele incrementa a variável do counter , ecoa-a na tela e adormece por um segundo.

Vamos executar o script e enviar sinais diferentes para ele.

 ./simple-loop.sh 

Um script que o identifica foi finalizado com Ctrl+C

Quando pressionamos “Ctrl + C”, nossa mensagem é impressa na janela do terminal e o script é encerrado.

Vamos executá-lo novamente e enviar o sinal SIGQUIT usando o comando kill . Precisaremos fazer isso de outra janela de terminal. Você precisará usar o ID do processo informado pelo seu próprio script.

 ./simple-loop.sh
 matar -SIGQUIT 4575 

Um script que o identifica foi finalizado com SIGQUIT

Como esperado, o script relata que o sinal chega e termina. E finalmente, para provar o ponto, faremos isso novamente com o sinal SIGTERM .

 ./simple-loop.sh
 matar -SIGTERM 4584 

Um script que o identifica foi encerrado com SIGTERM

Verificamos que podemos capturar vários sinais em um script e reagir a cada um de forma independente. A etapa que promove tudo isso de interessante a útil é adicionar manipuladores de sinal.

Manipulando Sinais em Scripts

Podemos substituir a string de resposta pelo nome de uma função em seu script. O comando trap então chama essa função quando o sinal é detectado.

Copie este texto em um editor e salve-o como um arquivo chamado “grace.sh”, e torne-o executável com chmod .

 #!/bin/bash

trap graceful_shutdown SIGINT SIGQUIT SIGTERM

Graceful_shutdown()
{
  echo -e "\nRemovendo arquivo temporário:" $temp_file
  rm -rf "$temp_file"
  saída
}

temp_file=$(mktemp -p /tmp tmp.XXXXXXXXXX)
echo "Arquivo temporário criado:" $temp_file

contador=0

enquanto verdadeiro
Faz 
  echo "Número do loop:" $((++contador))
  dormir 1
feito

O script define uma armadilha para três sinais diferentes — SIGHUP , SIGINT e SIGTERM — usando uma única instrução de trap . A resposta é o nome da função graceful_shutdown() . A função é chamada sempre que um dos três sinais capturados é recebido.

O script cria um arquivo temporário no diretório “/tmp”, usando mktemp . O modelo de nome de arquivo é “tmp.XXXXXXXXXX”, então o nome do arquivo será “tmp”. seguido por dez caracteres alfanuméricos aleatórios. O nome do arquivo é ecoado na tela.

O resto do script é igual ao anterior, com uma variável de counter e um loop while infinito.

 ./grace.sh 

Um script executando um desligamento normal excluindo um arquivo temporário

Quando o arquivo recebe um sinal que o faz fechar, a função graceful_shutdown() é chamada. Isso exclui nosso único arquivo temporário. Em uma situação do mundo real, ele pode executar qualquer limpeza que seu script exigir.

Além disso, agrupamos todos os nossos sinais capturados e os tratamos com uma única função. Você pode interceptar sinais individualmente e enviá-los para suas próprias funções de manipulador dedicadas.

Copie este texto e salve-o em um arquivo chamado “triple.sh”, e torne-o executável usando o comando chmod .

 #!/bin/bash

trap sgint_handler SIGINT
armadilha sigusr1_handler SIGUSR1
trap exit_handler EXIT

function sigint_handler() {
  ((++conta_assinatura))

  echo -e "\nSIGINT recebeu $sigint_count tempo(s)."

  if [[ "$sigint_count" -eq 3 ]]; então
    echo "Iniciando fechamento."
    loop_flag=1
  fi
}

function sigusr1_handler() {
  echo "SIGUSR1 enviou e recebeu $((++sigusr1_count)) tempo(s)."
}

function exit_handler() { 
  echo "Sair do manipulador: o script está fechando..."
}

eco $$
sigusr1_count=0
sgint_count=0
loop_flag=0

while [[ $loop_flag -eq 0 ]]; Faz
  matar -SIGUSR1 $$
  dormir 1
feito

Definimos três armadilhas na parte superior do script.

  • Um intercepta SIGINT e tem um manipulador chamado sigint_handler() .
  • A segunda captura um sinal chamado SIGUSR1 e usa um manipulador chamado sigusr1_handler() .
  • A armadilha número três intercepta o sinal EXIT . Este sinal é gerado pelo próprio script quando ele fecha. Definir um manipulador de sinal para EXIT significa que você pode definir uma função que sempre será chamada quando o script terminar (a menos que seja morto com o sinal SIGKILL ). Nosso manipulador é chamado exit_handler() .

SIGUSR1 e SIGUSR2 são sinais fornecidos para que você possa enviar sinais personalizados para seus scripts. Como você interpreta e reage a eles depende inteiramente de você.

Deixando os manipuladores de sinal de lado por enquanto, o corpo do script deve ser familiar para você. Ele ecoa o ID do processo para a janela do terminal e cria algumas variáveis. A variável sigusr1_count registra o número de vezes que SIGUSR1 foi manipulado e sigint_count registra o número de vezes que SIGINT foi manipulado. A variável loop_flag é definida como zero.

O loop while não é um loop infinito. Ele parará de fazer o loop se a variável loop_flag for definida para qualquer valor diferente de zero. Cada rotação do loop while usa kill para enviar o sinal SIGUSR1 para este script, enviando-o para o ID do processo do script. Scripts podem enviar sinais para si mesmos!

A função sigusr1_handler() incrementa a variável sigusr1_count e envia uma mensagem para a janela do terminal.

Cada vez que o sinal SIGINT é recebido, a função siguint_handler() incrementa a variável sigint_count e ecoa seu valor na janela do terminal.

Se a variável sigint_count igual a três, a variável loop_flag é definida como um e uma mensagem é enviada para a janela do terminal informando ao usuário que o processo de desligamento foi iniciado.

Como loop_flag não é mais igual a zero, o loop while termina e o script é concluído. Mas essa ação gera automaticamente o sinal EXIT e a função exit_handler() é chamada.

 ./triplo.sh 

Um script usando SIGUSR1, exigindo três combinações Ctrl+C para fechar e capturando o sinal EXIT no desligamento

Após três pressionamentos de Ctrl+C, o script termina e invoca automaticamente a função exit_handler() .

Leia os sinais

Ao capturar sinais e lidar com eles em funções de manipulador diretas, você pode fazer seus scripts Bash se organizarem, mesmo que sejam encerrados inesperadamente. Isso lhe dá um sistema de arquivos mais limpo. Ele também evita a instabilidade na próxima vez que você executar o script e, dependendo do propósito do seu script, pode até evitar falhas de segurança.

RELACIONADO: Como auditar a segurança do seu sistema Linux com o Lynis