web-dev-qa-db-ja.com

Ctrl + Cでbashスクリプトを停止できない

日付を出力してリモートマシンに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

42
nephewtom

何が起こるかは、bashpingの両方がSIGINTを受け取ることです(bashインタラクティブではありませんインタラクティブではありません。両方のpingbashは、作成され、端末のフォアグラウンドプロセスとして設定されている同じプロセスグループで実行されます。スクリプトを実行したインタラクティブシェルでグループ化します)。

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

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

上記では、Ctrl-Cを押すとbashshsleepはSIGINTを受け取りますが、shは通常0の終了コードで終了するため、bashはSIGINTを無視するため、「ここ」に表示されます。

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

sleepにはSIGINTに特別なハンドラがないため、そのループにsleep 1を追加してCtrl-Cを押すと、sleepにSIGINTで特別なハンドラがないため、終了してbashに報告されます。ケースbashは終了します(割り込みを親に報告するために、実際にはSIGINTで自分自身を強制終了します)。

bashがそのように動作する理由についてはわかりません。また、動作が常に確定的であるとは限りません。私はたった今質問しました bash開発メーリングリストの質問Update:@Jillesが理由を書き留めました 彼の答え )。

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

上記のbashと同じ動作が得られますが、次の点も異なります。

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

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

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

trap 'exit 130' INT

スクリプトの先頭で、SIGINTの受信時にbashを強制的に終了します(どの場合でも、SIGINTは同期的に処理されません。現在実行中のコマンドが終了した後にのみ実行されます)。

理想的には、SIGINTが原因で死んだことを親に報告したいとします(たとえば、別のbashスクリプトの場合、そのbashスクリプトも中断されます)。 exit 130を実行することは、SIGINTを終了することと同じではありません(ただし、シェルによっては$?が両方のケースで同じ値に設定されます)。最も多い2)。

ただし、bashksh93、またはFreeBSD shの場合は機能しません。その130の終了ステータスはSIGINTによる終了とは見なされず、親スクリプトはそこで中止しません

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

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

説明は、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で実装されていますが、他のほとんどのシェルでは実装されていません。

13
jilles

ご想像のとおり、これはSIGINTが下位プロセスに送信され、シェルがそのプロセスの終了後も継続していることが原因です。

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

5
Tom Hunt

端末はcontrol-cに気付き、INTが新しいフォアグラウンドプロセスグループを作成していないため、ping信号をフォアグラウンドプロセスグループに送信します。ここにはシェルが含まれます。これは、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

4
thrig

さて、sleep 1をbashスクリプトに追加してください。
これで、2つで停止できます Ctrl+C

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

sleep呼び出しのないスクリプトで、 Ctrl+C 本当に高速で何度も動作しないようで、ループを終了することはできません。私の推測では、2回押すのは、現在実行中のプロセスの中断と次のプロセスの開始との間の適切な瞬間にそれを行うには十分速くないと思います。毎 Ctrl+C を押すと、SIGINTがループ内で実行されるプロセスに送信されますが、サブシェルには送信されません。

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

2
nephewtom

誰かがこのbash機能の修正に興味があり、 その背後にある哲学 にそれほど関心がない場合は、以下が提案です。

問題のあるコマンドを直接実行しないでください。ただし、a)終了するのを待つラッパーからb)シグナルを乱さず、c)しますnotWCEメカニズム自体を実装しますが、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
1
mosvy
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は、実行中のfirefoxのPIDをgrepし、このPIDをany_file_nameというファイルに保存します。 「sed」コマンドは、「any_file_name」ファイルのPID番号の先頭にkillを追加します。 3行目はany_file_nameファイルを実行可能にします。ここで4行目は、any_file_nameファイルで使用可能なPIDを強制終了します。上記の4行をファイルに書き込み、そのファイルを実行すると、 Control-C。私にとってはまったく問題ありません。

0
user2176228

これを試して:

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