Ctrl + Cでbashスクリプトを停止できません


42

日付を出力してリモートマシンにpingするためのループを備えた単純なbashスクリプトを作成しました。

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

ターミナルから実行すると、で停止できませんCtrl+C^Cターミナルに送信するようですが、スクリプトは停止しません。

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

何回押しても、どれだけ速くしても。止めることはできません。
テストを行い、自分で実現します。

副次的な解決策としてCtrl+Z、で停止しkill %1ます。

ここで何が起きているの^Cでしょうか?

回答:


26

起こるのは、両方がSIGINT bashping受け取ることです(対話型でbashなく、両方ともpingbashそのスクリプトを実行した対話型シェルによって端末のフォアグラウンドプロセスグループとして作成および設定された同じプロセスグループで実行されます)。

ただし、bash現在実行中のコマンドが終了した後にのみ、SIGINTを非同期的に処理します。bash現在実行中のコマンドがSIGINTで終了した場合(つまり、その終了ステータスがSIGINTによって強制終了されたことを示している場合)、そのSIGINTを受信したときにのみ終了します。

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

上記、bashshsleep私はCtrl + Cキーを押すとSIGINTを受け取りますが、sh通常は0終了コードで終了なので、bash私たちは「ここ」を参照してください理由である、SIGINTを無視します。

ping、少なくともiputilsからのものは、そのように動作します。中断されると、統計が出力され、pingが応答されたかどうかに応じて、0または1の終了ステータスで終了します。したがって、ping実行中にCtrl-Cをbash押すCtrl-Cと、SIGINTハンドラーで押したが、ping正常に終了するため終了bashしないことに注意してください。

sleep 1そのループにaを追加し、実行Ctrl-C中にを押すと、SIGINTに特別なハンドラーがないsleepためsleep、SIGINTで終了したことを報告しbash、その場合bashは終了します(実際にはSIGINTでそれ自体を殺します中断をその親に報告するため)。

なぜそのようにbash振る舞うのかは定かではありませんが、振る舞いは必ずしも決定的ではないことに注意してください。開発用メーリングリストで質問したbashところです(更新:@Jillesは、彼の回答で理由を打ち明けまし)。

同様に振る舞うことがわかった他の唯一のシェルはksh93です(@Jillesが述べているように、sh更新もFreeBSDもそうです)。SIGINTは明らかに無視されているようです。またksh93、SIGINTによってコマンドが強制終了されるたびに終了します。

bash上記と同じ動作が得られますが、次のことも行われます。

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

「テスト」を出力しません。つまり、待機しているコマンドがSIGINTで終了した場合、SIGINTを受信しなかったとしても、(SIGINTで自身を殺すことで)終了します。

回避策は以下を追加することです:

trap 'exit 130' INT

bashSIGINTの受信時に強制的に終了するスクリプトの上部(いずれの場合も、SIGINTは現在実行中のコマンドが終了した後にのみ同期処理されないことに注意してください)。

理想的には、SIGINTで死亡したことを親に報告したいです(bashたとえば、別のスクリプトの場合、そのbashスクリプトも中断されます)。anを実行することexit 130はSIGINTを死ぬことと同じではありません($?両方のケースで同じ値に設定されるシェルもあります)が、SIGINTによる死亡を報告するためによく使用されます(SIGINTが2であるシステムで)。

しかしためbashksh93やFreeBSD sh、それは動作しません。130終了ステータスはSIGINTによる終了とは見なされず、親スクリプトはそこで終了しません

したがって、おそらくより良い代替手段は、SIGINTを受け取ったときにSIGINTで自分自身を殺すことです。

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

1
jillesの答えは「理由」を説明しています。説明例、検討します  for f in *.txt; do vi "$f"; cp "$f" newdir; done。ユーザーがファイルの編集中にCtrl + Cを入力viすると、メッセージが表示されます。ユーザーがファイルの編集を終了した後もループを継続することは理にかなっています。(そして、はい、あなたが言うことができることを知っていますvi *.txt; cp *.txt newdir;私はfor例としてループを提出しています。)
スコット

@スコット、良い点。けれどもvi(まあvim、少なくとも)が無効に端末を行いisig編集するとき(あなたが実行したとき、それはabviouslyない:!cmdけれども、それは非常に多く、その場合に適用されます)。
ステファンシャゼラス

@Tim、編集の修正については私の編集を参照してください。
ステファンチャゼラス

@StéphaneChazelasありがとう。pingSIGINTを受け取った後に0で終了するためです。bashスクリプトにのsudo代わりにが含まれているがpingsudoSIGINTを受け取った後に1で終了する場合、同様の動作が見つかりました。unix.stackexchange.com/questions/479023/…–
ティム

13

説明は、bashがhttp://www.cons.org/cracauer/sigint.htmlに従ってSIGINTおよびSIGQUITのWCE(待機および協調出口)を実装することです。つまり、bashは、プロセスの終了を待機している間にSIGINTまたはSIGQUITを受信した場合、プロセスが終了するまで待機し、そのシグナルでプロセスが終了した場合は自身を終了します。これにより、ユーザーインターフェイスでSIGINTまたはSIGQUITを使用するプログラムが期待どおりに動作することが保証されます(信号によってプログラムが終了しなかった場合、スクリプトは正常に続行されます)。

SIGINTまたはSIGQUITをキャッチし、そのために終了しますが、シグナルを自分自身に再送信するのではなく、通常のexit()を使用するプログラムには欠点があります。そのようなプログラムを呼び出すスクリプトを中断することはできません。pingやping6などのプログラムに実際の修正があると思います。

同様の動作はksh93とFreeBSDの/ bin / shで実装されていますが、他のほとんどのシェルでは実装されていません。


おかげで、それは非常に理にかなっています。cmdがexit(130)で終了しても、FreeBSD shは中断しないことに注意してください。これは、子のSIGINTによる死亡を報告する一般的な方法です(exit(130)たとえば、割り込みするとmkshが行いますmksh -c 'sleep 10;:')。
ステファンシャゼラス

5

推測すると、これはSIGINTが下位プロセスに送信され、そのプロセスが終了した後もシェルが継続しているためです。

これをより良い方法で処理するために、実行中のコマンドの終了ステータスを確認できます。Unixリターンコードは、プロセスが終了したメソッド(システムコールまたはシグナル)とexit()、プロセスに渡された値またはシグナルを終了したメソッドの両方をエンコードします。これはかなり複雑ですが、それを使用する最も簡単な方法は、シグナルによって終了されたプロセスがゼロ以外のリターンコードを持つことを知ることです。したがって、スクリプトでリターンコードを確認すると、子プロセスが終了した場合に自分自身を終了でき、不要なsleep呼び出しなどの不整合の必要性がなくなります。スクリプト全体でこれを行う簡単な方法は、を使用set -eすることです。ただし、終了ステータスがゼロ以外であると予想されるコマンドには、いくつかの調整が必要になる場合があります。


1
bash-4を使用している場合を除き、bashで-eが正しく機能しない
2015

「正しく動作しない」とはどういう意味ですか?私はbash 3でこれを正常に使用しましたが、おそらくいくつかのエッジケースがあります。
トム・ハント

いくつかの単純なケースでは、bash3はエラーで終了しました。ただし、これは一般的なケースでは発生しませんでした。典型的な結果として、ターゲットの作成に失敗してもmakeは停止せず、これはサブディレクトリ内のターゲットのリストで機能するmakefileからのものでした。David Kornと私は、bash4のバグを修正するよう説得するために、bashメンテナーに何週間もメールを送らなければなりませんでした。
気味悪い

4
ここでの問題は、pingSIGINTを受信すると0の終了ステータスで戻り、そのbash場合は受信したSIGINTを無視することに注意してください。「set -e」を追加するか、終了ステータスを確認しても、ここでは役に立ちません。SIGINTに明示的なトラップを追加すると役立ちます。
ステファンシャゼラス

4

ターミナルはcontrol-cに気付き、新しいフォアグラウンドプロセスグループを作成INTpingていないため、シェルを含むフォアグラウンドプロセスグループに信号を送信します。これは、トラップすることで簡単に確認できINTます。

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

実行中のコマンドが新しいフォアグラウンドプロセスグループを作成した場合、control-cはシェルではなくそのプロセスグループに移動します。その場合、シェルは終了コードを検査する必要があります。これは、端末によって通知されないためです。

INTシェルでの取り扱いは、シェルが時々信号を無視する必要があるとして、途方も、やり方によって、複雑な、そして時にはないソースダイビング好奇心旺盛であれば、あるいは熟考することができます。tail -f /etc/passwd; echo foo


この場合、問題はシグナル処理ではなく、それは、より多くの情報のための私の答え見えないはずですが、bashのスクリプトでjobcontrolをしているという事実
シリー

SIGINTが新しいプロセスグループに移動するには、コマンドは端末に対してioctl()を実行して、端末のフォアグラウンドプロセスグループにする必要があります。pingここで新しいプロセスグループを開始する理由はなく、OPの問題を再現できるpingのバージョン(Debianではiputils)はプロセスグループを作成しません。
ステファンシャゼラス

1
SIGINTを送信するのは端末ではなく、エスケープされていない(通常はlnextによって^ V)^ C文字を受信したときのttyデバイス(/ dev / ttysomethingデバイスのドライバー(カーネル内のコード))の回線制御であることに注意してくださいターミナルから。
ステファンシャゼラス

2

さて、sleep 1bashスクリプトにaを追加してみました。
今、私は2でそれを止めることができCtrl+Cます。

を押すCtrl+CSIGINT、ループ内で実行されたコマンドが現在実行されているプロセスに送信されます。次に、サブシェルプロセスはループ内の次のコマンドの実行を続け、別のプロセスを開始します。スクリプトを停止できるようにするにはSIGINT、実行中の現在のコマンドを中断する信号とサブシェルプロセスを中断する信号の2つの信号を送信する必要があります。

sleep呼び出しのないスクリプトでは、Ctrl+C非常に高速で何度も押すと機能しないようで、ループを終了することはできません。私の推測では、2回押すと、現在実行中のプロセスの中断と次のプロセスの開始の間のちょうど良い瞬間にそれをするのに十分な速さではないということです。Ctrl+C押されるたびにSIGINT、ループ内で実行されるプロセスにa が送信されますが、サブシェルには送信されません。

でのスクリプトではsleep 1、この呼び出しは実行を1秒間中断し、最初のCtrl+C(最初のSIGINT)によって中断されると、サブシェルは次のコマンドを実行するのにさらに時間がかかります。そのため、2番目Ctrl+C(second SIGINT)はサブシェルに移動し、スクリプトの実行は終了します。


正しく動作しているシェルでは、単一の^ Cで十分です。背景については私の答えを参照してください。
気味悪い

ええと、あなたは反対票を投じられており、現在あなたの答えは-1のスコアを持っていることを考えると、私はあなたの答えを真剣に受け止めるべきだとは確信していません。
-nephewtom

一部の人々が投票するという事実は、必ずしも返信の品質に関連するとは限りません。2回^ cを入力する必要がある場合は、間違いなくbashバグの犠牲者です。別のシェルを試しましたか?本物のボーンシェルを試しましたか?
気味悪い

シェルが正常に動作している場合、同じプロセスグループ内のスクリプトからすべてを実行し、単一の^ cで十分です。
気まぐれ

1
この答えで@nephewtomが説明する動作は、Ctrl-Cを受け取ったときにスクリプトのさまざまな動作が異なることで説明できます。スリープが存在する場合、スリープの実行中にCtrl-Cが受信される可能性が非常に高くなります(ループ内の他のすべてが高速であると想定)。スリープは終了値130で終了します。スリープの親であるシェルは、sigintによってスリープが終了したことに気付き、終了します。ただし、スクリプトにスリープが含まれていない場合、Ctrl-Cは代わりにpingに進み、0で終了することで反応するため、親シェルは次のコマンドの実行を続けます。
ジョナサンハートリー

0

これを試して:

#!/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

最初の行を次のように変更します。

#!/bin/sh

もう一度試してください-pingが中断可能かどうかを確認してください。


0
pgrep -f process_name > any_file_name
sed -i 's/^/kill /' any_file_name
chmod 777 any_file_name
./any_file_name

たとえばpgrep -f firefox、実行中のPIDをgrepし、firefoxこのPIDをというファイルに保存しますany_file_name。「sed」コマンドはkill、「any_file_name」ファイルのPID番号の先頭に追加します。3行目はany_file_name実行可能ファイルをファイルします。これで、4行目はファイルで使用可能なPIDを強制終了しますany_file_name。上記の4行をファイルに書き込み、そのファイルを実行すると、Control- を実行できますC。私にとってはまったく問題ありません。


0

誰かがこのbash機能の修正に興味があり、その背後にある哲学にあまり興味がない場合、ここに提案があります:

問題のあるコマンドを直接実行しないでください。ただし、a)終了するのを待つラッパーからb)シグナルを混乱させず、c)WCEメカニズム自体を実装せ、単にaを受信すると死にますSIGINT

このようなラッパーはawk、そのsystem()機能で作成できます。

$ 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

OPのようなスクリプトを入力します。

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

-3

あなたはよく知られているbashバグの犠牲者です。Bashはスクリプトのジョブ制御を行いますが、これは間違いです。

何が起こるかというと、bashはスクリプト自体に使用するものとは異なるプロセスグループで外部プログラムを実行します。TTYプロセスグループは現在のフォアグラウンドプロセスのプロセスグループに設定されているため、このフォアグラウンドプロセスのみが強制終了され、シェルスクリプトのループが継続します。

確認するには:組み込みプログラムとしてpgrp(1)を実装する最近のBourne Shellをフェッチしてコンパイルし、スクリプトループに/ bin / sleep 100(またはプラットフォームに応じて/ usr / bin / sleep)を追加してから、ボーンシェル。ps(1)を使用して、スリープコマンドとスクリプトを実行するbashのプロセスIDを取得したら、pgrp <pid>「<pid>」を呼び出して、スリープのプロセスIDとスクリプトを実行するbashに置き換えます。さまざまなプロセスグループIDが表示されます。次に、次のようなものを呼び出してpgrp < /dev/pts/7(tty名をスクリプトで使用されるttyに置き換えます)、現在のttyプロセスグループを取得します。TTYプロセスグループは、スリープコマンドのプロセスグループと同じです。

修正するには、別のシェルを使用します。

最近のBourne Shellのソースは、ここにある私のずるいツールパッケージに含まれています。

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


それのバージョンは何bashですか?AFAIK bashは、-mまたは-iオプションを渡す場合にのみそれを行います。
ステファンシャゼル

これはbash4にはもはや当てはまらないようですが、OPにそのような問題がある場合、bash3を使用するようです
15

bash3.2.48、bash 3.0.16、bash-2.05b(で試したbash -c 'ps -j; ps -j; ps -j')では再現できません。
ステファンシャゼル

これは間違いなくbashをとして呼び出すときに起こります/bin/sh -ce。階層化されたmake呼び出しを中止smakeできるようにするために、現在実行中のコマンドのプロセスグループを明示的に強制終了するい回避策を追加する^C必要がありました。bashがプロセスグループを、それが開始されたプロセスグループIDから変更したかどうかを確認しましたか?
気味悪い

ARGV0=sh bash -ce 'ps -j; ps -j; ps -j'3つのps呼び出しすべてで、psとbashに対して同じpgidを報告します。(ARGV0 = shはzshargv [0]を渡す方法です)。
ステファンシャゼラス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.