Como propagar um sinal para os processos infantis de um script bash

Como propagar um sinal para os processos infantis de um script bash

Suponha que escrevamos um script que gera um ou mais processos de execução longa; Se o referido script recebe um sinal como Sigint ou Sigterm, Provavelmente queremos que seus filhos também sejam demitidos (normalmente quando os pais morrem, os filhos sobrevivem). Também podemos querer executar algumas tarefas de limpeza antes que o próprio script saia. Para poder alcançar nosso objetivo, devemos primeiro aprender sobre grupos de processos e como executar um processo em segundo plano.

Neste tutorial, você aprenderá:

  • O que é um grupo de processo
  • A diferença entre os processos de primeiro plano e plano
  • Como executar um programa em segundo plano
  • Como usar a concha espere incorporado para aguardar um processo executado em segundo plano
  • Como encerrar os processos filhos quando o pai recebe um sinal
Como propagar um sinal para os processos infantis de um script bash

Requisitos de software e convenções usadas

Requisitos de software e convenções de linha de comando Linux
Categoria Requisitos, convenções ou versão de software usada
Sistema Distribuição Independente
Programas Nenhum software específico é necessário
Outro Nenhum
Convenções # - requer que os comandos Linux sejam executados com privilégios root diretamente como usuário root ou por uso de sudo comando
$ - Requer que os comandos do Linux sejam executados como um usuário não privilegiado regular

Um exemplo simples

Vamos criar um script muito simples e simular o lançamento de um processo de longa execução:

#!/BIN/BASH TRAP "ECHO SINAL RECEBIDO!"SIGINT ECHO" O script PID é $ "Sleep 30 
cópia de

A primeira coisa que fizemos no roteiro foi criar uma armadilha para pegar Sigint e imprima uma mensagem quando o sinal for recebido. Nós do que fazemos nosso script imprimir seu PID: podemos obter expandindo o $$ variável. Em seguida, executamos o dormir comando para simular um processo de longa execução (30 segundos).

Salvamos o código dentro de um arquivo (digamos que ele é chamado teste.sh), torná -lo executável e inicie -o de um emulador de terminal. Obtemos o seguinte resultado:

O script pid é 101248 

Se estivermos focados no emulador de terminal e pressionamos Ctrl+C enquanto o script está em execução, um Sigint o sinal é enviado e tratado por nosso armadilha:

O script pid é 101248 ^CSignal recebido! 

Embora a armadilha tenha lidado com o sinal como esperado, o script foi interrompido de qualquer maneira. Por que isso aconteceu? Além disso, se enviarmos um Sigint sinalize para o script usando o matar Comando, o resultado que obtemos é bem diferente: a armadilha não é executada imediatamente e o script continua até que o processo infantil não saia (depois 30 segundos de "Sleeping"). Por que essa diferença? Vamos ver…

Grupos de processos, em primeiro plano e trabalhos de fundo

Antes de respondermos às perguntas acima, devemos entender melhor o conceito de grupo de processos.

Um grupo de processos é um grupo de processos que compartilham o mesmo PGID (ID do grupo de processo). Quando um membro de um grupo de processos cria um processo infantil, esse processo se torna um membro do mesmo grupo de processo. Cada grupo de processos tem um líder; Podemos reconhecê -lo facilmente porque é PID e a PGID são os mesmos.

Podemos visualizar PID e PGID de executar processos usando o ps comando. A saída do comando pode ser personalizada para que apenas os campos em que estamos interessados ​​sejam exibidos: neste caso Cmd, PID e PGID. Fazemos isso usando o -o Opção, fornecendo uma lista de campos separados por vírgula como argumento:

$ ps -a -o pid, pgid, cmd 

Se executarmos o comando enquanto nosso script estiver executando a parte relevante da saída que obtemos é a seguinte:

 PID PGID CMD 298349 298349 /BIN /BASH ./teste.SH 298350 298349 Sono 30 

Podemos ver claramente dois processos: o PID do primeiro é 298349, O mesmo que o seu PGID: este é o líder do grupo de processos. Foi criado quando lançamos o script como você pode ver no Cmd coluna.

Este processo principal lançou um processo infantil com o comando sono 30: Como esperado, os dois processos estão no mesmo grupo de processo.

Quando pressionamos o Ctrl-C enquanto focamos no terminal do qual o script foi lançado, o sinal não foi enviado apenas ao processo pai, mas para todo o grupo de processos. Qual grupo de processos? O grupo de processos em primeiro plano do terminal. Todos os processos membros deste grupo são chamados processos de primeiro plano, Todos os outros são chamados processos de fundo. Aqui está o que o manual do Bash tem a dizer sobre o assunto:

VOCÊ SABIA?Para facilitar a implementação da interface do usuário para o controle de trabalho, o sistema operacional mantém a noção de um ID atual do grupo de processos terminais. Membros desse grupo de processos (processos cujo ID do grupo de processo é igual ao ID do grupo de processos do terminal atual) recebem sinais gerados pelo teclado, como o SIGINT. Dizem que esses processos estão em primeiro plano. Os processos de fundo são aqueles cujo ID do grupo de processos difere do do terminal; Tais processos são imunes a sinais gerados pelo teclado.

Quando enviamos o Sigint sinal com o matar Comando, em vez disso, direcionamos apenas o PID do processo pai; Bash exibe um comportamento específico quando um sinal é recebido enquanto aguarda a conclusão de um programa: o "código de armadilha" para esse sinal não é executado até que esse processo termine. É por isso que a mensagem "recebida do sinal" foi exibida somente após o dormir comando saiu.

Para replicar o que acontece quando pressionamos Ctrl-C no terminal usando o matar Comando para enviar o sinal, devemos atingir o grupo de processos. Podemos enviar um sinal para um grupo de processo usando a negação do PID do líder do processo, Então, supondo o PID do líder do processo é 298349 (Como no exemplo anterior), nós funcionaríamos:

$ kill -2 -298349 

Gerenciar a propagação de sinal de dentro de um script

Agora, suponha que lançamos um script de longa execução de um shell não interativo, e queremos que o script gerencie a propagação do sinal automaticamente, para que quando ele recebe um sinal como Sigint ou Sigterm Ele termina sua criança potencialmente longa, eventualmente executando algumas tarefas de limpeza antes de sair. Como podemos fazer isso?

Como fizemos anteriormente, podemos lidar com a situação em que um sinal é recebido em uma armadilha; No entanto, como vimos, se um sinal é recebido enquanto o shell está esperando a conclusão de um programa, o "Código de Trap" é executado somente após a saída do processo infantil.

Não é isso que queremos: queremos que o código de armadilha seja processado assim que o processo pai receber o sinal. Para atingir nosso objetivo, devemos executar o processo infantil no fundo: podemos fazer isso colocando o & símbolo após o comando. No nosso caso, escrevíamos:

#!/Bin/Bash Trap 'Echo Signal recebido!'SIGINT ECHO "O script pid é $" Sleep 30 & 
cópia de

Se deixarmos o script dessa maneira, o processo pai sairia logo após a execução do sono 30 comando, deixando -nos sem a chance de executar tarefas de limpeza depois de terminar ou ser interrompido. Podemos resolver esse problema usando o shell espere construídas em. A página de ajuda de espere define desta maneira:



Aguarda cada processo identificado por um ID, que pode ser um ID de processo ou uma especificação de trabalho e relata seu status de terminação. Se o ID não for dado, espera por todos os processos infantis atualmente ativos e o status de retorno é zero.

Depois de definirmos um processo a ser executado em segundo plano, podemos recuperar seu PID no $! variável. Podemos passar como um argumento para espere Para fazer o processo dos pais aguardar seu filho:

#!/Bin/Bash Trap 'Echo Signal recebido!'SIGINT ECHO "O script PID é $" Sleep 30 e espere $! 
cópia de

Terminamos? Não, ainda há um problema: a recepção de um sinal tratado em uma armadilha dentro do script, causa o espere Construído para retornar imediatamente, sem realmente esperar pelo término do comando em segundo plano. Este comportamento está documentado no manual do Bash:

Quando Bash está aguardando um comando assíncrono através da espera, a recepção de um sinal para o qual uma armadilha foi definida fará com que a espera incorporada retorne imediatamente com um status de saída maior que 128, imediatamente após o que a armadilha é executada. Isso é bom, porque o sinal é tratado imediatamente e a armadilha é executada sem ter que esperar que a criança termine, mas traz um problema, pois em nossa armadilha queremos executar nossas tarefas de limpeza apenas quando tivermos certeza de que a criança processo saiu.

Para resolver este problema, temos que usar espere Novamente, talvez como parte da própria armadilha. Aqui está a aparência do nosso script no final:

#!/Bin/Bash Cleanup () echo "Limpeza ..." # Nosso código de limpeza vai aqui TRAP 'ECHO SINAL RECEBIDO!; matar "$ Child_pid"; aguarde "$ child_pid"; limpeza 'sigint sigterm eco "O script pid é $" sono 30 & child_pid = "$!"Espere" $ Child_pid " 
cópia de

No script, criamos um limpar função onde poderíamos inserir nosso código de limpeza e fizer nosso armadilha Pegue também o Sigterm sinal. Aqui está o que acontece quando executamos este script e enviamos um desses dois sinais para ele:

  1. O script é lançado e o sono 30 O comando é executado em segundo plano;
  2. O PID do processo infantil é "armazenado" no Child_pid variável;
  3. O script aguarda o término do processo infantil;
  4. O script recebe um Sigint ou Sigterm sinal
  5. O espere O comando retorna imediatamente, sem esperar o término da criança;

Neste ponto, a armadilha é executada. Iniciar:

  1. A Sigterm sinal (o matar padrão) é enviado para o Child_pid;
  2. Nós espere para garantir que a criança seja encerrada após receber este sinal.
  3. Depois espere retorna, executamos o limpar função.

Propagar o sinal para várias crianças

No exemplo acima, trabalhamos com um script que tinha apenas um processo infantil. E se um script tiver muitos filhos, e se alguns deles tiverem filhos próprios?

No primeiro caso, uma maneira rápida de obter o pids de todas as crianças é usar o Jobs -P Comando: este comando exibe os PIDs de todos os trabalhos ativos no shell atual. Podemos do que usar matar para encerrá -los. Aqui está um exemplo:

#!/Bin/Bash Cleanup () echo "Limpeza ..." # Nosso código de limpeza vai aqui TRAP 'ECHO SINAL RECEBIDO!; matar $ (empregos -p); espere; limpeza 'sigint sigterm eco "o script pid é $" sono 30 e sono 40 e espere 
cópia de

O script lança dois processos em segundo plano: usando o espere incorporado sem argumentos, esperamos por todos eles e mantemos o processo pai vivo. Quando o Sigint ou Sigterm Os sinais são recebidos pelo script, enviamos um Sigterm para os dois, tendo seus PIDs devolvidos pelo Jobs -P comando (trabalho é um shell embutido; portanto, quando o usamos, um novo processo não é criado).

Se os filhos têm um processo de filhos, e queremos encerrar todos eles quando o ancestral receber um sinal, podemos enviar um sinal para todo o grupo de processos, como vimos antes.

Isso, no entanto, apresenta um problema, já que, enviando um sinal de terminação para o grupo de processos, entraríamos em um loop de "sinal de sinal/sinal". Pense nisso: no armadilha para Sigterm nós enviamos um Sigterm sinal para todos os membros do grupo de processos; Isso inclui o próprio script pai!

Para resolver esse problema e ainda ser capaz de executar uma função de limpeza após o término dos processos infantis, devemos alterar o armadilha para Sigterm Pouco antes de enviarmos o sinal para o grupo de processos, por exemplo:

#!/bin/bash limpeut () echo "limpando ..." # Nosso código de limpeza vai aqui trap 'trap' "sigterm; matar 0; espere; limpeza 'sigint sigterm eco "o script pid é $" sono 30 e sono 40 e espere 
cópia de

Na armadilha, antes de enviar Sigterm Para o grupo de processos, mudamos o Sigterm armadilha, para que o processo pai ignore o sinal e somente seus descendentes são afetados por ele. Observe também que na armadilha, para sinalizar o grupo de processos, usamos matar com 0 como pid. Este é um tipo de atalho: quando o PID passado para matar é 0, todos os processos no atual Grupo de processo é sinalizado.

Conclusões

Neste tutorial, aprendemos sobre grupos de processos e qual é a diferença entre os processos em primeiro plano e em segundo plano. Aprendemos que o Ctrl-C envia um Sigint sinalize para todo o grupo de processos em primeiro plano do terminal de controle e aprendemos a enviar um sinal para um grupo de processo usando matar. Também aprendemos a executar um programa em segundo plano e como usar o espere Shell incorporado para esperar que ela saia sem perder a concha pai. Finalmente, vimos como configurar um script para que, quando ele receber um sinal, ele termina seus filhos antes de sair. Perdi algo? Você tem suas receitas pessoais para realizar a tarefa? Não hesite em me avisar!

Tutoriais do Linux relacionados:

  • Gerenciamento de processos de fundo bash
  • Multi-thread Bash Script e Gerenciamento de Processos no…
  • Uma introdução à automação, ferramentas e técnicas do Linux
  • Como executar comando em segundo plano no Linux
  • Mastering Bash Script Loops
  • Como instalar o sinal no Linux
  • Como rastrear chamadas de sistema feitas por um processo com Strace em…
  • Mint 20: Melhor que o Ubuntu e o Microsoft Windows?
  • Comparando Linux Apache Prefork vs Worker MPMS
  • Como trabalhar com grupos de pacotes DNF