Linux 上での closefrom と async signal safety

*BSD や OS X, Solaris には、 closefrom( lowfd ) という「 lowfd 以上の値を持つファイルディスクリプタ (以下 FD) を全て閉じる」システムコールがある。一見ちょっと微妙なインターフェイスのシステムコールだけれど、実は後述する理由でとても有用である。

残念ながら Linux に closefrom は存在しないので、ユーザ空間で実装しようという話になるのだけど、いくつか微妙な問題がある。問題があるというか、問題があると思ったら、実は問題がなかったと言うか…。

話が二転三転するので、参考にする方は最後まで目を通してほしい。

closefrom の必要性

fork() 時にファイルディスクリプタを閉じる方法はいくつかある。

ファイルのオープン直後に fcntl( ..., FD_CLOEXEC )
最も古典的 (?) な方法で、移植性が高い。残念ながらマルチスレッド環境下で問題がある。 open() - fcntl() 間に他のスレッドで fork() すると、そのファイルは子プロセスで開いたままになってしまう。
open( ..., O_CLOEXEC ) [2]
Linux 限定(正確に言うと open() に関しては FreeBSD などでも使えるが、 pipe2() などのシステムコール新設が必要なものに関しては対応していないことが多い)。
また、サードパーティのライブラリ内部で open() している場合にこの方法は採れない。特にマルチスレッド環境下では関数内で open() - close() が完結していたとしても駄目。これも実用上はけっこうきつい。
closefrom()
上述の問題はクリアされるが、逆にサードパーティのライブラリ内で fork() されると無力。また Linux には存在しない。

一番正統な解決策は Linux の O_CLOEXEC かなあと個人的には思う。ただ現実的には closefrom() が便利なことが多い(== 私は多かった)。そこで、 closefrom() を Linux でエミュレートできれば、という話になる。

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_atfork() を用いて fork() 後にロックを開放するからだ。 [0]

これは別に readdir() を使った closefrom() の実装が async-signal-safe であるという意味ではない。が、すぐに exec() するような使い方に限定すれば特に問題はない。

まとめ

ということで多くの場合、 Linux 依存の非公開システムコールを使ってまで async-sygnal-safe な closefrom() を実装する必要はない。実際に、 Sun の Java VM や CPython では readdir() を使った closefrom() の実装が使われている。

参考

cd ../

Yasuhiro Fujii <y-fujii at mimosa-pudica.net>