rcmdnk's blog

Million Dollar Bash: Bob Dylan, the Band, and the Basement Tapes

パイプとか$()とかで生成するサブシェルのIDを取得する方法について。

PID, PPID

通常、スクリプトでもコマンドラインでも、自分のプロセスIDは$$に入っています。 (以下、特記がないものはMac OS X 10.9でBash 3.2.51でやっています。)

また、$PPIDには自分の親プロセスのIDが入っています。

なので、

ppid.sh
1
2
#!/usr/bin/env bash
echo PPID=$PPID, PID=$$

こんなスクリプトを作って実行すると

$ echo PPID=$PPID, PID=$$
PPID=26005, PID=26051
$ ./ppid.sh
PPID=26051, PID=34768

こんな感じで、スクリプト実行でサブシェルが立ち上がるので、 スクリプト内においては親プロセスがコマンドラインでの プロセスIDなってることが分かります。

関数の場合

上のスクリプトを関数にして

$ get_ids () { echo PPID=$PPID, PID=$$; }

こんな感じで定義すると、

$ echo PPID=$PPID, PID=$$
PPID=26005, PID=26051
$ get_ids
PPID=26005, PID=26051

と今度は同じ値を返します。関数を呼ぶ場合は 関数の中身も同じプロセスとして実行されてる、ということ。

ここで、上のコマンドをパイプでも$()でもいいので サブシェルとして実行してみてもスクリプトを実行した時と同じく 新たなPIDが割り振られてる気がするんですが

$ echo test| get_ids
PPID=26005, PID=26051
$ echo "$(get_ids)"
PPID=26005, PID=26051

となり、上と同じ値を示します。

実際に同じかどうか、psで見てみます。

$ show_ps () { ps -a -o ppid,pid,pgid,tty,comm; echo;}
$ show_ps
 PPID   PID  PGID TTY      COMM
...
26005 26051 26051 ttys003  /bin/bash
26051 78054 78054 ttys003  ps
...
$ echo "$(show_ps)"
 PPID   PID  PGID TTY      COMM
...
26005 26051 26051 ttys003  /bin/bash
26051 79211 26051 ttys003  /bin/bash
79211 79212 26051 ttys003  ps
...

こんな感じで$()で実行した時は1つbashのプロセスが生まれて、その下で psが実行されています。(以下psの結果はここで試してた時に使っていた 関係あるttys003以外のttyのプロセスは削除してかいてあります)

パイプや$()として起動されたプロセスはBashとしての初期化が行われないので、 PID等のシェル変数が再定義されず親の物をそのまま引き継いでしまう様です。

サブシェルとして実行された関数内でのほんとのPIDを取得する方法

直接$$の変数が使えないわけですが、 この関数の中でシェルを1つ立ち上げてその親を見てあげると 結果的に関数自体のPIDを得ることが出来ます。

$ get_pid () { $SHELL -c 'echo $PPID';}
$ get_pid
26051
$ echo "$(show_ps;echo pid in func;get_pid;)"
 PPID   PID  PGID TTY      COMM
26005 26051 26051 ttys003  /bin/bash
26051 96877 26051 ttys003  /bin/bash
96877 96878 26051 ttys003  ps

pid in func
96877

こんな感じで最後に96877と、確かに2番めのbashのPIDが取得出来ています。

ただし、注意として、PIDを得たいときに、

$ echo "$( pid=$(get_pid);echo PID=$pid)"

などとしてしまうとpidはさらに下のプロセスに行ってしまうので 上手く行きません。

ので、使いたい時はファイルに一時書き出しして、みたいにするしかないでしょうか? (結局うまい方法が思い浮かばなかった。。。)

PIDが初期化されてるプロセスでの$SHELL...について

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

という関数なのですが、これを関数として定義すると、 コマンドラインから実行してもサブシェル内で実行しても 1つ下でシェルを起動してその中下で親プロセスを見るので 上に書いた様に新しく起動したプロセスIDになります。

ただ、これを関数でなく、直接コマンドを打つと

$ echo $$
26051
$ $SHELL -c 'echo $PPID'
26051
$ echo "$($SHELL -c 'echo $PPID')"
26051

と上と違ってサブシェルが立ち上がって無い様に見えます。 これを

$ ($SHELL -c 'echo $PPID')
26051

の様に()だけにして実行してみても一緒。

psも単独でコマンドだけで見てみると

$ echo "$(ps -a -o ppid,pid,pgid,tty,comm)"
 PPID   PID  PGID TTY      COMM
26051 22526 26051 ttys003  ps
26005 26051 26051 ttys003  /bin/bash

となって、実際psコマンドが現在のコマンドラインプロセスの直下に動いています。 ここに、なんでもいいんですが前後どちらかにコマンドを加えると

$ echo "$(ps -a -o ppid,pid,pgid,tty,comm;echo)"
 PPID   PID  PGID TTY      COMM
26051 24146 26051 ttys003  /bin/bash
24146 24147 26051 ttys003  ps
26005 26051 26051 ttys003  /bin/bash

というふうに、psの時点でサブシェルが出来ててpsコマンドもその下に出来ます。

他にも

$ echo "$(sleep 10)"

として他から覗いて見ると、bashのプロセスはこのttyで一つだけで

$ echo "$(sleep 10;echo)"

とするともうひとつbashのプロセスが出てきてその下にsleepが動いていました。

一方、サブシェル下で同じようなことをしてみると

$ echo "$(echo "$(ps -a -o ppid,pid,pgid,tty,comm)")"
 PPID   PID  PGID TTY      COMM
26005 26051 26051 ttys003  /bin/bash
26051 28170 26051 ttys003  /bin/bash
28170 28171 26051 ttys003  /bin/bash
28171 28172 26051 ttys003  ps

$ echo "$(echo "$(ps -a -o ppid,pid,pgid,tty,comm;echo)")"
 PPID   PID  PGID TTY      COMM
26005 26051 26051 ttys003  /bin/bash
26051 31325 26051 ttys003  /bin/bash
31325 31326 26051 ttys003  /bin/bash
31326 31327 26051 ttys003  ps

と言った感じで単独コマンドでも最初からサブシェル下にさらにサブシェルを作ってそこの下で 実行していることが分かります。

どうも、関数にしたり、複数コマンドが呼ばれる場合には$() 1 の中にサブシェルがきちんと作られますが、 中が単独コマンドだとサブシェルを立ち上げずに現在のプロセスの直下で コマンドを実行する様です。

この仕様についてman bashを読んでみてもいまいち分からなかったんですが、 ちょっと気持ち悪い。。。 (きっと何か意味があると思うのですが。。。余計なプロセスを作りたくないだけ?)。

ちなみに、下に挙げてるBash Version 4やZsh (5.0.2)でも同じような 結果になりました。

Bash Version 4の場合

実はBash 4以降では$BASHPIDと言うシェル変数が加えられていて、 この変数はサブシェルにおいてもきちんと初期化?されてそのPIDを正しく示します。

ので、Bash 4以降では上みたいなget_pidなんかせずにecho $BASHPIDだけでOK。

$BASHPID$$の値は上の様なサブシェルの場合には違うので ちょっと注意が必要です。

このBASHPIDも上の場合に当てはめるとまたちょっと特殊?で、

$ echo "$(echo $BASHPID)"

とするとコマンドラインでのBASHPID(=$$)と違う値を示すので、 この場合は何故か単独コマンドでもいきなりサブシェルを立ち上げてる様子。 さらに不可解。。。(BASHPIDという変数その物が何かトリガーかけてる?)

Zshの場合

Zshの場合、差し当たり探してみた感じではBASHPIDの様な変数はありません。 上のget_pidみたいな関数はBash同様に使えるので、 Bash 3.Xと同じ様なことは出来ます。

まとめ

ちょっと関数の中がどこで動いてるのか調べようと思ったら 結構面倒なことになってしまったんですが、 単純にデバッグ用とかで表示させるだけならget_pidを使って簡単に出来そうです。

ただし、ここで関数にしてるのが重要で、関数にしないと サブシェルみたく呼んだ時と通常の個所で呼んだ場合で結果が変わってしまいます。

更にこの値を直接使おうと思うと結構面倒で、下手に渡そうとすると さらにサブシェルを立ち上げてしまうので、出力を一時ファイルとかに 出して読み込み直すのが一番手っ取り早いと思うのですが、 もっとスマートな方法がありそうなものですが。

まあ、Bash 4.x系ではBASHPIDという便利な変数があるので それ使えばいいじゃん、という話にもなったりするわけですが。

いずれにしろ、何か色々な不思議な仕様が見えた 2話でした。

Sponsored Links
  1. パイプの場合も試してみましたが$()と同様の結果でした

  2. か単に自分が何か勘違いしてる。。。

Sponsored Links

« シェルスクリプトで無理やり端末に表示させる方法 LogMeInのフリーアカウントが終了なのでTeamViewerに乗り換える »

}