第73章 mmap の使い方

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 関数の引数は以下のような感じになりますね。

addr
マップ先のアドレス、NULL を指定すると新しい領域、それ以外は mmap がマッピングするアドレスのヒントにする
length
マップしたい領域の長さを指定。
prot
マップの保護レベルを指定
flags
変更した結果が他のプロセスから見えるかを指定( mmap の設定パラメーターも指定可能)
fd
ファイル記述子を指定
offset
開始点となるファイルオフセット(ファイルマッピングの場合)。ページサイズの倍数とする制限がある

これらの引数で特に厄介なのが 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 種類のマクロがあるので、それを使います。

表73.1 メモリー保護( prot )

説明
PROT_NONE
アクセス無し
PROT_READ
読み込みアクセス
PROT_WRITE
書き込みアクセス
PROT_EXEC
実行可能

PROT_READ は読み込みが可能なページ。

PROT_WRITE は書き込みが可能なページ。

PROTO_EXEC は実行できるページてな感じです。

flags 引数にはいろんな指定が可能ですが、基本的な用途には以下 2 つを抑えれば十分です。

MAP_PRIVATE
メモリーマップに加えた変更は外部プロセスから見えない。Copy-On-Write (COW) という方式を使うため、プロセスまるごとにコピーはせず必要な時に元のプロセスから子プロセスに対してコピーが行われる。 ( flags で指定 )
MAP_SHARED
メモリーマップを外部プロセスと共有できる ( flags で指定 )

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

このソースコードは少し複雑なので、軽くまとめてからコードの説明に入りたいと思います。

  1. buf は COUNT バイトの配列です。0 から len までを 1 とし len から len*2 までを 2 に設定します。
  2. abc.txt を生成または開き fd にファイル記述子を保存します。
  3. mmap をコールして len オフセットを起点として len バイトのマッピングをします。マッピングしたアドレスは mm に保存します
  4. mm のマッピングを解除します。
  5. mmap をコールしてオフセット 0 を起点として len バイトのマッピングをします。マッピングしたアドレスは mm に保存します。
  6. mm のマッピングを解除します。
  7. fd を close をコールして閉じます。

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