シェルスクリプトで wait で待たずに終了ステータスを得るには

結論 : ps コマンドでジョブの実行状態をチェックして、終了していた場合にのみ wait で終了ステータスを拾えば良い。

bashの組込コマンド wait は、指定のプロセスが終了していなければ終了するまで待機するので、使い勝手が悪いことがある。
例えば次のケースを考えてみる。

ジョブネットをシェル ( bash ) で作ろうとしています。 ジョブが A から E まであります。 D は A, B に依存しています。 すなわち A, B の終了後に実行可能です。 E は B, C に依存しています。
全てのジョブが無駄な待機をすることなく速やかに実行されるようなシェルスクリプトはどのようになるでしょうか?

失敗例

まず、 A, B, C をバックグラウンドで実行する。 次に D を実行するために A, B の終了をチェックする。 このとき wait でチェックすると、 A, B の終了まで中断することになる。仮に A が終了しておらず、 B, C が終了している場合、 E の実行条件が満たされているにも関わらず wait で中断されるため E の実行も待たされてしまう。

失敗例のサンプルコード ( sample1.sh )。 ジョブ A は sleep 10, B 〜 D は sleep 1, E は sleep 5 とした。 また、各ジョブが異常終了した場合の処理は、コードが長くなるため端折った。

 1#!/bin/bash
 2
 3# 概要 : ジョブ(ここでは sleep )を実行する
 4# 引数 : ジョブ名とスリープ秒数
 5job(){
 6   echo "$1 | start |  $(date '+%H:%M:%S')"
 7   sleep $2; sts=$?
 8   echo "$1 |   end |  $(date '+%H:%M:%S')"
 9   return $sts
10}
11
12job A 10 & Apid=$! # A を実行  
13job B 1 & Bpid=$!  # B を実行
14job C 1 & Cpid=$!  # C を実行
15
16wait $Apid; Asts=$?  # A の終了ステータスを取得
17wait $Bpid; Bsts=$?  # B の終了ステータスを取得
18wait $Cpid; Csts=$?  # C の終了ステータスを取得
19
20if [ $Asts -eq 0 ] && [ $Bsts -eq 0 ];then
21    # A, B が正常終了の場合
22    job D 1 & Dpid=$! # D を実行
23fi
24
25if [ $Bsts -eq 0 ] && [ $Csts -eq 0 ];then
26    # B, C が正常終了の場合
27    job E 5 & Epid=$! # E を実行
28fi
29
30wait $Dpid $Epid  # D と E を待機

実行結果。 E の実行は、E の依存ジョブ B, C が終わってから 9 秒間待たされている。 全体の処理時間は 15 秒。

 1$ ./sample1.sh
 2A | start |  22:44:03
 3B | start |  22:44:03
 4C | start |  22:44:03
 5C |   end |  22:44:04
 6B |   end |  22:44:04
 7A |   end |  22:44:13
 8D | start |  22:44:13
 9E | start |  22:44:13
10D |   end |  22:44:14
11E |   end |  22:44:18

対応策の検討と成功例

単にジョブの実行状態(実行中か終了したか)をチェックするだけであれば、 wait を使わずに、例えば ps コマンドで可能だ。 しかし、それだとジョブの終了ステータス(正常終了したか異常終了したか)を得ることはできない。

c 言語で waitpid システムコールを使って独自の wait コマンドを作れば良いかとも思ったが、外部から指定されたプロセスIDはその自作プログラムの子プロセスではないので、これも終了ステータスを得ることはできない。

実は上記のような問題を解決する方法、つまり、ジョブの実行状態チェックを待たされることなく行い、かつ、ジョブの終了ステータスを拾う方法は、単純なものであった。 ps と wait を組み合わせるだけだ。 ps コマンドでジョブの実行状態をチェックして、終了していた場合にのみ wait で終了ステータスを拾えば良いだけだ。 wait 実行時には指定のジョブは終了しているので、待機することはない。 wait の待機機能が邪魔なので、それを潰してしまうわけだ。

成功例のサンプルコード ( sample2.sh )。 18 〜 23 行目が要となる箇所だ。

 1#!/bin/bash
 2
 3# 概要 : ジョブ(ここでは sleep )を実行する
 4# 引数 : ジョブ名とスリープ秒数
 5job(){
 6   echo "$1 | start |  $(date '+%H:%M:%S')"
 7   sleep $2; sts=$?
 8   echo "$1 |   end |  $(date '+%H:%M:%S')"
 9   return $sts
10}
11
12# 概要 : 指定プロセスの終了ステータスを返す
13#        未実行または実行中の場合、100を返す
14# 引数 : プロセスID
15getexitcode(){
16    if [ -n "$1" ];then
17        # 実行済みの場合
18        ps -p $1 >& /dev/null
19        if [ $? -ne 0 ];then
20            # プロセスが終了していた場合
21            wait $1   # 終了ステータスを取得
22            return $? # 終了ステータスをリターン
23        fi
24    fi
25    # 未実行または実行中の場合
26    return 100
27}
28
29job A 10 & Apid=$! # A を実行
30job B 1 & Bpid=$!  # B を実行
31job C 1 & Cpid=$!  # C を実行
32
33while :;do
34    getexitcode $Apid; Asts=$?  # A の終了ステータスを取得
35    getexitcode $Bpid; Bsts=$?  # B の終了ステータスを取得
36    getexitcode $Cpid; Csts=$?  # C の終了ステータスを取得
37    getexitcode $Dpid; Dsts=$?  # D の終了ステータスを取得
38    getexitcode $Epid; Ests=$?  # E の終了ステータスを取得
39
40    if [ $Asts -eq 0 ] && [ $Bsts -eq 0 ] && [ -z "$Dpid" ];then
41        # A, B が正常終了、かつ、D が未実行の場合
42        job D 1 & Dpid=$!  # D を実行
43    fi
44    if [ $Bsts -eq 0 ] && [ $Csts -eq 0 ] && [ -z "$Epid" ];then
45        # B, C が正常終了、かつ、E が未実行の場合
46        job E 5 & Epid=$!  # E を実行
47    fi
48
49    # D, E が正常終了したら、ループ脱出
50    if [ $Dsts -eq 0 ] && [ $Ests -eq 0 ];then break;fi
51
52    sleep 0.2
53done

実行結果。 E の実行は、 E の依存ジョブ B, C が終わった直後に開始されている。 全体の処理時間は 11 秒で、失敗例より短くなっている。

 1$ ./sample2.sh
 2A | start |  22:45:53
 3B | start |  22:45:53
 4C | start |  22:45:53
 5C |   end |  22:45:54
 6B |   end |  22:45:54
 7E | start |  22:45:54
 8E |   end |  22:45:59
 9A |   end |  22:46:03
10D | start |  22:46:03
11D |   end |  22:46:04

ちょっとしたジョブネットを作るときに役立つはず。

関連ページ