2006年09月12日

bash で,サブシェルが起動される条件

はてなブックマークに登録

今回は少々マニアックというか,重箱の隅的お話です. bash(1) には,複数のコマンドをまとめたり,コマンドの実行結果をコマンドラインに取り込むための記法が複数あります.それらのコマンドを実行するために,bash は必要に応じてサブシェルを起動しますが,どういう記述をした際にサブシェルが起動されるのか,いまいちはっきりしなかったため,実際に試してみました.今回試したのは,( ), $( ), { }, <( ) です.
さて,今回サブシェルが立ち上がるか否かは,ps --forest を実行して,ps コマンドの親プロセスがどれになっているかで確認しています.bash が設定する $PPID 変数を見ないのは,変数の展開をどのシェルがするかに依存するために,確認しにくいからです.ps コマンドを --forest オプション付きで単純に起動すると
$ ps --forest
  PID TTY          TIME CMD
20218 pts/18   00:00:00 bash
27975 pts/18   00:00:00  \_ ps
となります.一番上の PID = 20218 の bash がターミナル上で動いている bash になります.サブシェルが起動される場合は,この bash と ps の間に,もう一つ bash が動くはずです.
  • ( )
単純に,ps コマンドだけを指定すると
$ (ps --forest)
  PID TTY          TIME CMD
20218 pts/18   00:00:00 bash
27976 pts/18   00:00:00  \_ ps
となって,サブシェルは起動されません.しかしコマンドをもう一つ追加してやると
$ (ps --forest; echo -n)
  PID TTY          TIME CMD
20218 pts/18   00:00:00 bash
27977 pts/18   00:00:00  \_ bash
27978 pts/18   00:00:00      \_ ps
となって,サブシェルが起動されます.次の二つのパターンは,{ } のケースとの比較用です.( ) ではパイプ接続されても結果に影響はないようです.
$ (ps --forest) | cat
  PID TTY          TIME CMD
20218 pts/18   00:00:00 bash
27979 pts/18   00:00:00  \_ ps
27980 pts/18   00:00:00  \_ cat

$ echo | (ps --forest)
  PID TTY          TIME CMD
20218 pts/18   00:00:00 bash
27982 pts/18   00:00:00  \_ ps
  • $( )
$( ) は,指定されたコマンドの実行結果をコマンドライン上に差し込むためのものです ` ` と機能的には同じです.出力結果を表示するために echo を使っています.$( ) 全体を " でくくっているのは,改行が保存されるようにするためです.結果は次のようになりました.パターン的には ( ) と同じです.
$ echo "$(ps --forest)"
  PID TTY          TIME CMD
20218 pts/18   00:00:00 bash
27983 pts/18   00:00:00  \_ ps

$ echo "$(ps --forest; echo -n)"
  PID TTY          TIME CMD
20218 pts/18   00:00:00 bash
27984 pts/18   00:00:00  \_ bash
27985 pts/18   00:00:00      \_ ps
  • { }
{ } はブロックを作るための構文です.コマンドが一つの場合は
$ { ps --forest; }
  PID TTY          TIME CMD
20218 pts/18   00:00:00 bash
27986 pts/18   00:00:00  \_ ps
となります.コマンドを二つにした場合
$ { ps --forest; echo -n; }
  PID TTY          TIME CMD
20218 pts/18   00:00:00 bash
27987 pts/18   00:00:00  \_ ps
となって,前の ( )$( ) と違いサブシェルは起動されません.では,ブロックの前後にパイプを挟んでやるとどうなるでしょうか.
$ { ps --forest; } | cat
  PID TTY          TIME CMD
20218 pts/18   00:00:00 bash
27988 pts/18   00:00:00  \_ bash
27990 pts/18   00:00:00  |   \_ ps
27989 pts/18   00:00:00  \_ cat

$ echo | { ps --forest; }
  PID TTY          TIME CMD
20218 pts/18   00:00:00 bash
27992 pts/18   00:00:00  \_ bash
27993 pts/18   00:00:00      \_ ps
のように,やっぱり ( ) や $( ) の場合と違い,今度はコマンドが一つだけでもサブシェルが起動されるようになってしまいました.この違いは,当然ながらシェル変数の変更を伴う場合に影響を及ぼします.つまり,このようなことが起こります.
$ a=aaa
$ { a=bbb; }
$ echo $a
bbb

$ echo | { a=ccc; }
$ echo $a
bbb
ちなみに,この関係は whilefor などの制御文のブロックでも同様に発生します.
$ check=true; while $check; do ps --forest; check=false; done
  PID TTY          TIME CMD
29996 pts/9    00:00:00 bash
30458 pts/9    00:00:00  \_ ps

$ check=true; while $check; do ps --forest; check=false; done | cat
  PID TTY          TIME CMD
29996 pts/9    00:00:00 bash
30459 pts/9    00:00:00  \_ bash
30461 pts/9    00:00:00  |   \_ ps
30460 pts/9    00:00:00  \_ cat

$ check=true; echo | while $check; do ps --forest; check=false; done
  PID TTY          TIME CMD
29996 pts/9    00:00:00 bash
30463 pts/9    00:00:00  \_ bash
30464 pts/9    00:00:00      \_ ps

$ for i in 1; do ps --forest; done
  PID TTY          TIME CMD
29996 pts/9    00:00:00 bash
30466 pts/9    00:00:00  \_ ps

$ for i in 1; do ps --forest; done | cat
  PID TTY          TIME CMD
29996 pts/9    00:00:00 bash
30467 pts/9    00:00:00  \_ bash
30469 pts/9    00:00:00  |   \_ ps
30468 pts/9    00:00:00  \_ cat

$ echo | for i in 1; do ps --forest; done
  PID TTY          TIME CMD
29996 pts/9    00:00:00 bash
30471 pts/9    00:00:00  \_ bash
30472 pts/9    00:00:00      \_ ps           
関数でも同じです.
$ testf(){ ps --forest; }

$ testf
  PID TTY          TIME CMD
29996 pts/9    00:00:00 bash
30478 pts/9    00:00:00  \_ ps

$ testf | cat
  PID TTY          TIME CMD
29996 pts/9    00:00:00 bash
30479 pts/9    00:00:00  \_ bash
30481 pts/9    00:00:00  |   \_ ps
30480 pts/9    00:00:00  \_ cat

$ echo | testf
  PID TTY          TIME CMD
29996 pts/9    00:00:00 bash
30483 pts/9    00:00:00  \_ bash
30484 pts/9    00:00:00      \_ ps
  • <$( )
最後に <$( ) です.他の物に比べてなじみが薄いかもしれませんが,これは
$ less <(ssh dokoka cat hoge)
のような形で使います.つまり,コマンドの実行結果を名前付きパイプを通して取り出すことができるようにする構文です.リモートのホストのファイルとの diff を取るのにも便利で重宝してます.さて結果がどうなるかですが,
$ cat <(ps --forest)
  PID TTY          TIME CMD
20218 pts/18   00:00:00 bash
27994 pts/18   00:00:00  \_ bash
27995 pts/18   00:00:00  |   \_ ps
27996 pts/18   00:00:00  \_ cat

$ cat <(ps --forest; echo -n)
  PID TTY          TIME CMD
20218 pts/18   00:00:00 bash
27997 pts/18   00:00:00  \_ bash
27998 pts/18   00:00:00  |   \_ ps
27999 pts/18   00:00:00  \_ cat
となりました.つまり,必ずサブシェルが起動されます.
klab_gijutsu2 at 20:33│Comments(6)TrackBack(0)

トラックバックURL

この記事へのコメント

1. Posted by _goma   2006年09月15日 10:54
こんにちは
これ、わかってないとはまりますよね。
v="old";(v="new";echo $v);echo $v
$vは変わんないですね。

私はreadでよくはまってました。
v="old";VV="old"
echo -e "a\nc\nd" | while read v ;do
VV=$v
echo $v $VV
done
echo $v $VV
2. Posted by かつみ   2006年09月15日 11:29
while read はよく使いますよね.私もしばらく悩んだことがあります.これは,ちょっと強引ですがこういう形にすれば,サブシェルが起動されずに済みます.

v="old";VV="old"
while read v ;do
VV=$v
echo $v $VV
done < <(echo -e "a\nc\nd")
echo $v $VV

ちなみに,本文では ( ) の中のコマンドが一つだけの時はサブシェルは起動されないと書きましたが,

v="old";(v="new");echo $v

でもやっぱり v の値は old のままだったりします.bash(1) によれば,( ) の中身はサブシェルで実行する,と書いてますので,それに合わせてるのだと思います.
3. Posted by _goma   2006年09月15日 14:26
5 なるほど。勉強になります。
4. Posted by fumiyas   2011年06月09日 23:08
「本文では ( ) の中のコマンドが一つだけの時はサブシェルは起動されない」は間違いじゃないでしょうか。Linux で bash 4.1.5 を strace(8) すると clone(2) しているし、Solaris で bash 3.00.16 を truss(1) すると fork1(2) しています。

()、$()、<() >() と | (パイプ)の右辺(?)は別プロセスで実行されることを知っていれば「シェル変数を変更したのに反映されない?!」なんてミスはなくなるかと。
5. Posted by fumiyas   2011年06月09日 23:12
ちなみに ksh, zsh は | パイプの左辺が子プロセスで実行されます。

なので以下を ksh, zsh で実行すると「foo」が表示されます。

echo foo |read bar
echo $bar
6. Posted by かつみ   2011年06月10日 11:48
strace で見ると、確かに ( ) でも、サブシェルが起動されてますね。 ps コマンドで親プロセスを確認した結果にとらわれすぎていたようです…
(ps --forest) の時の ps と親のシェルプロセスが直接の親子関係に見えたのは、( ) の中のコマンド(記事の例では ps)が1つだけの時は、サブシェルが fork() & exec() ではなく、サブシェルが直接 ps を exec() しているのでしょうね…

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント