web-dev-qa-db-ja.com

スクリプトの終了時にバックグラウンドプロセスを強制終了するための信頼できる手法は何ですか?

シェルスクリプトを使用して、システムイベントに反応し、ウィンドウマネージャーのステータス表示を更新します。たとえば、1つのスクリプトが複数のソースをリッスンすることで現在のwifiステータスを判別します。

  1. wpa_supplicantからのイベントの関連付け/関連付け解除
  2. iPからのアドレス変更(dhcpcdがいつアドレスを割り当てたかがわかります)
  3. タイマープロセス(信号強度は時々更新されます)

多重化を実現するために、最終的にバックグラウンドプロセスを生成します。

{ wpa_cli -p /var/run/wpa_supplicant -i wlan0 -a echo &
ip monitor address &
while sleep 30; do echo; done } |
while read line; do update_wifi_status; done &

つまり、イベントソースのいずれかが回線を出力するたびに、wifiステータスが更新されるように設定されています。スクリプトを終了させる別のイベントソースも監視しているため、パイプライン全体がバックグラウンド(最後の「&」)で実行されます。

wait_for_termination
kill $!

キルはバックグラウンドプロセスをクリーンアップすることになっていますが、この形式では完全には機能しません。 'wpa_cli'および 'ip'プロセスは、少なくとも常に存続し、次のイベントで終了することもありません(理論的には、SIGPIPEを取得する必要があります。読み取りプロセスもまだ存続している必要があります)。

問題は、生成されたすべてのバックグラウンドプロセスを確実に[そしてエレガントに!]クリーンアップする方法です。

7
sqweek

非常に簡単な解決策は、スクリプトの最後にこれを追加することです。

kill -- -$$

説明:

$$は、実行中のシェルのPIDを提供します。したがって、kill $$はSIGTERMをシェルプロセスに送信します。ただし、negate PIDの場合、killはSIGTERMをプロセスグループ内のすべてのプロセスに送信します。事前に--が必要なので、killは、-$$がプロセスグループIDであり、フラグではないことを認識します。

これは、実行中のシェルがプロセスグループリーダーであることに依存していることに注意してください!そうでない場合、$$(PID)はプロセスグループIDと一致せず、最終的には知っている人にシグナルを送信します。ここで(グループリーダーでない場合、IDが一致するプロセスグループが存在する可能性は低いため、おそらくどこにもありません)。

シェルが起動すると、新しいプロセスグループ[1]が作成されます。フォークされたすべてのプロセスは、syscall(setpgid)を介してプロセスグループを明示的に変更しない限り、そのプロセスグループのメンバーになります。

特定のスクリプトがプロセスグループリーダーとして実行されることを保証する最も簡単な方法は、setsidを使用してスクリプトを起動することです。たとえば、親スクリプトから起動するこれらのステータススクリプトがいくつかあります。

#!/bin/sh
wifi_status &
bat_status &

このように記述されているため、wifiスクリプトとbatteryスクリプトはどちらも、親スクリプトと同じプロセスグループで実行され、kill -- -$$は機能しません。修正は次のとおりです。

#!/bin/sh
setsid wifi_status &
setsid bat_status &

pstree -p -gは、プロセスとプロセスグループIDを視覚化するのに役立ちます。

貢献してくれて、もう一度掘り下げてくれたすべての人のおかげで、私は何かを学びました! :)

[1]シェルがプロセスグループを作成する他の状況はありますか?例えば。サブシェルの開始時に?知りません...

9
sqweek

OK、cgroupsを使用しないかなりまともなソリューションを考え出しました。 Leonardo Dagninoが指摘したように、フォークプロセスに直面しても機能しません。

$を介してプロセスIDを手動で追跡する際の問題の1つ!後でそれらを強制終了することは、固有の競合状態です。プロセスを強制終了する前にプロセスが終了すると、スクリプトは存在しない、またはおそらく正しくないプロセスにシグナルを送信します。

wait組み込みを介してシェル内のプロセス終了を確認できますが、allのいずれかの終了を待つことしかできませんバックグラウンドプロセス、または単一のpid。どちらの場合もwaitブロックであるため、特定のPIDがまだ実行されているかどうかを確認するタスクには適していません。

上記の解決策を探しているときに、jobsコマンドに出くわしました。これは、以前はインタラクティブシェルでしか利用できないと思っていました。スクリプトで正常に動作し、起動したバックグラウンドプロセスを自動的に追跡します。プロセスが終了すると、ジョブリストに表示されなくなります。

したがって、コマンド:

trap 'kill $(jobs -p)' EXIT

単純なケースでは、現在のシェルが終了したときにバックグラウンドプロセスを確実に終了させるのに十分です。

私の場合、サブシェルからもバックグラウンドプロセスを起動しているため、1つでは不十分であり、新しいサブシェルごとにトラップがクリアされます。したがって、サブシェル内で同じトラップを実行する必要があります。

{ trap 'kill $(jobs -p)' EXIT
wpa_cli -p /var/run/wpa_supplicant -i wlan0 -a echo &
ip monitor address &
while echo; do sleep 30; done } |
while read line; do update_wifi_status; done &

最後に、jobs -pは、パイプラインの最後のプロセスのpidのみを提供します($!と同様)。ご覧のとおり、バックグラウンドパイプラインのfirstプロセスでバックグラウンドプロセスを生成しているので、そのpidにもシグナルを送信したいと思います。

最初のプロセスのpidはjobsから取得できますが、これをどの程度移植可能に実現できるかはわかりません。 bashを使用すると、次のスタイルで出力が得られます。

$ sleep 20 | sleep 20 &
$ jobs -l
[1]+ 25180 Running                 sleep 20
     25181                       | sleep 20 &

したがって、親スクリプトからわずかに変更されたkillコマンドを使用することで、パイプライン内のすべてのプロセスにシグナルを送ることができます。

wait_for_termination
kill $(jobs -l |awk '$2 == "|" {print $1; next} {print $2}')
2
sqweek

次のように入力することで、それぞれのPIDを取得できます。

PID1=$!

wpa_cliの後、

PID2=$!

iPの後、スクリプトの最後で両方を強制終了します。

kill $PID1
kill $PID2

ただし、プロセスがフォークした場合、それは機能しません。その場合、cgroupsが最良の解決策になります。

0