ファイル記述子の継承とマルチスレッド

今日は昨日の話のPOSIX版です。


Windowsのハンドルに対応する概念はPOSIXではファイル記述子です。
ファイル記述子もハンドルと同じく子プロセスに継承させることができます。(というかWindowsの方がマネしているんでしょうが。)
面倒なことに、ファイル記述子は既定ではすべて継承可能であり、継承させないようにするためには、fcntl()でFD_CLOEXECフラグを明示的に設定しなければいけません。
これは、ファイル記述子の取得と継承不能にする操作がアトミックに行えないことを意味します。
このため、FD_CLOEXECフラグだけではファイル記述子の意図しない継承を防ぐことはできません。

int flags = fcntl(fd, F_GETFD);
flags |= FD_CLOEXEC;
fcntl(fd, F_SETFD, flags);


一方、POSIXで子プロセスに実行ファイルを起動させる操作は、fork()による子プロセスの作成と、exec系関数によるファイルの実行の二段階で行われます。
なので、fork()してからexecするまでの間に不要なファイル記述子を閉じたり、標準入出力のセットアップを行ったりできるわけです。


ここで、残したいファイル記述子は分かるでしょうが、それ以外の不要なファイル記述子のリストをどうやって調べるかが問題になります。
ファイル記述子は0以上の連番ですし、sysconf(_SC_OPEN_MAX)でファイル記述子の最大数が取得できるので、0から最大値までのファイル記述子を全てを確認するのが素直な方法です。

// 全てのファイル記述子を閉じる(バグあり)
void close_all()
{
    int open_max = sysconf(_SC_OPEN_MAX);
    for (int i = 0; i < open_max; ++i)
        close(i);
}

しかし、sysconf()は判定不能(無制限)の場合に-1を返すので、これも確実な方法ではありません。
仕方ないのでHamigaki.Processでは判定不能の場合は256で決め打ちしています。


実際問題、数千、数万ものファイル記述子を使用するプログラムでポコポコ子プロセスを起動していたら、ファイル記述子を閉じるだけでも大変なので、運用でカバーするしかないのでしょう。
決め打ちの256は、我慢できる妥当な線ではないかと思います。


別の問題として、マルチスレッド環境でfork()を呼び出しても、子プロセスにはfork()を呼び出したスレッドしかコピーされないという事実があります。
このため、別スレッドがmalloc()などのロックを必要とする関数を呼んでいる場合、子プロセス側で永遠にロックが取れない状況が起こりえます。
よって、fork()からexecまでの間には、この手のロックを必要としない非同期シグナル安全関数しか呼べません。
まぁ、これは実装する上での注意点ですね。
とはいえ、サードパーティー製のライブラリの中には勝手にfork()したり、スレッドを起動したりするものもあるので厄介です。
UNIX流に単機能のプロセスに分割して、そういったライブラリの影響を局所化するしかないのでしょう。
#個人的には、UNIXでスレッドを使うこと自体がNGだと思っています。


本当はPOSIXの仕様を眺めていて、posix_spawn()という関数を見つけたので、これらの問題を解決してくれる関数かと期待したのですが、上記の手順を一つにまとめたラッパーみたいでした。
というわけで、今日は細かいバグ修正のみです。
今日の成果物