*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()
の実装が使われている。