The Strawgler

Este script é um ótimo ponto de partida para aprender conceitos de shell scripting. Eu criei para ajudar a debugar um possível leak de memória no Strawberry Music Player, um player de música multiplataforma escrito em Qt e C++, que utiliza o gStreamer como um de seus backends. Identifiquei esse problema de crescimento de consumo de memória RAM e precisava saber em intervalos pormenorizadamente o valor do consumo de RAM enquanto eu debugava. A idéia desse script é printar em intervalos definidos a quantidade em MB de RAM consumida pelo processo determinado, no caso por default, “strawberry”. Como um bonus ele reinicia o processo se atingir um threshold superior em porcentagem definida também pela variável respectiva ($MEM_THRESHOLD).

Vamos comentar seus truques e analisar o código!

Truques e Técnicas:

  • Valores Padrão: O script fornece valores padrão para argumentos, tornando-o flexível. $1, $2, $3 seriam os tres primeiros parametros passados na linha de comando para o script: caso nao sejam passados existem valores padrão: strawberry, para o nome do processo, 30 segundos, para o intervalo de iteração e 20, no caso os vinte porcento do total de RAM do sistema.
  • Tratamento de Erros: O script verifica se há instâncias duplicadas para evitar conflitos, e se recusa a iniciar mais de uma instância de si mesmo.
  • Execução em Segundo Plano: O processo reiniciado é jogado para segundo plano, permitindo que o script continue o monitoramento, sem bloquear o fluxo do script.
  • bc para Cálculos: O script aproveita o bc para realizar cálculos dentro do ambiente shell. Isso acontece porquê shell não reconhece nada além de strings e números inteiros: um ponto-flutuante é uma string, para o shell. Mas não para a calculadora bc! Fica a dica.
  • Detalhe sobre prevenir scripts duplicados: O script verifica se outra instância já está em execução usando pgrep. Tem uma razão, não utilizar ps. Verifique depois o manual de pgrep e suas opções;
  • Se outra instância for encontrada (pela contagem de wc -l), o script sai com uma mensagem de erro.

Este script demonstra vários conceitos de shell scripting. Ao compreendê-lo e modificá-lo, você pode aprender a automatizar várias tarefas em seu sistema.

#!/bin/bash

# Set process to monitor mem usage
STRAWGLER=${1:-"strawberry"};
# Set loop interval (in seconds)
INTERVAL=${2:-30};
# Set memory threshold (in percentage)
MEM_THRESHOLD=${3:-20};
export LC_ALL=C;

strawgler_main() {

# Loop continuously, restart process if reaches threshold!
while true; do
# Get total RAM in Megabytes
TOTAL_MEM=$(free -m | awk '/Mem:/{print $2}')
# Get memory in MB used by the monitored process
STRAWGLER_MEM=$(ps -eO rss,fname | grep ${STRAWGLER}$ | awk '{sum+=$2} END {print sum/1024}')
# Calculate memory usage percentage
MEM_USAGE=$(echo "scale=2; 100 * $STRAWGLER_MEM / $TOTAL_MEM" | bc)
echo "${STRAWGLER_MEM} MB (${MEM_USAGE}%)";
###notify-send -i ${STRAWGLER} -a ${STRAWGLER} "${STRAWGLER_MEM} MB ($MEM_USAGE%)";
# Check if memory usage exceeds threshold
if [[ $(echo "$MEM_USAGE >= $MEM_THRESHOLD" | bc -l) -eq 1 ]]; then
echo "${STRAWGLER} memory usage ($MEM_USAGE%) exceeded threshold. Restarting..."
notify-send -i ${STRAWGLER} -a ${STRAWGLER} "${STRAWGLER} mem: ($MEM_USAGE%)" "process Exceeded threshold. Restarting!";
# Send HUP signal to gracefully restart process
killall -HUP ${STRAWGLER};
# Wait to allow shutdown
sleep 1;
# then Start "${STRAWGLER}" in background
${STRAWGLER} &
fi

# Wait some time before checking again
sleep ${INTERVAL};

done

}

## Check if there is an instance already running
if [ $( pgrep -x "$( basename $0 )" | grep -v $$ | wc -l ) -gt 1 ]; then
echo "Tried to spawn more than one script, ¡there can be only one!";
exit 1;
else
strawgler_main ${1};
fi

Shebang (#! /bin/bash):

  • Esta linha indica ao sistema que use o interpretador bash para executar o script.

Variáveis:

  • $STRAWGLER: Esta variável armazena o nome do processo a ser monitorado. O padrão é “strawberry” se nenhum argumento for fornecido ao executar o script (por exemplo, ./script.sh).
  • $INTERVAL: Esta variável define o intervalo do loop em segundos entre verificações. O padrão é 30 segundos.
  • $MEM_THRESHOLD: Esta variável define o limite de uso de memória em porcentagem. O padrão é 20%.

LC_ALL=C: Isso força o script a usar o locale C, garantindo comportamento consistente independente do locale padrão do sistema, assim se espera ponto ao invés de vírgulas, para separar pontos decimais, por exemplo.

Função strawgler_main:

  • Esta função representa o loop principal do script.

Looping (while true):

  • O script entra em loop contínuo usando while true. Isso garante que o script continue monitorando o processo.

Cálculo de Uso de Memória:

free -m
  • Este comando recupera a memória total do sistema em Megabytes.
ps -eO rss,fname
  • Este comando lista todos os processos com seu tamanho do conjunto residente (RSS) e nome do arquivo.
grep ${STRAWGLER}$
  • Filtra a saída para obter informações apenas para o processo chamado “strawberry” (ou o nome armazenado em $STRAWGLER, passado como o primeiro parâmetro $1 ).
awk '{sum+=$2} END {print sum/1024}'
  • Processa a saída filtrada:
sum+=$2
  • Adiciona o uso de memória RSS de cada processo correspondente a uma variável sum.
END {print sum/1024}
  • Após processar todas as linhas, imprime o uso total de memória em Megabytes. O awk por si só é uma linguagem de programação!
  • bc: Este comando realiza cálculos matemáticos e garante a representação correta dos valores fracionários.
echo "scale=2; 100 * $STRAWGLER_MEM / $TOTAL_MEM" 
  • Esta linha prepara o cálculo para bc. Define a escala para 2 casas decimais e calcula a porcentagem de uso de memória.

Verificação do Limite de Memória:

if [[ $(echo "$MEM_USAGE >= $MEM_THRESHOLD" | bc -l) -eq 1 ]]; then
  • Esta instrução condicional complexa verifica se o uso de memória ($MEM_USAGE) é maior ou igual ao limite ($MEM_THRESHOLD).bc -l: Informa ao bc para usar a biblioteca matemática para operadores de comparação como >=.-eq 1: Verifica se o resultado da comparação é verdadeiro (1).

Reinício do Processo (se o limite for excedido):

  • O script envia uma notificação usando notify-send (descomentado no código).
killall -HUP ${STRAWGLER}
  • Envia um sinal HUP para todos os processos nomeados por $STRAWGLER. O sinal HUP geralmente aciona uma reinicialização amigável do processo.
  • sleep 1: Permite uma breve pausa para o processo encerrar.
${STRAWGLER} &
  • Inicia o processo nomeado por $STRAWGLER em segundo plano.

O script utiliza o comando pgrep para verificar se outra instância já está em execução, em vez de ps. Existem algumas razões para essa escolha:

Eficiência:

  • O pgrep é geralmente mais rápido e eficiente que o ps para encontrar processos específicos, especialmente quando se lida com um grande número de processos. Isso se deve ao fato de que o pgrep usa técnicas otimizadas para pesquisa de processos, enquanto o ps pode ler todos os processos em execução no sistema, o que pode ser um processo mais lento e pesado.

Precisão:

  • O pgrep é projetado para identificar processos com base em seus nomes ou IDs, enquanto o ps oferece uma gama mais ampla de opções de pesquisa, o que pode levar a resultados mais imprecisos no contexto deste script. O pgrep é mais preciso para encontrar a instância específica do script em execução.

Simplicidade:

  • O pgrep possui uma sintaxe mais simples e direta para encontrar processos por nome, tornando-o mais fácil de usar e ler no script. O ps, por outro lado, possui diversas opções e flags que podem tornar a construção do comando de pesquisa mais complexa.

Embora o ps possa ser usado para encontrar scripts duplicados, sua desvantagem em termos de eficiência e precisão o torna menos adequado para essa tarefa específica.

A escolha do pgrep demonstra uma boa prática de selecionar a ferramenta mais adequada para cada tarefa, priorizando eficiência, precisão e simplicidade no código.

E com isso eu automatizei a tarefa de me reportar a RAM utilizada, gerar logs centralizados, etc. não tendo que me preocupar com movimentações manuais para saber a informação que eu precisava, e com certeza isso colaborou para eu ter solucionado o problema de forma mais ágil e menos pentelha.

Obviamente esse script serve como safe-guard contra qualquer memory leak em qualquer aplicativo, impedindo exaustão de recursos, mas entendam ele antes de usar, cada caso tem um uso diferente. O meu script manda um killall -HUP em caso de atingir o threshold, mas pq não executar outra coisa mediante esse trigger?

Por hj chega! VLW!