*BSD や OS X, Solaris には、 closefrom( lowfd ) という「 lowfd 以上の値を持つファイルディスクリプタ (以下 FD) を全て閉じる」システムコールがある。一見ちょっと微妙なインターフェイスのシステムコールだけれど、実は後述する理由でとても有用である。
残念ながら Linux に closefrom は存在しないので、ユーザ空間で実装したくなるのだけれど、いくつか微妙な問題がある。問題があるというか、問題があると思ったら、実は問題がなかったと言うか…。
話が二転三転するので、参考にする方は最後まで目を通してほしい。
fork() 時にファイルディスクリプタを閉じる方法はいくつかある。
fcntl( ..., FD_CLOEXEC )
open() - fcntl() 間に他のスレッドで fork() すると、そのファイルは子プロセスで開いたままになってしまう。
open( ..., O_CLOEXEC ) [2]
open() に関しては FreeBSD などでも使えるが、 pipe2() などのシステムコール新設が必要なものに関しては対応していないことが多い)。
open() している場合にこの方法は採れない。特にマルチスレッド環境下では関数内で open() - close() が完結していたとしても駄目。これも実用上はけっこうきつい。
closefrom()
fork() されると無力である。また Linux には存在しない。
一番正統な解決策は Linux の O_CLOEXEC かと個人的には思う。ただ、現実的には closefrom() が便利なことが多い。そこで closefrom() を Linux でエミュレートできれば、という話になる。
Linux では /proc/self/fd から自プロセスの開いている FD が取得できるので、これを使ってみる。
void closefrom( int lowfd ) {
DIR* dir = opendir( "/proc/self/fd" );
int dfd = dirfd( dir );
while( true ) {
dirent* entry = readdir( dir );
if( entry == nullptr ) {
break;
}
if( entry->d_type == DT_DIR ) {
continue;
}
int fd = atoi( entry->d_name );
if( fd >= lowfd && fd != dfd ) {
close( fd );
}
}
closedir( dir );
}
この実装、じつは問題がある。 closefrom() は fork() と exec() の間で呼べないとあまり意味がないし、シグナルハンドラ内でも動いてほしいので、 async-signal-safe でなければいけない。実際に *BSD の man には、そう明記されていたはず。しかしこの実装で使っている opendir() は async-signal-safe ではない。
余談ながら、一般にマルチスレッド環境下だからと言って readdir_r() を使う必要はない。 readdir_r() を使う必要があるのは、 opendir() で開いた同じディレクトリストリームに対して複数のスレッドからアクセスする場合だけである。これはレアケースだし、そもそも readdir_r() はあまり推奨されない(らしい)。 [1]
さて、 POSIX の範囲で async-signal-safe にディレクトリの内容を列挙する方法はないけれど、 Linux には getdents() という低レベルシステムコールがあるので、これを使って実装できる。 readdir() も実際には glibc が getdents() を使って実装しているのだと思う(未確認)。
void closefrom_fallback( int lowfd ) {
int maxfd = sysconf( _SC_OPEN_MAX );
for( int fd = lowfd; fd < maxfd; ++fd ) {
close( fd );
}
}
void closefrom( int lowfd ) {
assert( lowfd >= 0 );
struct linux_dirent {
unsigned long d_ino;
unsigned long d_off;
uint16_t d_reclen;
char d_name[1];
};
int dfd = open( "/proc/self/fd", O_RDONLY | O_DIRECTORY );
if( dfd < 0 ) {
return closefrom_fallback( lowfd );
}
while( true ) {
uint8_t buf[PIPE_BUF];
int n = syscall( SYS_getdents, dfd, buf, PIPE_BUF );
if( n <= 0 ) {
break;
}
int offset = 0;
while( offset < n ) {
linux_dirent* entry = reinterpret_cast<linux_dirent*>( buf + offset );
uint8_t d_type = buf[offset + entry->d_reclen - 1];
if( d_type != DT_DIR ) {
int fd = 0;
char* it = entry->d_name;
while( '0' <= *it && *it <= '9' ) {
fd = fd * 10 + (*it - '0');
++it;
}
if( it == entry->d_name || *it != '\0' ) {
// /proc may not be procfs.
close( dfd );
return closefrom_fallback( lowfd );
}
if( fd >= lowfd && fd != dfd ) {
close( fd );
}
}
offset += entry->d_reclen;
}
}
close( dfd );
}
念のため atoi() の使用も避けてみたり、 /proc が存在しない場合のフォールバックも用意したりしてみたけれど、まあ細かいことは気にするな。
この実装は、これはこれで(非公開システムコールを使っている点を除けば)問題なく動く。でも実は readdir() を使った元のコードを fork() - exec() 間で呼んでもデッドロックしない。理由は次の通りである。
なぜ opendir() が async-signal-unsafe かというと、 opendir() が内部で malloc() を呼ぶ必要があるからだ。 malloc() は内部でロックを保持するので、 malloc() の途中に他スレッドが fork() を呼びだすとロックが獲得されたままになる可能性がある。だから exec() する前に malloc() を呼ぶとデッドロックの可能性は避けられない…ように思える。でもこれは glibc においては正しくない。なぜなら glibc malloc() は pthread_ を用いて fork() 後にロックを開放するからだ。 [0]
これは別に readdir() を使った closefrom() の実装が async-signal-safe であるという意味ではない。が、すぐに exec() するような使い方に限定すれば特に問題はない。
ということで多くの場合、 Linux 依存の非公開システムコールを使ってまで async-sygnal-safe な closefrom() を実装する必要はない。実際に、 Sun の Java VM や CPython では readdir() を使った closefrom() の実装が使われている。