シェルスクリプト最大の罠、while問題:スマートな紳士のためのシェルスクリプト(8)(1/2 ページ)
シェルスクリプトプログラミングに取り組むときに最もはまりやすい問題、それが「while問題」だ。今回はその原因を掘り下げてみよう。(編集部)
シェルスクリプト最大の罠:while問題
前回はシェルスクリプトプログラミングのコツの1つとして「アット・ア・グランス性」を紹介した。紹介の中でwhileが引き起こしやすい問題について触れたが、前回の説明だけではよく分からなかった方もいると思う。
今回はこの「while問題」に焦点を当て、シェルスクリプトプログラミングで最もはまりやすい問題を掘り下げて説明する。
whileとパイプの組み合わせで問題発生
次のシェルスクリプトを見てほしい。実行結果を予測してほしいのだが、おそらくほとんどの方が「標準出力にLinuxが出力される」と答えるだろう。
#!/bin/sh OS=FreeBSD while : do OS=Linux break done echo ${OS}
今度は次のシェルスクリプトの実行結果を想像してみてほしい。ここで「標準出力にLinuxが出力される」と考えた方は、今回の話は最後まで読んだほうが良いということになる。一方「標準出力にFreeBSDが出力される」と答え、かつ、その理由をシステムコールの動きを取り上げて説明できる方は、今回の話はここで切り上げてもらっても構わない。
#!/bin/sh OS=FreeBSD echo | while : do OS=Linux break done echo ${OS}
実行すると次のようになる。
$ ./while01.sh Linux $ ./while02.sh FreeBSD $
挙動が違うのは、「シェルはパイプが使用されると、コマンドの分だけシェルをfork(2)し、fork(2)して生成されたシェルの方でコマンドを実行する」ためだ。以降でシェルの内部で何が実行されているのか追っていこう。
パイプはコマンドの分だけシェルのfork(2)を必要とする
truss(1)を使ってシステムコールの動きを追う。データの加工に便利なので、コマンドとしてOpen usp Tukubaiを使用する。追実験する場合にはこのコマンドを使ってみてほしい。先日、最初からOpen usp TukubaiがセットアップされたVirtualBox仮想環境のイメージファイルの配布が始まったので、試してみるにはこちらをダウンロードした方が簡単かもしれない (Tukubai on FreeBSD 9.1-RC1)。
$ truss -c ./while01.sh 2>&1 | tail +3 | ctail -2 | self 1 3 | sort -k1,1 | keta access 3 close 11 fcntl 2 fstat 10 getegid 1 geteuid 2 getgid 1 getpid 1 getppid 1 getuid 1 lseek 3 lstat 2 mmap 20 munmap 7 open 11 read 12 readlink 1 sigaction 8 sigprocmask 10 stat 2 write 1 $
$ truss -c ./while02.sh 2>&1 | tail +3 | ctail -2 | self 1 3 | sort -k1,1 | keta access 3 close 13 fcntl 2 fork 2 fstat 10 getegid 1 geteuid 2 getgid 1 getpgrp 1 getpid 1 getppid 1 getuid 1 lseek 3 lstat 2 mmap 20 munmap 7 open 11 pipe 1 read 12 readlink 1 sigaction 8 sigprocmask 10 stat 2 write 1 $
ctail(1)やself(1)、keta(1)はOpen usp Tukubaiのコマンドだ。テキストの整形に便利なのでよく利用する。使い方はオンラインマニュアルにまとまっている。
使われているシステムコールの差異だけを取り出すと次のようになる。パイプを挟んだ方はfork(2)システムコールが2回、pipe(2)システムコールが1回、新しく追加されていることが分かる。
--- truss01 2012-09-09 17:06:28.846231611 +0900 +++ truss02 2012-09-09 17:08:11.563231300 +0900 @@ -1,12 +1,14 @@ -$ truss -c ./while01.sh 2>&1 | tail +3 | ctail -2 | +$ truss -c ./while02.sh 2>&1 | tail +3 | ctail -2 | self 1 3 | sort -k1,1 | keta access 3 - close 11 + close 13 fcntl 2 + fork 2 fstat 10 getegid 1 geteuid 2 getgid 1 + getpgrp 1 getpid 1 getppid 1 getuid 1 @@ -15,6 +17,7 @@ mmap 20 munmap 7 open 11 + pipe 1 read 12 readlink 1 sigaction 8
パイプを挟んでいるのだから、pipe(2)システムコールが追加されているのは当然だ。問題はfork(2)がなぜ2回呼ばれているのか、という点にある。
そこで、次のようなシンプルなモデルを考えてみよう。
a | b
シェルはこのようなラインをパースすると、まず自分自身を2回fork(2)する。そして、fork(2)して生成した子プロセスの入出力をpipe(2)で接続する。
自分をfork(2)してからaを実行 <==この間をpipe(2)で接続==> 自分をfork(2)してからbを実行
コマンドの入出力をpipe(2)で接続するためには、それぞれを個別のプロセスとして独立させなければならない。このためfork(2)して子プロセスを生成している。パイプが4つ使われていれば、5回fork(2)を実行して子プロセスを生成し、それらをpipe(2)で接続する。例えばパイプが4つ連続して使われていれば、fork(2)は5回実行される。
この子プロセスは、シェルではいわゆる「サブシェル」と呼ばれる。
Copyright © ITmedia, Inc. All Rights Reserved.