2006年5月 3日

glibc の fopen() で 'm' オプションを使う

最近の glibc の fopen() には 'm' というオプションがあると知りました。 'm' オプションを指定すると、リードオンリーでファイルを開いたとき、可能な場合、 mmap が内部的に使われるようになります。

 

次のようなプログラムに対して strace をかけると fgets() の内部的で read システムコールが呼ばれていることがわかります。

#include <stdio.h>
#include <assert.h>
int main() {
    char buf[1024];
    FILE *fp = fopen("/etc/shells", "r");
    // FILE *fp = fopen("/etc/shells", "rm");  // using mmap I/O
    assert(fp != NULL);
    fgets(buf, sizeof(buf), fp);
    printf("%s", buf);
    return 0;
}

実行結果は以下の通りです。

% ./a.out
# /etc/shells: valid login shells
% strace ./a.out 2>&1 |grep read |grep /etc/shells
read(3, "# /etc/shells: valid login shell"..., 4096) = 240

次に、上のプログラムの fopen("/etc/shells", "r") の部分を fopen("/etc/shells", "rm") で置き換えると、read システムコールは消えて、代わりに mmap が使われるようになります。

% ./a.out
% strace ./a.out 2>&1 |grep read |grep /etc/shells
% strace ./a.out 2>&1 |grep 'open.*shells' -A2
open("/etc/shells", O_RDONLY)           = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=240, ...}) = 0
mmap2(NULL, 240, PROT_READ, MAP_SHARED, 3, 0) = 0x40018000

通常、 I/O を頻繁に行うプログラムで read の代わりに mmap を使うと性能が向上します。fopen に 'm' を渡すだけで stdio ベースのプログラムの性能を向上できるなら、おいしい話です。

とはいうものの、次のような問題もあります。

  • glibc 固有のオプションゆえに移植性が下がる
  • 現状、文書化されていないオプションである
  • 古い glibc ではサポートされていない (ChangeLog によると 2002-08-29 に入った模様)
  • リードオンリーで開いたときしか効果がない (ファイルのサイズが変わっちゃうと困るので)
  • 他のプロセスにファイルを truncate されるとおそらく SIGBUS が起こる
  • 32ビットのアーキテクチャでは 1MB までしか mmap しない (glibc-2.4/libio/fileops.c の decide_maybe_mmap() でチェックされている)

最後の点は、32ビットアーキテクチャでは大きなファイルをどんどん mmap していくとアドレス空間が簡単に枯渇してしまうため施されている対策です。

まとめ

glibc の fopen の 'm' オプションを紹介しました。移植性が下がるなどの問題もありますが、既存の stdio ベースのプログラムの性能を手軽に向上させる手段として役に立つ場合もあるのではないかと思います。