mmap はシステムプログラミングの本で良く出てきたり、C言語が得意なハッカーのブログ等に掲載されたソースコードで良く見る機会があると思います。
mmap は高度に見えるかもしれませんが、あまり深く考えずにファイルシステムとメモリー領域をマッピングしてくれる関数と考えるぐらいで大丈夫です。
宣言は以下のようになります。
#include <sys/mman.h> void *mmap(void * addr , size_t length , int prot , int flags , int fd , off_t offset ); int munmap(void *addr, size_t length);
mmap 関数は void* 型を返します。
まあこれはどんな型でも型変換で対応できるってことです。
そして munmap はマップを解除する関数です。munmap の引数 length は解除したいデータ範囲のサイズです。
ちなみに close でファイル記述子を閉じてもマップした領域がキャンセルされるわけではないので、意外に重要だったりします。
mmap 関数の引数は以下のような感じになりますね。
これらの引数で特に厄介なのが offset です。
offset はマップしする領域の開始オフセットですが、これはページサイズの倍数で指定される必要があります。
ページサイズは OS のビルド設定によって変動するため、問題を回避するためにランタイムで取得すべきとされています。
その際に使うのが sysconf 関数です。
#include <unistd.h> long sysconf (int name);
name 変数にはマクロを指定するのですが、以下のようにできます。
length.
long page_size = sysconf (_SC_PAGESIZE);
まあ大したことじゃないですが、ページサイズの取得をやってみましょう。
1 #include <unistd.h> 2 #include <stdio.h> 3 4 int main() 5 { 6 long page_size = sysconf(_SC_PAGESIZE); 7 printf("%ld\n",page_size); 8 return 0; 9 }
ビルドと実行結果.
$ gcc main.c $ ./a.out 4096
結果は 4KB でしたね。
もうひとつ厄介な引数は proto です。
保護レベルには 4 種類のマクロがあるので、それを使います。
PROT_READ は読み込みが可能なページ。
PROT_WRITE は書き込みが可能なページ。
PROTO_EXEC は実行できるページてな感じです。
flags 引数にはいろんな指定が可能ですが、基本的な用途には以下 2 つを抑えれば十分です。
MAP_PRIVATE は単一プロセスでしか使わないページのマッピングに適しています。メモリー側での処理が行われるだけで、ファイルに変更が更新されません。
MAP_SHARED は他のプロセスから見えるだけでなく、マップしたファイルにも変更が更新されます。
ではいくつか引数を指定しながら説明していきたいと思います。
main.c.
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/mman.h> 4 #include <sys/types.h> 5 #include <sys/stat.h> 6 #include <fcntl.h> 7 #include <stdlib.h> 8 9 #define COUNT 65536 10 11 int main() 12 { 13 int fd; 14 char buf[COUNT]; 15 char *mm; 16 off_t len; 17 long sum; 18 19 int i; 20 len = sysconf(_SC_PAGESIZE); 21 for(i = 0; i < len; i++) 22 buf[i] = 1; 23 for(i = len; i < len*2; i++) 24 buf[i] = 2; 25 buf[len*2] = '\0'; 26 27 fd = open("abc.txt",O_CREAT|O_RDWR|O_TRUNC,0644); 28 if(fd < 0){ 29 perror("open"); 30 exit(1); 31 } 32 write(fd,buf,COUNT); 33 34 mm = mmap(NULL,len,PROT_READ,MAP_PRIVATE,fd,len); 35 sum = 0; 36 for(i = 0; i < len; i++) 37 sum += mm[i]; 38 printf("sum = %ld\n",sum); 39 40 munmap(mm,len); 41 42 mm = mmap(NULL,len,PROT_READ,MAP_PRIVATE,fd,0); 43 sum = 0; 44 for(i = 0; i < len; i++) 45 sum += mm[i]; 46 printf("sum = %ld\n",sum); 47 48 munmap(mm,len); 49 close(fd); 50 51 return 0; 52 }
ビルドと実行結果.
$ gcc main.c $ ./a.out sum = 8192 sum = 4096
このソースコードは少し複雑なので、軽くまとめてからコードの説明に入りたいと思います。
buf が元データの配列で mm がマップ先のアドレスとなります。
13 int fd; 14 char buf[COUNT]; 15 char *mm; 16 off_t len; 17 long sum;
fd はファイル記述子を保持し len はページサイズとして使います。
sum 変数は(加算結果による)データマッピングのチェックに使います。
9 #define COUNT 65536 //途中割愛 19 int i; 20 len = sysconf(_SC_PAGESIZE); 21 for(i = 0; i < len; i++) 22 buf[i] = 1; 23 for(i = len; i < len*2; i++) 24 buf[i] = 2; 25 buf[len*2] = '\0';
len は sysconf を使ってシステムのページサイズをランタイム取得した値を保持します。
次に buf にデータを設定します。
buf の配列のうち添字が [0,len) の範囲は全て 1 に設定します。
[len,len*2) の範囲は全て 2 に設定します。
buf の値は正確なデータ箇所をマッピングしたかのチェックに使います。
ファイルマッピングには「 abc.txt 」を使います。
27 fd = open("abc.txt",O_CREAT|O_RDWR|O_TRUNC,0644); 28 if(fd < 0){ 29 perror("open"); 30 exit(1); 31 } 32 write(fd,buf,COUNT);
O_CREAT が指定されているので、既に同名のファイルが存在していれば内容をクリーンにして読み込みます。
write 関数は buf を abc.txt に書き込みます。
buf のデータサイズである COUNT は 65536 になっていますが、そのまま全てを書き込みます。
9 #define COUNT 65536
そしてやっとですが mmap をコールします。
34 mm = mmap(NULL,len,PROT_READ,MAP_PRIVATE,fd,len);
まず addr 引数は NULL とします。
NULL はどのアドレスにマッピングを作るかはカーネルに任せるということです。
まあどのアドレスにマッピングしたいか特別な設計でも無い限りこれで大丈夫です。
まず length は len に設定していますが、これはページサイズのことです。
筆者のシステムでは 4096 になっています。
offset 引数についても len に設定しています。
これは [len,len+len) の範囲をマッピングしたアドレスを mm に返す効果があります。
prot 引数には PROT_READ を設定してます。
これは mm からデータを読み込みよという意味です。
最後に flags 引数には MAP_PRIVATE を指定していますね。
これはローカルプロセス内でのみ使うという意味です。
後はデータチェックの部分です。
35 sum = 0; 36 for(i = 0; i < len; i++) 37 sum += mm[i]; 38 printf("sum = %ld\n",sum); 39
このループは mm にマップされたデータを一つ残らず sum に加算していき、以下を標準出力します。
sum = 8192
buf に入っていたデータがファイルに書き込まれているので、マップによって mm には指定された領域のデータが詰まっています。
てなことでこの場合はページサイズこと len が 4096 となり、マップした領域は全て 2 に設定されているので sum は 8192 になります。
後は mm は不要になるのでマップを解除します。
40 munmap(mm,len);
マップの解除によって mm は再利用可能となります。
次は引数 length と offset を変えて実験します。
42 mm = mmap(NULL,len,PROT_READ,MAP_PRIVATE,fd,0); 43 sum = 0; 44 for(i = 0; i < len; i++) 45 sum += mm[i]; 46 printf("sum = %ld\n",sum); 47 48 munmap(mm,len); 49 close(fd);
この場合の mmap は [0,len) の範囲を mm にマップします。
該当範囲は 1 に設定されているので全て加算するとページサイズ len と同じ 4096 が出力されます。
Copyright 2018-2019, by Masaki Komatsu