LoginSignup
25
18

More than 5 years have passed since last update.

Bash で並列に処理を実行して Ctrl-C でまとめて終了する

Posted at

例えば 2 並列で無限ループでなにか処理させて、Ctrl+C で止められるようにする。
しかもシェルスクリプトではなく、コンソールにペタッと貼り付けて実行させたい。

ダメなケース

for no in {1..2}; do
  (
    while sleep 1; do
      printf "%s: my name is %d\n" "$(date +%H:%M:%S)" "$no"
    done
  ) &
done
wait

このようなプロセスツリーになります。

bash,6979,6979
  ├─bash,7840,7840
  │   └─sleep,7848,7840 1
  └─bash,7841,7841
      └─sleep,7850,7841 1

フォアグラウンドなプロセスはシェルである bash(6979)wait なので、Ctrl+C しても子の Bash には SIGINT が飛ばないので死なない。

ダメなケース

サブシェルで囲んで wait がサブエシェルから実行されるようにします。

(
  for no in {1..2}; do
    (
      while sleep 1; do
        printf "%s: my name is %d\n" "$(date +%H:%M:%S)" "$no"
      done
    ) &
  done  
  wait
)

シェルから直接起動しているのは一番上のサブシェルなのでそれがプロセスグループリーダーになります。

bash,6979,6979
  └─bash,7963,7963
      ├─bash,7964,7963
      │   └─sleep,7969,7963 1
      └─bash,7965,7963
          └─sleep,7971,7963 1

サブシェルの wait がフォアグラウンドなので Ctrl+C でプロセスグループ 7963SIGINT が送られて全滅するかと思ったんですがしませんでした。Bash が SIGINT を無視するからですね。次のようにただの ping とかなら止まります。

(
  for no in {1..2}; do
    ping "192.168.2.$no" &
  done  
  wait
)

ダメなケース

バックグラウンドプロセスの PID を保存して trapkill する。

(
  pids=()
  trap 'kill ${pids[@]}' EXIT
  for no in {1..2}; do
    (
      while sleep 1; do
        printf "%s: my name is %d\n" "$(date +%H:%M:%S)" "$no"
      done
    ) &
    pids+=($!)
  done  
  wait
)

バックグラウンドプロセスの Bash だけ死んで、その Bash から起動している何かは死なずに孤児る。上の例だと sleep は死なない。

ダメなケース

バックグラウンドプロセスはサブシェルと同じプロセスグループなので、プロセスグループを指定して全滅させる。

(
  trap "kill -- -$$" EXIT
  for no in {1..2}; do
    (
      while sleep 1; do
        printf "%s: my name is %d\n" "$(date +%H:%M:%S)" "$no"
      done
    ) &
  done  
  wait
)

$$ のようなシェル変数はサブシェルでは初期化されない。この例では $$ は親のシェルの PID なるため、うまくいかない。

下記のようにすればよく分かる。

echo "$$:$PPID"     # 6979:6978
( echo "$$:$PPID" ) # 6979:6978 ... サブシェルではなく親の PID となている

成功なケース

$BASHPID ならサブシェルの PID が得られるのでそれを使う。

echo "$$:$PPID:$BASHPID"     # 6979:6978:6979
( echo "$$:$PPID:$BASHPID" ) # 6979:6978:8219
(
  trap "kill -- -$BASHPID" EXIT
  for no in {1..2}; do
    (
      while sleep 1; do
        printf "%s: my name is %d\n" "$(date +%H:%M:%S)" "$no"
      done
    ) &
  done  
  wait
)

成功なケース

$$ とか $BASHPID とか使わなくても kill 0 で現在のプロセスグループにシグナルを送ることができる。

(
  trap "kill 0" EXIT
  for no in {1..2}; do
    (
      while sleep 1; do
        printf "%s: my name is %d\n" "$(date +%H:%M:%S)" "$no"
      done
    ) &
  done  
  wait
)

これが一番簡単だと思います。

25
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
18