mmap()の使い方

mmapのAPI

まずはmmap()のAPIを説明します。
fd = open(file, access);
a' = mmap(a, n, prot, share, fd, offset);
まずは普通にファイルをopen()で開きます。その後、mmap()でマップします。 aは「この仮想メモリアドレスにマップしてほしい」という値を指定できるのですが、普通はNULLでOKです。nはファイルの何バイト目をマップするのか、offsetは何バイト目からをマップするのかという指定です。

shareという引数がありますが、これは複数のプロセスが同じファイルをmmapしたときの動作です。 shareにMAP_PRIVATEを指定すると、そのプロセスは別のコピーを見て、プロセス間でデータは共有されません。メモリ上に複数データがある…ということで、ディスクにはどのデータを書いていいのか分からないので、書き込み結果はファイルに反映されません。

これに対し、shareにMAP_SHAREDを指定すると、 複数のプロセスが共通の物理メモリを参照します。また、書き込んだデータは共有されます。

メモリ割り当てに用いる

mmap()はこのようにファイルをメモリのように読むためのAPI…のはずなのですが、 実際にはファイルを全く用いない使われ方の方が多いです。 ファイルとして/dev/zeroという、0が無限に並んでいるような特殊なファイルを 指定してmmap()を実行すると、mmap()はディスクに実体の無いメモリ領域を確保できます。 このメモリを読んでも0しか読まれないし、メモリに書いたデータはディスクに残りません。 これはまさに新しいページを確保したことに相当します。
元々Unixのメモリ確保のシステムコールはbrk()のはずなのですが、 大きなメモリを確保する時にはmmap()が用いられています。 両者のサンプルコードを書いてみます。
[mmapでのメモリallocation]

#include <unistd.h>
#include <sys/mman.h>
#include <stdio.h>
#include <fcntl.h>

char *alloc_with_mmap(int size){
  int fd;
  char *ret;
  fd = open("/dev/zero", O_RDONLY);
  ret = mmap(NULL, size, PROT_WRITE , MAP_PRIVATE, fd, 0);
  close(fd);
  return ret;
}
因みにbrk()を使った時のメモリ割り当ては以下です。
(brkに少しだけ処理を加えた、sbrkというライブラリ関数を使っています)
[brkでのメモリallocation]

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>

char *alloc_with_sbrk(int size){
  char *ret;
  ret = sbrk(size);
  return ret;
}
動作としては、brk()がヒープを拡張するコマンドであるのに対し、mmap()は「どこからか」勝手な領域を取ってきます。 brk()は必ず順番に(さっき確保したメモリの隣に)メモリを確保するのに対し、 mmap()では適当な番地を指定して、そこにメモリを確保することもできます。 mmap()の方が自由度が高く、ヒープを伸ばせない場合でも空き番地を見つけられる一方で、 対応表の更新など、コストはmmap()の方が大きくなってしまいます。また、mmap()ではページ単位でしかメモリを確保できません。
malloc()は、標準設定では128kBを境に両者を使い分けています。ただし、malloc()は一度大きめにメモリを確保しておき、 必要に応じて「切り売り」するという手法を取っているので、いつも128kb未満の場合にbrk()が呼ばれるわけではありません。 場合によっては、mmap()された領域がbrk()された領域のすぐ近くであるためbrk()が失敗してしまう場合もありますが、 その場合malloc()は再度mmap()で領域を取り直します。

余談: brk()とmmap()とmalloc()

brk()はヒープを順番に伸ばすシステムコールです。だから、brk()される領域のすぐ横をmmap()しておくと、 brk()は「物理メモリが無くなった」と勘違いして失敗してしまいます。そのデモ。
#include <unistd.h>
#include <sys/mman.h>
#include <stdio.h>
#include <fcntl.h>

char *alloc_with_mmap(int size, char *A){
  int fd;
  char *ret;
  fd = open("/dev/zero", O_RDONLY);
  ret = mmap(A, size, PROT_WRITE , MAP_PRIVATE, fd, 0);
  printf("MMAPed address: %x\n",ret);
  return ret;
}


char *alloc_with_brk(int size){
  char *ret;
  ret = sbrk(size);
  printf("BRKed address: %x\n",ret);
  return ret;
}



int main(void){
  char *cp;
  cp = alloc_with_brk(4096); // brk()で4096バイトメモリを確保
  alloc_with_mmap(4096, cp+10); // brk()したアドレスの隣から初めて、mmapで4096バイトメモリを確保
  alloc_with_brk(4096); // 再度、brk()で4096バイトメモリを確保
}
出力の例。最後のbrk()は失敗してしまっています。
BRKed address: 501000
MMAPed address: 502000
BRKed address: ffffffff
(しかしFreeBSDだとそんなことにはならないんだな。やっぱりBSDの方が手堅いですね。)
malloc()は通常128kB以下のアロケートにはbrk()を使いますが、このようにbrk()に失敗したら 改めてmmap()でメモリを取ってきます。その様子は、上のプログラムでmain()を次のように書き換えて straceで実行すると分かります。
int main(void){
  char *cp;
  cp = alloc_with_brk(4096); // brk()で4096バイトメモリを確保
  alloc_with_mmap(4096, cp+10); // brk()したアドレスの隣から初めて、mmapで4096バイトメモリを確保
  malloc(10); // malloc()で4096バイトメモリを確保
}
brk(0)                                  = 0x501000 現在のbrk位置を調べる
brk(0x502000)                           = 0x502000 brkで領域拡張成功 (引数と返り値が同じ)
...
mmap(0x50100a, 4096, PROT_WRITE, MAP_PRIVATE, 3, 0) = 0x502000 mmapで番地を指定してallocate
...
brk(0x523000)                           = 0x502000 brkで領域拡張失敗 (引数と返り値が異なる)
mmap(NULL, 1048576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x2b23377d1000 改めてmmapで領域確保