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


42

Eu escrevi um script bash simples com um loop para imprimir a data e executar ping em 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 o executo de um terminal, não consigo parar com ele Ctrl+C. Parece que envia o ^Cpara 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 com que rapidez. Eu não sou capaz de pará-lo.
Faça o teste e realize por si mesmo.

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

O que exatamente está acontecendo aqui ^C?

Respostas:


26

O que acontece é que tanto bashe pingreceber a SIGINT ( bashsendo não interativa, tanto pinge bashexecutados no mesmo grupo de processos que foi criado e definido como grupo de processo de primeiro plano do terminal pelo shell interativo você executou o script a partir).

No entanto, bashlida com o SIGINT de forma assíncrona, somente após a saída do comando em execução no momento. bashsó 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 morto pelo SIGINT).

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

Acima, bash, she sleepreceber SIGINT quando eu pressionar Ctrl-C, mas shsaídas normalmente com um código de saída 0, de modo bashignora a SIGINT, que é por isso que vemos "aqui".

ping, pelo menos o de iputils, se comporta assim. Quando interrompido, ele imprime estatísticas e sai com um status de saída 0 ou 1, dependendo se seus pings foram ou não respondidos. Portanto, quando você pressiona Ctrl-C enquanto pingestá em execução, bashobserva que você pressionou Ctrl-Cseus manipuladores SIGINT, mas desde que pingsai normalmente, bashnão sai.

Se você adicionar um sleep 1nesse loop e pressionar Ctrl-Cenquanto sleepestiver em execução, porque sleepnão possui um manipulador especial no SIGINT, ele morrerá e informará bashque morreu de um SIGINT e, nesse caso bash, sairá (ele realmente se matará com o SIGINT, de modo que para relatar a interrupção ao pai).

Quanto a por que bashse comporta assim, não tenho certeza e observo que o comportamento nem sempre é determinístico. Acabei de fazer a pergunta na bashlista de discussão de desenvolvimento ( atualização : @Jilles agora identificou o motivo em sua resposta ).

O único outro shell que descobri que se comporta de maneira semelhante é o ksh93 (o Update, como mencionado pelo @Jilles, o FreeBSDsh ). Lá, o SIGINT parece ser claramente ignorado. E ksh93sai sempre que um comando é morto pelo SIGINT.

Você obtém o mesmo comportamento bashacima, mas também:

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

Não gera "teste". Ou seja, ele sai (se matando com o SIGINT lá) se o comando que ele estava esperando morre do SIGINT, mesmo que ele próprio não tenha recebido esse SIGINT.

Uma solução alternativa seria adicionar um:

trap 'exit 130' INT

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

Idealmente, gostaríamos de informar aos nossos pais que morremos de um SIGINT (para que, se for outro bashscript, por exemplo, esse bashscript também seja interrompido). Fazer um exit 130não é o mesmo que morrer com o SIGINT (embora algumas conchas tenham o $?mesmo valor nos dois casos), no entanto, é frequentemente usado para relatar uma morte pelo SIGINT (em sistemas onde o SIGINT é 2, o que é mais).

No entanto bash, para o ksh93FreeBSD sh, isso não funciona. Esse status de saída 130 não é considerado como uma morte pelo SIGINT e um script pai não seria interrompido lá.

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

trap '
  trap - INT # restore default INT handler
  kill -s INT "$$"
' INT

11
a resposta de jilles explica o "porquê". Como um exemplo ilustrativo , considere  for f in *.txt; do vi "$f"; cp "$f" newdir; done. Se o usuário digitar Ctrl + C durante a edição de um dos arquivos, viapenas exibirá uma mensagem. Parece razoável que o loop continue depois que o usuário terminar de editar o arquivo. (E sim, eu sei que você poderia dizer vi *.txt; cp *.txt newdir, estou apenas a apresentação do forcircuito como um exemplo.)
Scott

@ Scott, bom ponto. Embora vi( vimpelo menos) desabilite o tty isigdurante a edição (no entanto, não é possível quando você executa :!cmd, e isso se aplica muito nesse caso).
Stéphane Chazelas

@ Tim, veja minha edição para correção na sua edição.
Stéphane Chazelas

@ StéphaneChazelas Obrigado. É assim porque pingsai com 0 depois de receber o SIGINT. Eu encontrei um comportamento semelhante quando um script bash contém em sudovez de ping, mas sudosai com 1 após receber o SIGINT. unix.stackexchange.com/questions/479023/…
Tim

13

A explicação é que o bash implementa o WCE (espera e saída cooperativa) para SIGINT e SIGQUIT em http://www.cons.org/cracauer/sigint.html . Isso significa que, se o bash receber SIGINT ou SIGQUIT enquanto aguarda a saída de um processo, ele aguardará até o processo sair e sairá se o processo sair nesse sinal. Isso garante que os programas que usam SIGINT ou SIGQUIT em sua interface com o usuário funcionem conforme o esperado (se o sinal não fez o programa terminar, o script continuará normalmente).

Uma desvantagem aparece nos programas que capturam SIGINT ou SIGQUIT, mas terminam por causa disso, mas usando uma saída normal () em vez de reenviar o sinal para si mesmos. Talvez não seja possível interromper scripts que chamam esses programas. Eu acho que a correção real existe em programas como o ping e o ping6.

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


Obrigado, isso faz muito sentido. Observo que o FreeBSD sh também não é cancelado quando o cmd sai com exit (130), que é uma maneira comum de relatar a morte por SIGINT de uma criança (mksh faz isso, exit(130)por exemplo, se você interromper mksh -c 'sleep 10;:').
Stéphane Chazelas 19/09/2015

5

Como você supõe, isso se deve ao envio do SIGINT ao processo subordinado, e o shell continuando depois que o processo é encerrado.

Para lidar com isso de uma maneira melhor, você pode verificar o status de saída dos comandos em execução. O código de retorno do Unix codifica o método pelo qual um processo foi encerrado (chamada ou sinal do sistema) e qual valor foi passado exit()ou qual sinal encerrou o processo. Tudo isso é bastante complicado, mas a maneira mais rápida de usá-lo é saber que um processo que foi encerrado por sinal terá um código de retorno diferente de zero. Portanto, se você verificar o código de retorno em seu script, poderá sair de si mesmo se o processo filho tiver sido encerrado, eliminando a necessidade de deselegâncias, como sleepchamadas desnecessárias . Uma maneira rápida de fazer isso em todo o script é usar set -e, embora possa exigir alguns ajustes para comandos cujo status de saída é esperado como diferente de zero.


11
Set -e não funciona corretamente em bash a menos que você estiver usando bash-4
Schily

O que significa "não funciona corretamente"? Eu usei no bash 3 com sucesso, mas provavelmente há alguns casos extremos.
Tom Hunt

Em alguns casos simples, o bash3 saiu por erro. No entanto, isso não aconteceu no caso geral. Como resultado típico, o make não parava ao criar um destino com falha e era de um makefile que funcionava em uma lista de destinos nos subdiretórios. David Korn e eu tivemos que enviar muitas semanas com o mantenedor do bash para convencê-lo a corrigir o bug do bash4.
schily

4
Observe que o problema aqui é que pingretorna com um status de saída 0 ao receber o SIGINT e, em bashseguida, ignora o SIGINT que ele próprio recebeu, se for esse o caso. Adicionar um "set -e" ou verificar o status de saída não ajudará aqui. Adicionar uma armadilha explícita no SIGINT ajudaria.
Stéphane Chazelas

4

O terminal percebe o controle-c e envia um INTsinal ao grupo de processos em primeiro plano, que aqui inclui o shell, pois pingnão criou um novo grupo de processos em primeiro plano. Isso é fácil de verificar capturando 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 está sendo executado criou um novo grupo de processos em primeiro plano, o controle-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 não será sinalizado pelo terminal.

( INTHandling em conchas podem ser fabulosamente complicado, pelo caminho, como o shell às vezes precisa ignorar o sinal, e às vezes não Fonte mergulho se curioso, ou ponderar:. tail -f /etc/passwd; echo foo)


Neste caso, o problema não é sinal de manipulação mas o fato de que o bash faz jobcontrol no script embora não deva, veja a minha resposta para mais informações
Schily

Para que o SIGINT vá para o novo grupo de processos, o comando também precisará fazer um ioctl () no terminal para torná-lo o grupo de processos em primeiro plano do terminal. pingnão tem motivos para iniciar um novo grupo de processos aqui e a versão do ping (iputils no Debian) com a qual eu posso reproduzir o problema do OP não cria um grupo de processos.
Stéphane Chazelas

11
Observe que não é o terminal que envia o SIGINT, é a disciplina de linha do dispositivo tty (o driver (código no kernel) do dispositivo / dev / ttysomething) ao receber um caractere sem escape (pelo lnext geralmente ^ V) ^ C do terminal.
Stéphane Chazelas

2

Bem, eu tentei adicionar um sleep 1ao script bash, e bang!
Agora eu consigo parar com dois Ctrl+C.

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

No script sem a sleepchamada, pressionar Ctrl+Cmuito 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 chegar no momento certo entre a interrupção do processo executado atualmente e o início do próximo. Cada Ctrl+Cpressionado envia um SIGINTpara um processo executado dentro do loop, mas também não para o subshell .

No script com sleep 1, essa chamada suspenderá a execução por um segundo e, quando interrompida pelo primeiro Ctrl+C(primeiro SIGINT), o 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á.


Você está enganado, em um shell que funcione corretamente, um único ^ C é suficiente, veja minha resposta em segundo plano.
schily

Bem, considerando que você recebeu votos negativos, e atualmente sua resposta tem pontuação -1, não estou muito convencido de que devo levar sua resposta a sério.
Nephewtom 18/09/2015

O fato de algumas pessoas terem voto negativo nem sempre está relacionado à qualidade de uma resposta. Se você precisar digitar duas vezes ^ c, definitivamente será vítima de um erro do bash. Você tentou um shell diferente? Você experimentou o verdadeiro Bourne Shell?
schily 19/09/15

Se o shell estiver funcionando corretamente, ele executará tudo, desde um script no mesmo grupo de processos e, em seguida, um único ^ c será suficiente.
schily 19/09/15

11
O comportamento que @nephewtom descreve nesta resposta pode ser explicado por diferentes comandos no script que se comportam de maneira diferente quando recebem Ctrl-C. Se um sono estiver presente, é extremamente provável que o Ctrl-C seja recebido enquanto o sono estiver em execução (assumindo que tudo o mais no loop é rápido). O sono é morto, com o valor de saída 130. O pai do sono, uma concha, percebe que o sono foi morto por sigint e sai. Mas se o script não contém suspensão, o Ctrl-C passa a executar ping, o que reage saindo com 0, para que o shell pai continue executando o próximo comando.
Jonathan Hartley

0

Tente o seguinte:

#!/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 está agora interrompível.


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 firefoxcumprirá o PID da execução firefoxe salvará esse PID em um arquivo chamado any_file_name. O comando 'sed' adicionará o killno início do número PID no arquivo 'any_file_name'. A terceira linha apresentará o any_file_namearquivo executável. Agora, a linha eliminará o PID disponível no arquivo any_file_name. Escrever as quatro linhas acima em um arquivo e executar esse arquivo pode fazer o Control- C. Trabalhando absolutamente bem para mim.


0

Se alguém estiver interessado em uma correção para esse bashrecurso, e não tanto na filosofia por trás dele , aqui está uma proposta:

Não execute o comando problemático diretamente, mas a partir de um wrapper que a) aguarde o término b) não mexa nos sinais ec) não implemente o mecanismo WCE, mas simplesmente morre ao receber a SIGINT.

Esse invólucro pode ser feito com awk+ sua system()função.

$ while true; do awk 'BEGIN{system("ping -c5 localhost")}'; done
PING localhost(localhost (::1)) 56 data bytes
64 bytes from localhost (::1): icmp_seq=1 ttl=64 time=0.082 ms
64 bytes from localhost (::1): icmp_seq=2 ttl=64 time=0.087 ms
^C
--- localhost ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1022ms
rtt min/avg/max/mdev = 0.082/0.084/0.087/0.009 ms
[3]-  Terminated              ping -c5 localhost

Coloque um script como o OP:

#!/bin/bash
while true; do
        echo -e "\n*** DATE:" `date` " ***";
        echo "********************************************"
        awk 'BEGIN{system(ARGV[1])}' "ping -c5 ${1-localhost}"
done

-3

Você é vítima de um conhecido erro do bash. O Bash controla os scripts, o que é um erro.

O que acontece é que o bash executa os programas externos em um grupo de processos diferente do usado para o próprio script. Como o grupo de processos TTY é definido como o grupo de processos do primeiro plano atual, apenas esse processo é 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 interno, adicione um / bin / sleep 100 (ou / usr / bin / sleep, dependendo da sua plataforma) ao loop de script e inicie o Bourne Shell. Depois de usar o 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 da Bourne Shell estão no meu pacote de ferramentas inteligentes que você pode encontrar aqui:

http://sourceforge.net/projects/schilytools/files/


Que versão bashé essa? O AFAIK bashsó faz isso se você passar a opção -m ou -i.
Stéphane Chazelas

Parece que isso não se aplica a bash4 mas quando o OP tem tais problemas, ele parece usar bash3
Schily

Não é possível reproduzir com o bash3.2.48 nem o bash 3.0.16 nem o bash-2.05b (tentado com bash -c 'ps -j; ps -j; ps -j').
Stéphane Chazelas

Definitivamente, isso acontece quando você chama o bash como /bin/sh -ce. Eu tive que adicionar uma solução alternativa feia para smakematar explicitamente o grupo de processos de um comando em execução no momento, a fim de permitir o ^Ccancelamento de uma chamada em camadas. Você verificou se o bash alterou o grupo de processos a partir do ID do grupo de processos com o qual foi iniciado?
schily

ARGV0=sh bash -ce 'ps -j; ps -j; ps -j'relata o mesmo pgid para ps e bash em todas as invocações de 3 ps. (ARGV0 = sh é uma zshmaneira de passar o argumento [0]).
Stéphane Chazelas
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.