Não é possível parar um script bash com Ctrl + C

34

Eu escrevi um script bash simples com um loop para imprimir a data e pingar para uma máquina remota:

#!/bin/bash
while true; do
    #     *** DATE: Thu Sep 17 10:17:50 CEST 2015  ***
    echo -e "\n*** DATE:" 'date' " ***";
    echo "********************************************"
    ping -c5 $1;
done

Quando eu o executo a partir de um terminal, não consigo pará-lo com Ctrl + C . Parece que envia o ^ C para o terminal, mas o script não para.

MacAir:~ tomas$ ping-tester.bash www.google.com

*** DATE: Thu Sep 17 23:58:42 CEST 2015  ***
********************************************
PING www.google.com (216.58.211.228): 56 data bytes
64 bytes from 216.58.211.228: icmp_seq=0 ttl=55 time=39.195 ms
64 bytes from 216.58.211.228: icmp_seq=1 ttl=55 time=37.759 ms
^C                                                          <= That is Ctrl+C press
--- www.google.com ping statistics ---
2 packets transmitted, 2 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 40.887/59.699/78.510/18.812 ms

*** DATE: Thu Sep 17 23:58:48 CEST 2015  ***
********************************************
PING www.google.com (216.58.211.196): 56 data bytes
64 bytes from 216.58.211.196: icmp_seq=0 ttl=55 time=37.460 ms
64 bytes from 216.58.211.196: icmp_seq=1 ttl=55 time=37.371 ms

Não importa quantas vezes eu pressione ou o quão rápido eu faço. Eu não posso pará-lo.
Faça o teste e perceba por si mesmo.

Como uma solução paralela, eu estou parando com Ctrl + Z , que para e kill %1 .

O que exatamente está acontecendo aqui com ^ C ?

    
por nephewtom 18.09.2015 / 01:29

8 respostas

22

O que acontece é que ambos bash e ping recebem o SIGINT ( bash sendo não interativo, ping e bash são executados no mesmo grupo de processos que foi criado e configurado como o grupo de processos de primeiro plano do terminal pelo shell interativo do qual você executou o script.

No entanto, bash manipula SIGINT de forma assíncrona, somente depois que o comando em execução no momento é encerrado. bash só sai ao receber esse SIGINT se o comando atualmente em execução morrer de um SIGINT (ou seja, seu status de saída indica que ele foi eliminado pelo SIGINT).

$ bash -c 'sh -c "trap exit\ 0 INT; sleep 10; :"; echo here'
^Chere

Acima, bash , sh e sleep recebem SIGINT quando eu pressiono Ctrl-C, mas sh sai normalmente com um código de saída 0, então bash ignora o SIGINT, e é por isso que vemos "aqui".

ping , pelo menos o do iputils, comporta-se assim. Quando interrompido, imprime estatísticas e sai com um status de saída 0 ou 1, dependendo de seus pings terem sido respondidos ou não. Portanto, quando você pressiona Ctrl-C enquanto ping está em execução, bash observa que você pressionou Ctrl-C em seus manipuladores SIGINT, mas como ping sai normalmente, bash não sai.

Se você adicionar sleep 1 nesse loop e pressionar Ctrl-C enquanto sleep estiver em execução, porque sleep não tem manipulador especial no SIGINT, ele morrerá e reportará a bash que morreu de um erro SIGINT, e nesse caso bash irá sair (ele irá realmente se matar com SIGINT para relatar a interrupção para seu pai).

Quanto ao porquê bash se comporta assim, não tenho certeza e noto que o comportamento nem sempre é determinístico. Acabei de fazer uma pergunta à sobre a lista de discussão bash ( > Atualização : o @Jilles já descobriu o motivo em sua resposta .

O único outro shell que eu encontrei que se comporta de forma semelhante é o ksh93 (Update, mencionado pelo @Jilles, assim como o FreeBSD sh ). Lá, o SIGINT parece ser claramente ignorado. E ksh93 sai sempre que um comando é eliminado pelo SIGINT.

Você tem o mesmo comportamento que bash acima, mas também:

ksh -c 'sh -c "kill -INT \$\$"; echo test'

Não imprime "teste". Ou seja, ele sai (se matando com SIGINT) se o comando que estava esperando morrer de SIGINT, mesmo que ele próprio não tenha recebido essa SIGINT.

Uma solução seria adicionar um:

trap 'exit 130' INT

Na parte superior do script, para forçar o bash a sair ao receber um SIGINT (observe que, em qualquer caso, o SIGINT não será processado de forma síncrona, somente após o comando em execução no momento).

Idealmente, gostaríamos de relatar a nosso pai que morremos de um SIGINT (para que, se for outro script bash , por exemplo, esse script bash também seja interrompido). Fazer um exit 130 não é o mesmo que morrer de SIGINT (embora alguns shells definam $? para o mesmo valor para ambos os casos), mas é frequentemente usado para reportar uma morte por SIGINT (em sistemas onde o SIGINT é 2 que é mais ).

No entanto, para bash , ksh93 ou FreeBSD sh , isso não funciona. Aquele 130 status de saída não é considerado uma morte pelo SIGINT e um script pai não abortará lá.

Assim, uma alternativa possivelmente melhor seria nos matar com SIGINT ao receber o SIGINT:

trap '
  trap - INT # restore default INT handler
  kill -s INT "$$"
' INT
    
por 18.09.2015 / 16:58
13

A explicação é que o bash implementa o WCE (wait e saída cooperativa) para SIGINT e SIGQUIT por link . Isso significa que, se o bash receber SIGINT ou SIGQUIT enquanto aguarda a saída de um processo, ele aguardará até que o processo saia e saia sozinho se o processo terminar nesse sinal. Isso garante que os programas que usam SIGINT ou SIGQUIT em sua interface de usuário funcionem como esperado (se o sinal não tiver causado a finalização do programa, o script continuará normalmente).

Uma desvantagem aparece com programas que pegam SIGINT ou SIGQUIT, mas terminam por causa disso, mas usando uma saída normal () ao invés de reenviar o sinal para si mesmos. Pode não ser possível interromper os scripts que chamam esses programas. Eu acho que a correção real existe nesses programas, como ping e ping6.

Comportamento semelhante é implementado pelo ksh93 e / bin / sh do FreeBSD, mas não pela maioria dos outros shells.

    
por 19.09.2015 / 18:36
5

Como você supõe, isso se deve ao fato de a SIGINT estar sendo enviada para o processo subordinado, e a shell continuar depois que o processo termina.

Para lidar com isso de uma maneira melhor, você pode verificar o status de saída dos comandos que estão em execução. O código de retorno Unix codifica tanto o método pelo qual um processo saiu (chamada ou sinal do sistema) e qual valor foi passado para exit() ou qual sinal terminou o processo. Isso tudo é bastante complicado, mas a maneira mais rápida de usá-lo é saber que um processo que foi terminado por sinal terá um código de retorno diferente de zero. Assim, se você verificar o código de retorno em seu script, poderá sair de si mesmo se o processo filho foi encerrado, removendo a necessidade de inelegociações, como chamadas sleep desnecessárias. Uma maneira rápida de fazer isso em todo o script é usar set -e , embora isso possa exigir alguns ajustes para os comandos cujo status de saída é um valor diferente de zero.

    
por 18.09.2015 / 01:38
4

O terminal percebe o control-c e envia um sinal INT para o grupo de processos em primeiro plano, que inclui o shell, pois ping não criou um novo grupo de processos em primeiro plano. Isso é fácil de verificar por meio do trapping de INT .

#! /bin/bash
trap 'echo oh, I am slain; exit' INT
while true; do
  ping -c5 127.0.0.1
done

Se o comando que estiver sendo executado tiver criado um novo grupo de processos em primeiro plano, o control-c irá para esse grupo de processos e não para o shell. Nesse caso, o shell precisará inspecionar os códigos de saída, pois ele não será sinalizado pelo terminal.

( INT manipulação em shells pode ser incrivelmente complicada, a propósito, já que o shell às vezes precisa ignorar o sinal, e algumas vezes não. Source dive se curioso, ou ponderar: tail -f /etc/passwd; echo foo )

    
por 18.09.2015 / 02:02
2

Bem, tentei adicionar um sleep 1 ao script bash e bang!
Agora posso pará-lo com dois Ctrl + C .

Ao pressionar Ctrl + C , um sinal SIGINT é enviado para o processo atualmente executado, comando que foi executado dentro do loop. Então, o processo subshell continua executando o próximo comando no loop, que inicia outro processo. Para poder parar o script, é necessário enviar dois sinais SIGINT , um para interromper o comando atual em execução e outro para interromper o processo subshell .

No script sem a chamada sleep , pressionando Ctrl + C muito rápido e muitas vezes parece não funcionar, e não é possível sair do loop. Meu palpite é que pressionar duas vezes não é rápido o suficiente para fazê-lo apenas no momento certo entre a interrupção do processo atual executado e o início do próximo. Todo Ctrl + C pressionado enviará um SIGINT para um processo executado dentro do loop, mas não para o subshell .

No script com sleep 1 , essa chamada suspenderá a execução por um segundo e, quando for interrompida pelo primeiro Ctrl + C (primeiro SIGINT ), a subshell levará mais tempo para executar o próximo comando. Então, agora, o segundo Ctrl + C (segundo SIGINT ) irá para o subshell , e a execução do script terminará.

    
por 18.09.2015 / 01:29
0

Tente isto:

#!/bin/bash
while true; do
   echo "Ctrl-c works during sleep 5"
   sleep 5
   echo "But not during ping -c 5"
   ping -c 5 127.0.0.1
done

Agora mude a primeira linha para:

#!/bin/sh

e tente novamente - veja se o ping agora é interrompível.

    
por 10.02.2016 / 23:10
0
pgrep -f process_name > any_file_name
sed -i 's/^/kill /' any_file_name
chmod 777 any_file_name
./any_file_name

por exemplo, pgrep -f firefox irá fazer o PID executar firefox e irá salvar este PID em um arquivo chamado any_file_name . O comando 'sed' adicionará o kill no início do número do PID no arquivo 'any_file_name'. Terceira linha irá executar o arquivo any_file_name . Agora, a linha matará o PID disponível no arquivo any_file_name . Escrever as quatro linhas acima em um arquivo e executá-lo pode fazer o Control - C . Trabalhando absolutamente bem para mim.

    
por 17.04.2017 / 08:44
-3

Você é uma vítima de um bug muito conhecido. O Bash faz o jobcontrol para scripts, o que é um erro.

O que acontece é que o bash executa os programas externos em um grupo de processos diferente do que ele usa para o próprio script. Como o grupo de processos TTY é definido para o grupo de processos do processo de primeiro plano atual, somente esse processo de primeiro plano é eliminado e o loop no script de shell continua.

Para verificar: Busque e compile um Bourne Shell recente que implemente o pgrp (1) como um programa embutido, depois adicione um / bin / sleep 100 (ou / usr / bin / sleep dependendo da sua plataforma) ao loop de script e então inicie o Bourne Shell. Depois que você usou ps (1) para obter os IDs do processo para o comando sleep e o bash que executa o script, chame pgrp <pid> e substitua "< pid >" pelo ID do processo do sleep e do bash que executa o script. Você verá diferentes IDs de grupos de processos. Agora chame algo como pgrp < /dev/pts/7 (substitua o nome tty pelo tty usado pelo script) para obter o grupo de processos tty atual. O grupo de processos TTY é igual ao grupo de processos do comando sleep.

Para corrigir: use um shell diferente.

As fontes recentes do Bourne Shell estão no meu pacote de ferramentas que você pode encontrar aqui:

link

    
por 18.09.2015 / 11:54