bashでのPID取得方法まとめ($$、$PPID、$!、$BASHPID)

More than 1 year has passed since last update.

いつも忘れるのでPIDの取得方法についてまとめてみた。


bashにおけるPID用変数一覧

bashでは、PIDを取得するための変数があらかじめ用意されている。

シェル変数と特殊パラメータに分かれているが、取得できる対象はすべての変数で異なってくる。


PID取得用の特殊パラメータ

特殊パラメータ
説明

$
シェルのプロセスIDに展開されます。()を使ったサブシェルの内部では、$ はサブシェルではなく、現在のシェルのプロセス ID に展開されます。

!
最後に実行されたバックグラウンド (非同期) コマンドの プロセス ID に展開されます。


PID取得用のシェル変数

シェル変数
説明

BASHPID
現在の bash のプロセス ID に展開されます。 bash を再初期化しないサブシェルのような、いくつかの環境においては、 $$ と値が異なります。

PPID
そのシェルの親のプロセス ID。この変数は読み込み専用です。


PID用変数の詳細

それぞれのPID用変数について、どんなケースに利用できるか見ていく。


シェル自身のプロセスID($$)

まずは特殊パラメータ$から見ていこう。

これは最も簡単だ。

例えば、実行中のプロンプトで以下を実行すると、プロンプトを実行しているbashのPIDが表示される。

$$を表示してみるついでに、psコマンドでも確認してみよう。

表示したPIDがbashであることがわかる。

$ echo $$ # シェル自身のPIDが表示される。

5887
$ ps -f | egrep "$$|PID"
UID PID PPID C STIME TTY TIME CMD
user 5887 5884 0 21:37 pts/1 00:00:00 -bash
user 22781 5887 0 22:58 pts/1 00:00:00 ps -f

次に、同様のコマンドをシェルスクリプトで実行してみよう。

今度取得しているPIDはprint_pid_sample1.shを実行しているbashになったことがわかるだろう。

これが「シェル自身のPID」である。


print_pid_sample1.sh

echo $$

ps -f | egrep "$$|PID"

$ bash print_pid_sample1.sh

23193
UID PID PPID C STIME TTY TIME CMD
user 23193 5887 0 23:03 pts/1 00:00:00 bash print_pid_sample1.sh
user 23194 23193 0 23:03 pts/1 00:00:00 ps -f


シェルの親のプロセス ID($PPID)

次は、自身のプロセスIDと関連性の高い親のPIDを取得してみよう。

実は先ほどから実行しているpsコマンドの結果にすでにPPIDが表示されていた。

今まで見ていたpid「5887」の親「5884」だろう。

さらに、「5884」をpsで見てみるとsshdになっている。

これで、現在のプロンプトがsshdの上で動いているbashであることがわかる。

$ echo $PPID

5884
$ ps -ef | egrep "$PPID|PPID"
UID PID PPID C STIME TTY TIME CMD
user 5884 5880 0 21:37 ? 00:00:00 sshd: XXX
user 5887 5884 0 21:37 pts/1 00:00:00 -bash

ついでにシェルスクリプトでも実行してみよう。

今度は、「5887」からprint_pid_sample2.shを実行したため、「5887」がPPIDとして表示されることになる。


print_pid_sample2.sh

echo $PPID

ps -ef | egrep "$PPID|PPID"

$ bash print_pid_sample2.sh

5887
UID PID PPID C STIME TTY TIME CMD
user 5887 5884 0 21:37 pts/1 00:00:00 -bash
user 23673 5887 0 23:18 pts/1 00:00:00 bash print_pid_sample2.sh


バックグラウンドコマンドのプロセスID($!)

次は、バックグラウンドコマンドのプロセスIDを取得してみる。

とりあえず、echoしてみよう。

$ echo $!

なにも表示されませんでした。

$!については、バックグラウンド実行したコマンドがない場合、空で返されます。

というわけで、ちゃんと表示されるようにしましょう。

$ : &

$ echo $!
24511
$ echo $!
24511
$ : & echo $!
24550

$!は最後に実行したバックグラウンドコマンドのPIDを保存し続けるため、フォアグラウンドコマンドを何度実行しても値は変わりません。

ちなみに、「:」はシェルの組み込みコマンドで「何もしないコマンド」です。


: [arguments]

何もしません。このコマンドは arguments を展開し、指定されたリダイレクトを実行する以外には何も行いません。 終了コード 0 を返します。



現在の bash のプロセス ID($BASHPID)

さぁ、最後に一番めんどくさい$BASHPIDについてみていく。

まずは、普通に使っていれば、$$と変わらない。

通常のbashでたたけば同じ値が返ってくる。

echo [BASHPID]=$BASHPID,  [PID]=$$

[BASHPID]=28591, [PID]=28591


print_pid_sample3.sh

echo [BASHPID]=$BASHPID,  [PID]=$$


bash print_pid_sample3.sh

[BASHPID]=29142, [PID]=29142

では、どういう場合に値が変わってくるかだ。

サブシェルが動いたときにBASHPIDはサブシェル自体のPIDを返す。

サブシェルの簡単な実行方法は(list)あたりだろう。

以下を比較してみる。


print_pid_sample4.sh

echo "# 通常のシェル実行"

echo [PID]=$$
ps -ef | egrep "$$|PID"
echo [BASHPID]=$BASHPID
pid=$BASHPID
ps -ef | egrep "$pid|PID"

echo "# サブシェル実行"
(
echo [PID]=$$
ps -ef | egrep "$$|PID"
echo [BASHPID]=$BASHPID
pid=$BASHPID
ps -ef | egrep "$pid|PID"
)


bash print_pid_sample4.sh# 通常のシェル実行

[PID]=2913
UID PID PPID C STIME TTY TIME CMD
user 2913 28591 0 21:21 pts/0 00:00:00 bash print_pid_sample4.sh
user 2914 2913 0 21:21 pts/0 00:00:00 ps -ef
[BASHPID]=2913
UID PID PPID C STIME TTY TIME CMD
user 2913 28591 0 21:21 pts/0 00:00:00 bash print_pid_sample4.sh
user 2916 2913 0 21:21 pts/0 00:00:00 ps -ef
# サブシェル実行
[PID]=2913
UID PID PPID C STIME TTY TIME CMD
user 2913 28591 0 21:21 pts/0 00:00:00 bash print_pid_sample4.sh
user 2918 2913 0 21:21 pts/0 00:00:00 bash print_pid_sample4.sh
[BASHPID]=2918
UID PID PPID C STIME TTY TIME CMD
user 2918 2913 0 21:21 pts/0 00:00:00 bash print_pid_sample4.sh
user 2921 2918 0 21:21 pts/0 00:00:00 ps -ef

サブシェル側だけが変わったことがわかるだろう。

サブシェルが実行された場合は$$やPPIDでは取得することができないため、BASHPIDがかなり有効になる。


まとめ

さぁ、大体説明したところで今までのPIDをまとめてみてみよう。

先に説明したサブシェル実行の例もコマンド置換、パイプライン当たりを追加している。

また、bashで複数のコマンドを一つのコマンドとして扱う方法(複合コマンド)で取り上げた(list)と{ list; }の違いについても見て取れるだろう。


print_pid_sample_all.sh

# 現在のシェル環境実行

echo
echo
"### 現在のシェル環境実行"
echo "*** default ***"
echo [BASHPID]=$BASHPID, [PPID]=$PPID, [PID]=$$

# 別のシェル環境実行
echo
echo
"### 別のシェル環境実行"
bash print_pid_sample_sub.sh

# 複合コマンド実行
echo
echo
"### 複合コマンド実行"
echo "*** (list) ***" # サブシェルが動く
( echo [BASHPID]=$BASHPID, [PPID]=$PPID, [PID]=$$)
echo "*** { list; } ***" # サブシェルが動かない
{ echo [BASHPID]=$BASHPID, [PPID]=$PPID, [PID]=$$; }

# コマンド置換(サブシェルが動く)
echo
echo
"### コマンド置換"
echo "*** \$(command) ***"
echo "$( echo [BASHPID]=$BASHPID, [PPID]=$PPID, [PID]=$$)"
echo "*** \`command\` ***"
echo "` echo [BASHPID]=$BASHPID, [PPID]=$PPID, [PID]=$$`"

# パイプライン(サブシェルが動く)
echo
echo
"### パイプライン"
echo "*** pipeline1 ***"
echo "" | { echo [BASHPID]=$BASHPID, [PPID]=$PPID, [PID]=$$; }
echo "*** pipeline2 ***"
{ echo [BASHPID]=$BASHPID, [PPID]=$PPID, [PID]=$$; } | cat



print_pid_sample_sub.sh

echo [BASHPID]=$BASHPID, [PPID]=$PPID, [PID]=$$


$ bash print_pid_sample_all.sh

### 現在のシェル環境実行
*** default ***
[BASHPID]=27949, [PPID]=5887, [PID]=27949

### 別のシェル環境実行
[BASHPID]=27950, [PPID]=27949, [PID]=27950

### 複合コマンド実行
*** (list) ***
[BASHPID]=27951, [PPID]=5887, [PID]=27949
*** { list; } ***
[BASHPID]=27949, [PPID]=5887, [PID]=27949

### コマンド置換
*** $(command) ***
[BASHPID]=27952, [PPID]=5887, [PID]=27949
*** `command` ***
[BASHPID]=27953, [PPID]=5887, [PID]=27949

### パイプライン
*** pipeline1 ***
[BASHPID]=27955, [PPID]=5887, [PID]=27949
*** pipeline2 ***
[BASHPID]=27956, [PPID]=5887, [PID]=27949


いろんなPIDを取得してみる。

bashにおけるサブシェルが大体説明できたところで、ちょっとした小技的なものを見ていこう。


直前に実行したコマンドのPIDを取得

直前に実行されたコマンドのPIDが欲しいことがたまにあるので、やり方を考えてみた。

条件としては以下だ。


  • 直前に実行したPIDを取得する。

  • フォアグラウンドで実行されるのが理想である、そのコマンドが終了するまで次へは行きたくない。

  • 実行時間の極端に短いコマンドでも使える方法が欲しい

というわけで、単純に以下だろう。

echo 1 &

pid=$!
wait $pid # 直前に実行したバックグラウンドコマンドが終了するまで待機
echo $pid

バックグラウンドで実行するが、取得したPIDとwaitコマンドを組み合わせることでフォアグラウンドでの実行と同様の効果が期待できる。

今のことろ、この実行方法で困るようなケースは思いつかない。


子プロセスをまとめて取得

シェルスクリプトでサブシェルのプロセスIDを取る方法

で説明してくれている方法だ。

シェルで複数のバックグラウンドコマンドを実行するとすべてのコマンドはシェルの子プロセスとなっている。

この場合、waitコマンドを引数なしで実行すればすべての子プロセスが終了するまで待ち状態になってくれる。


bg_sample.sh

sleep 1 &

sleep 10 & # これが終わるまでwaitで待機する。
sleep 1 &
wait

さぁ、ここで問題なのが、waitと強制終了した場合だ。

試しに実行直後にCtrl+cでコマンドを中断し、sleepプロセスを確認してみよう。

bash bg_sample.sh

^C
ps -ef | grep sleep
user 31522 1 0 20:38 pts/0 00:00:00 sleep 10

残っている。。。これは困るだろう。

Ctrl+cで止めたからには、バックグラウンド実行のプロセスも止まってくれるのが大体の期待だろう。

ということで、jobsとtrapコマンドを使って実現するわけだ。

jobs -pを実行すれば子プロセスのPID一覧が取得できる。

sleep 1 &

sleep 10 & # これが終わるまでwaitで待機する。
sleep 1 &
jobs -p
32186
32225
32264

trapは特定のシグナルが発生したことをキャッチしてその時の動作をしてすることができる。

# trapはあまり得意ではないので、そこまで踏み込まない。。。

Ctrl+cが来た時にシグナル名はSIGINTだ。

ただ、今回の場合何で落ちるか不特定なことも多いためEXITを指定してもいいだろう。

シェルスクリプトで trap を忘れちゃいませんか


EXIT: シェルスクリプト実行が終了したタイミングで実行される



trap_bg_kill_sample.sh

trap 'kill $(jobs -p)' EXIT

sleep 1 &
sleep 10 & # これが終わるまでwaitで待機する。
sleep 1 &
wait

これで、強制終了した場合にも子プロセスにkillをかけることができる。

タイミング的にプロセスの終了とkillが重なるとエラーが出たりするが、強制終了されるときにそれくらいはいいと思う。


BASHPIDを使わずにサブシェルのPIDを取得

次は、BASHPIDを使わずにサブシェルのPIDを取得してみる。

なぜそんなことを?

BASHPIDがbash4以降に導入された機能だからだ。

bash3までの環境に当たってしまった場合に困るだろう。

やり方は以下のサイトにあった。

シェルスクリプトでサブシェルのプロセスIDを取る方法

これだ。

get_pid () { $SHELL -c 'echo $PPID';}

説明が面倒なので、、使ってみよう。


print_pid_sample_all2.sh

get_pid () { $SHELL -c 'echo -n [get_pid]=$PPID," "'; }

# 現在のシェル環境実行
echo_
echo "### 現在のシェル環境実行"
echo "*** default ***"
get_pid; echo [BASHPID]=$BASHPID, [PPID]=$PPID, [PID]=$$

# 別のシェル環境実行
echo_
echo "### 別のシェル環境実行"
bash print_pid_sample_sub2.sh

# 複合コマンド実行
echo_
echo "### 複合コマンド実行"
echo "*** (list) ***"
(get_pid; echo [BASHPID]=$BASHPID, [PPID]=$PPID, [PID]=$$)
echo "*** { list; } ***"
{ get_pid; echo [BASHPID]=$BASHPID, [PPID]=$PPID, [PID]=$$; }

# コマンド置換
echo_
echo "### コマンド置換"
echo "*** \$(command) ***"
echo "$(get_pid; echo [BASHPID]=$BASHPID, [PPID]=$PPID, [PID]=$$)"
echo "*** \`command\` ***"
echo "`get_pid; echo [BASHPID]=$BASHPID, [PPID]=$PPID, [PID]=$$`"

# パイプライン
echo_
echo "### パイプライン"
echo "*** pipeline1 ***"
echo "" | { get_pid; echo [BASHPID]=$BASHPID, [PPID]=$PPID, [PID]=$$; }
echo "*** pipeline2 ***"
{ get_pid; echo [BASHPID]=$BASHPID, [PPID]=$PPID, [PID]=$$; } | cat



print_pid_sample_sub2.sh

get_pid () { $SHELL -c 'echo -n [get_pid]=$PPID," "'; }

get_pid; echo [BASHPID]=$BASHPID, [PPID]=$PPID, [PID]=$$


bash print_pid_sample_all2.sh

### 現在のシェル環境実行
*** default ***
[get_pid]=2367, [BASHPID]=2367, [PPID]=28591, [PID]=2367

### 別のシェル環境実行
[get_pid]=2369, [BASHPID]=2369, [PPID]=2367, [PID]=2369

### 複合コマンド実行
*** (list) ***
[get_pid]=2371, [BASHPID]=2371, [PPID]=28591, [PID]=2367
*** { list; } ***
[get_pid]=2367, [BASHPID]=2367, [PPID]=28591, [PID]=2367

### コマンド置換
*** $(command) ***
[get_pid]=2374, [BASHPID]=2374, [PPID]=28591, [PID]=2367
*** `command` ***
[get_pid]=2376, [BASHPID]=2376, [PPID]=28591, [PID]=2367

### パイプライン
*** pipeline1 ***
[get_pid]=2379, [BASHPID]=2379, [PPID]=28591, [PID]=2367
*** pipeline2 ***
[get_pid]=2381, [BASHPID]=2381, [PPID]=28591, [PID]=2367

お見事!BASHPIDと同一値になりました。


参考文献

シェルスクリプトでサブシェルのプロセスIDを取る方法

bashスクリプトで子プロセスを全部殺すイディオム

シェルスクリプトで trap を忘れちゃいませんか