Bash E o Tratamento De Sinais (trap)

Introdução

Um tratamento eficiente de sinais é necessário para aplicações robustas, inclusive aquelas escritas em Shell Script. Hoje iremos aprender como tratar sinais em nossos scripts usando o comando trap fornecido pelo bash.

Os Sinais

Sinais são um modo de IPC (Inter Process Communication) que servem para comunicação assíncrona com uma determinada aplicação. Sinais geralmente notificam eventos críticos do sistema, como divisão por zero ou acesso a uma localização de memória inválida. Outros sinais podem ser disparados pelo usuário, como o SIGKILL para terminar um processo e SIGSTOP para paralisar uma aplicação (ctrl+z). No caso de sinais gerados pelo usuário, o sinal só é enviado corretamente para um processo se o usuário é dono desse processo (ou root).

Por curiosidade, você pode verificar a lista de sinais suportados pelo seu sistema. Para isso digite:

$ kill -l

Essa é uma pequena introdução aos sinais, para mais informações veja as referências [1] e [2].

Um script problema

Abaixo um situação comum que envolve a cópia de um arquivo grande:

#!/bin/bash
# [bigfilecpy.sh]

cp /tmp/ultra-mega-big-file.zip ~/backups

Vamos supor que esse script seja interrompido exatamente durante a cópia do arquivo, seja por falta de espaço no disco ou por um sinal do usuário. Nesse caso teremos um arquivo inconsistente em ~/backups que obviamente não se parece com o arquivo real.

Vendo essa situação devemos modificar esse script para que quando ele for interrompido de forma brusca, o arquivo seja deletado da pasta ~/backups. A questão é: Como saber quando um script foi interrompido?

trap

O bash fornece um built-in chamado trap para tratamento de sinais. Com esse comando podemos verificar quando determinados sinais acontecem e se possível, setar alguma rotina para tratamento desse sinal. A sintaxe básica é:

trap [COMANDO] [SINAIS ..]
  • SINAIS: Esse parâmetro é uma lista de sinais separados por espaços que serão associados a alguma ação. Podemos referenciar os sinais pelo seus nomes (veja: $ kill -l) ou pelo número (não portátil – pois sistemas podem utilizar um mesmo número para sinais diferentes).
  • COMANDO: É a ação dos sinais. Para todo sinal em SINAIS recebido pelo script, esse comando será executado. O comportamento do trap muda dependendo do valor desse parâmetro. Veja:
    • Se nulo (“” ou ”), os sinais serão ignorados, ou seja, nenhuma ação será tomada, ex: se o usuário pressionar ctrl+c, o script não irá seguir o comportamento padrão de finalizar.
    • Se igual a ‘-‘, o comportamento padrão de cada sinal será restaurado.
    • Para outros valores, a string especificada será executada como comando.

Vejamos um exemplo:

#!/bin/bash
#[trapexemplo.sh]

function pegarctrlc {
echo Recebi um ctrl+c
}

trap pegarctrlc SIGINT

while :
do
    echo soninho..
    sleep 3
done

Executando:

$ ./trapexemplo.sh
soninho
soninho
^C
Recebi um ctrl+c
soninho
....
^Z
[1]+  Stopped                 ./trapexemplo.sh
$ kill %1

Em:

trap pegarctrlc SIGINT

trocamos o comportamento padrão do sinal SIGINT, que é o de finalizar o processo, para a rotina pegarctrlc, definida no início do script. Como essa rotina não finaliza o script, toda vez que ctrl+c é pressionado a função retorna uma string e o script não é finalizado.

Um meio de finalizar o script é pressionar ctrl+z que gera um SIGSTOP, um sinal que não pode ser ignorado, e depois matar o processo.

Mais exemplos

1) Verificar interrupção do script

Essa é uma solução para o problema da cópia de um arquivo grande. Se o processo de cópia for interrompido, teremos um arquivo inconsistente no disco, assim a aplicação que realiza a cópia deve estar ciente desse fato e remover esse arquivo. Vejamos:

#!/bin/bash
#[bigfilecpy2.sh]

function removefile {
echo Deletando arquivo inconsistente
rm -f ~/backups/ultra-mega-big-file.zip
# Sai do script
exit
}

# Trata os seguintes sinais:
# - SIGINT  : enviado por um ctrl+c
# - SIGTERM : enviado por um kill 
trap removefile SIGINT SIGTERM EXIT

cp /tmp/ultra-mega-big-file.zip ~/backups

# Retorna com o tratamento padrão dos sinais.
trap - SIGINT SIGTERM EXIT

Quando o script receber os sinais SIGINT ou SIGTERM ele pode ter uma saída amigável e deletar o arquivo inconsistente. Repare que outros eventos que finalizam o script, como falta de energia, farão que o arquivo incompleto não seja deletado e para isso outras medidas devem ser tomadas, como um verificador de hashes ou algo do tipo.

Na última linha do script retornamos o comportamento padrão dos sinais. Se um sinal só é interessante ser interceptado durante alguma parte do script, é sempre uma boa prática retornar ao comportamento padrão nas demais partes do script.

2) Limpeza de arquivos temporários

Esse talvez seja o uso mais usual do trap, limpar arquivos temporários. É comum um script criar arquivos temporários para guardarem dados parciais, e por isso, se ele for finalizado bruscamente, esses arquivos  temporários ocuparão espaço no disco desnecessariamente.

Uma boa prática de programação em Shell Script é sempre interceptar certos sinais para realizar limpeza de arquivos temporários.

#!/bin/bash
#[sortdictwords.sh]

DICT='/usr/share/dict/words'

# Remove arquivo temporário caso
# o processo seja interrompido por
# algum desse sinais
trap "rm -f /tmp/temp$$" SIGINT SIGTERM EXIT

# Palavras que iniciam com vogais no arquivo temp
cat $DICT | grep '^[aeiou]' > /tmp/temp$$

# Primeiras dez palavras ordenadas com mais de 5 chars.
sort /tmp/temp$$ | grep '.\{5,\}' | nl | head

# Essa linha usual em scripts pode ser evitada,
# pois o fim do script gera um sinal EXIT que
# e' interceptado pelo trap.
# rm -f /tmp/temp$$

Se um dos sinais citados (ctrl, kill e exit) for entregue ao script, o comando rm será executado. O pseudo-sinal EXIT é lançado quando o processo é finalizado, seja com exit ou fim do script. Ainda nesse post falarei mais sobre pseudo-sinais.

Na referência [4] há um exemplo interessante em que um array de comandos é criado para armazenar os comandos que serão executados pelo trap.

3) Mais exemplos

Vários outros exemplos podem ser feitos usando sinais. Um script pode receber sinais do como USR1 e USR2 para mudar seu comportamento (em [7], veja o exemplo da barra de progresso). Outros exemplos também podem ser encontrados na referência [7].

Pseudo-sinais

Esses sinais não são do sistema, mas sim criados pelo bash para fornecer uma maior flexibilidade aos scripts. Um deles é o EXIT, que é o sinal lançado pelo bash quando o script é finalizado com uma chamada direta a exit ou o fim do script é alcançado. Outra situação em que esse sinal é gerado, acontece quando um programa é executado incorretamente caso a opção errexit esteja ativada[3].

Um outro pseudo-sinal interessante fornecido pelo bash é o DEBUG que nos permite rastrear o uso de uma variável.

O pseudo-sinal DEBUG

O bash permite que determinada variável tenha um atributo especial, chamado trace. Esse atributo somado ao sinal DEBUG nos permite rastrear o valor de uma variável sempre que ela é usada. Para entender, nada melhor que um exemplo:

#!/bin/bash
#[vervalorsemtrap.sh]

VAR=3; echo 'Var tem valor 3'
for i in {1..5}; do VAR=$((VAR * i)); echo VAR=$VAR; done

Executando:

$ ./vervalorsemtrap.sh
Var tem valor 3
VAR=3
VAR=6
VAR=18
VAR=72
VAR=360

Nesse script monitoramos o valor de VAR em algumas situações. Se o script crescer em linhas ficará difícil e trabalhoso verificar a cada instrução o valor dessa variável. Pensando nisso, o bash fornece o sinal DEBUG que é lançado toda vez que uma variável com atributo trace é usada. Reescrevendo o script acima temos.

#!/bin/bash
#[vervalorcomtrap.sh]

# Seta o atributo trace
declare -t VAR=3

trap 'echo "VAR=$VAR"' DEBUG EXIT

for i in {1..5}; do VAR=$((VAR*i)) ; done

Executando:

$ ./vervalorcomtrap.sh | uniq
VAR=3
VAR=6
VAR=18
VAR=72
VAR=360

O uniq foi necessário porque a variável é lida e alterada numa mesma linha, e por isso dois sinais são gerados e assim a saída é duplicada.

Na man-page do trap há mais informações sobre pseudo-sinais.

Conclusão

Um sinal nada mais é que a indicação que um evento do sistema ocorreu ou quando um outro processo do usuário que se comunicar com outro. Nesse post vimos como usar o comando trap fornecido pelo bash para interceptar um sinal. Um sinal sempre tem um comportamento padrão (ex: SIGINT interrompe o processo) e alguns deles, esse comportamento pode ser alterado pelo trap, permitindo-nos indicar um conjunto de comandos para ser executados no lugar.

Esse tutorial foi apenas uma introdução ao assunto. Para mais informações acesse as referências.

Referências

[1] Signals, by The Linux Tutorial (Acessado em: Maio/2012)
http://www.linux-tutorial.info/modules.php?name=MContent&pageid=289

[2] Traps, by Bash Beginners Guide, cap 12 (Acessado em: Maio/2012)
http://linux.die.net/Bash-Beginners-Guide/sect_12_02.html

[3] Writing Robust Shell Scripts, by David Pashley (Acessado em: Maio/2012)
http://www.davidpashley.com/articles/writing-robust-shell-scripts.html

[4] Use the Bash trap Statement to Clean Up Temporary Files, by Mitch Frazier (Acessado em: Maio/2012)
http://www.linuxjournal.com/content/use-bash-trap-statement-cleanup-temporary-files

[5] Errors and Signals and Traps (Oh, My!) – Part 2, by William Shotts, Jr. (Acessado em: Maio/2012)
http://linuxcommand.org/wss0160.php

[6] Debugging by Advanced Bash-Scripting Guide, cap 32 (Acessado em: Maio/2012)
http://tldp.org/LDP/abs/html/debugging.html

Deixe um comentário