シェルスクリプト最大の罠、while問題スマートな紳士のためのシェルスクリプト(8)(1/2 ページ)

シェルスクリプトプログラミングに取り組むときに最もはまりやすい問題、それが「while問題」だ。今回はその原因を掘り下げてみよう。(編集部)

» 2012年09月14日 00時00分 公開
[後藤大地,BSDコンサルティング株式会社]

シェルスクリプト最大の罠: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回実行される。

 この子プロセスは、シェルではいわゆる「サブシェル」と呼ばれる。

       1|2 次のページへ

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。