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(0x502000) = 0x502000
...
mmap(0x50100a, 4096, PROT_WRITE, MAP_PRIVATE, 3, 0) = 0x502000
...
brk(0x523000) = 0x502000
mmap(NULL, 1048576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x2b23377d1000