Cannot forkと言われてしまった話

環境

NetBSD-8.99.1 amd64

それはテスト中に突然起きた

basepkgはシェルスクリプトで実装されています. NetBSDのBase Systemをパッケージにするというものですが, いくつか中身が空のパッケージができあがることに気づいたので 出力をファイルに書き込みながら動作テストをしていました.すると,

./basepkg.sh: Cannot fork (Resource temporarily unavailable)

という出力が7個くらい出ていたことが分かりました.どうやらこれが怪しい.

まずは調査から

ひとまず「Cannot fork Resource temporarily unavailable」でググってみると, Oracleのサイトが最初にヒットしました. 主要メッセージの手引き というやつで,トラブルの原因と対処方法がわかるすごいページです.これによると

原因

このエラーは、システムのプロセステーブルがいっぱいになっているために fork(2) システムコールが失敗した、あるいはメモリーまたはスワップ空間が足りないためにシステムコールが失敗したことを示します。また、ユーザーがそれ以上プロセスの作成を許されていない可能性もあります。

らしい.Solarisの情報ですがNetBSDでも似たようなものだろうと睨み, プロセステーブルがいっぱいになっている == fork()でプロセスを作りすぎた?と 仮定してみることにしました.

たくさんforkしてそうなところを探る

そこで,シェルスクリプトでたくさんforkしてそうなところはどこか探しました. "fork"なんて書いたおぼえ一度もないぞとかぼやいていると, 2年前C言語の勉強をしていたときプロセス間通信にfork()を使った記憶が蘇ってきました. そのとき読んだ本『例解UNIXプログラミング教室』(冨永和人・権藤克彦)を漁ると229ページに

プロセス間で通信するにはforkを使います. forkすると,子プロセスはファイル記述子を引き継ぐので,親子で同じパイプを共有することになります.そして使わないファイル記述子をクローズすれば,親から子(または子から親)への通信路ができます.

とありました.なんかこれっぽい.

シェルスクリプトのプロセス間通信は……

シェルスクリプトでプロセス間通信をするにはパイプ「|」を使います. パイプの数を減らせば,fork()が実行される回数も減り, 結果プロセステーブルに最後まで余裕ができ上記のエラーメッセージは出ないのではと考えました. つまり(?)リファクタリングの時間です.

冗長なパイプの表現を探してみると意外とあり, 例えば与えられた引数から始まる行をファイルから抜き出し,その行の2列目を取得する処理は

grep -E "^$1" ${src}/${deps} | ${CUT} -d ' ' -f 2

のように書いていました(\${GREP}は/usr/bin/grep).しかしこれはawkで

awk '/^'"$1"'/{print $2}' ${src}/${deps}

と書き直せます.パイプを使う必要はありません.また

package-name    description line one.
package-name    description line two.
package-name    description line three.

description line one.
description line two.
description line three.

のように整形するとき

grep -E "^package-name" ${src}/${descr} | \
sed -e "s/package-name//" | tr -d '\t'

と書いていましたがこれもawkで

awk '
/^pacakge-name/ {
    for (i = 2; i <= NF; i++) {
        if (i == NF)
            printf $i"\n"
        else
            printf $i" "
    }
}
'

と書けるはずです.色々やっつけ仕事が多かった証拠ですね.あと極めつけはこれで

grep "$j"a ${PWD}/${i}/CATEGORIZED | ${TR} ' ' '\n' | \
awk 'NR != 1 {print}' | sort | \
grep -v -E "x${i}-[a-z]+-[a-z]+" > ${PWD}/${i}/${j}/${j}.FILES

もはや本能のままにパイプをつなげてしまった感がありますが

awk '
/^'"$j"'/ {
    for (i = 2; i < NF; i++) {
        print $i
    }
}' ${PWD}/${i}/CATEGORIZED > ${PWD}/${i}/${j}/${j}.FIELS

すっきりしました.

リファクタリングを終え

パイプの数を減らし実行してみるとエラーは出ず,無事に直せました. 「いつプロセステーブルが開放されるのか?」というカーネル側の問題はひとまず置いておいて, むやみにパイプを使ってデカいシェルスクリプトを書くのはやめておこうという教訓が得られました.