コンピューター:C言語講座:mmapについて
概要
mmapはファイルをメモリにマッピングするものです。わかりやすく言うと、UNIXのOSが行なっているページングやスワッピングを自プロセスで行なうようなものです。
これを使うメリットとしては、メモリ確保サイズが確保開始時に決定できない場合、簡単にはmalloc()で適当な量を確保し、足りなければrealloc()を使用して拡大することになりますが、realloc()を繰り返すとメモリ領域中に空きが出来やすく、大規模なシステムでは実際使用しているメモリより空き空間の方が数倍大きくなってしまい、メモリ不足で実行不可能になる場合があります。mmapを使用すれば、別々に拡大していくような管理が可能なので、また、スワップスペースとは別の位置に置くことにより、多数の動的メモリを必要とする場合に安全に動作することが可能になる場合があります。ただし、基本的にファイルなので、最大ファイルオープン可能数を越えてマッピングすることは出来ませんので、環境の調査・調整はしっかり行なわなければなりません。また、mmapは可能な範囲でRAM上に展開しながら動作してくれるので比較的高速ですが、巨大な領域を移動する場合には当然ページングなどと同様に遅くなります。
その他に、メモリイメージをファイルとして扱えますので、単純なメモリイメージの保存・再利用も可能ですし、更に共有メモリの用にメモリイメージを共有することも可能です。
サンプル
ここでは単純にファイルをマップし、データの利用が可能というサンプルでも良いかと思ったのですが、せっかくなので、書き込みプロセスと読みだしプロセスを分けて共有も可能だ、ということも見れるようなサンプルにします。
書き込みプロセス
#include <stdio.h>
#include <math.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/mman.h>
typedef struct{
char str[512];
long lval;
double dval;
}SSS;
#define NUMBER (10000)
void main()
{
int fd;
char c;
long psize,size;
SSS *ptr;
long i,lval;
double dval;
char buf[512];
/* マップ用ファイルオープン */
if((fd=open("MapFile",O_RDWR|O_CREAT,0666))==-1){
perror("open");
exit(-1);
}
/* ページサイズで境界合わせを行なったサイズを計算 */
#ifdef BSD
psize=getpagesize();
#else
psize=sysconf(_SC_PAGE_SIZE);
#endif
size=(NUMBER*sizeof(SSS)/psize+1)*psize;
/* ファイルの必要サイズ分先にシークし、0を書き込み */
/* ファイルのサイズをマップしたいサイズにする為 */
if(lseek(fd,size,SEEK_SET)<0){
perror("lseek");
exit(-1);
}
if(read(fd,&c,sizeof(char))==-1){
c='\0';
}
if(write(fd,&c,sizeof(char))==-1){
perror("write");
exit(-1);
}
/* マップ */
ptr=(SSS *)mmap(0,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if((int)ptr==-1){
perror("mmap");
exit(-1);
}
/* テスト */
while(1){
/* 標準入力からデータ読み込み */
gets(buf);
if(feof(stdin)){
break;
}
lval=atoi(buf);
dval=atof(buf);
/* 全データの値をセット */
for(i=0;i<NUMBER;i++){
strcpy(ptr[i].str,buf);
ptr[i].lval=lval;
ptr[i].dval=dval;
}
/* 実際にファイルに書き込みたい場合はmsync()する */
/* msync(ptr,size,MS_ASYNC); */
}
/* 実際にファイルに書き込み、同期を取る */
msync(ptr,size,0);
/* アンマップ */
if(munmap(ptr,size)==-1){
perror("munmap");
}
/* ファイルクローズ */
close(fd);
}
コメントを入れてあるのでわかりやすいと思いますが、まず、マップ用ファイルを作成します。ここではMapFileという名前のファイルを使用していますが、以前のファイルが残っていた場合はそのまま使うようにしてみました。つぎに、サイズを計算しますが、ページサイズに合わせておく必要があります。ページサイズとはOSのメモリ管理単位量のようなもので、getpagesize(),sysconf()で得ます。そのサイズ分のファイルの実体を作成する為に、シークし、値を書き込んでおきます。この時点でMapFileが実際にサイズ分のファイルとして存在するようになります。サンプルでは低水準入出力を使用していますが、高水準でもかまいません。ただし、実体に反映する為にfflush()でフラッシュすることを忘れずに。
ファイルの実体が出来た時点で、mmap()でマップします。このサンプルではlong,double,char[]の3つのメンバを持つ構造体の配列を想定してみました。これ以降は普通のメモリと何ら変わり無くアクセスすることが出来ます。
サンプルでは標準入力から文字列を得て、それをそれぞれのメンバに代入しています。確保分、全て同じ値をセットしています。標準入力がEOFとなる(^Dを入力)とmsync()でファイルに書き込み同期を取り、アンマップして、ファイルをクローズして終了します。
読み出しサンプル
#include <stdio.h>
#include <math.h>
#include <fcntl.h>
#include <math.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/mman.h>
typedef struct {
char str[512];
long lval;
double dval;
}SSS;
#define NUMBER (10000)
void main()
{
int fd;
long psize,size;
SSS *ptr;
long i;
/* マップ用ファイルオープン */
if((fd=open("MapFile",O_RDWR))== -1){
perror("open");
exit(-1);
}
/* ページサイズで境界合わせを行なったサイズを計算 */
#ifdef BSD
psize=getpagesize();
#else
psize=sysconf(_SC_PAGE_SIZE);
#endif
size=(NUMBER*sizeof(SSS)/psize+1)*psize;
/* マップ */
ptr=(SSS *)mmap(0,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
/* テスト */
while(1){
/* 全データの表示 */
for(i=0;i<NUMBER;i++){
if(i%1000==0){
printf("{%5d:%d,%g,%s}\n",i,ptr[i].lval,ptr[i].dval,ptr[i].str);
}
}
printf("\n");
/* 1秒待ち */
sleep(1);
}
}
ほとんど同じ感じですが、こちらはファイルを作成はしないようにオープンしています。すでに書き込みサンプルでMapFileがある状態での動作を想定しています。サイズ計算・マップは書き込みと同じです。テストとして、全データを1000個おきに表示しています。1秒周期で延々と続けます。この間に書き込みプロセスで値を変更するとこの表示の値も変更されるはずです。
また、環境によると思いますが、書き込み側で変更した場合に、起動後すぐと、何回か行なった後では読み出し側の表示が変わる速度が後のほうが速いと思います。よく使うとわかった場合にOSがたくさんメモリ上に展開してくれる為です。
まとめ
ファイルのメモリマッピングというと難しそうですが、以外と簡単に使えるものではないでしょうか?この他に確保したものをリサイズしたい場合もあると思いますが、これは共有を考えればわかると思います。1つのプロセスで一旦アンマップし、ファイルサイズを拡大してからもう一度マップすれば良いのです。ただし、アンマップする前にmsync()でファイルに実際に書き込ませたほうが安全です。
この他にもmadvise(),mctl()などでOSに対して詳細な指示も可能ですが、素人がいい加減な指示を与えるとかえって遅くなります。また、mlock(),mlockall()などでロックも可能です。
このようにmmapは意外に簡単に使えますが、実際のシステムへの応用は十分な検討が必要ですし、更に細かな指示まで含めると奥が深いものです。しかし、完全に理解しなくても必要に応じた範囲で使うだけでも十分に役に立ちます。大データを扱う場合や、共有メモリに入らないくらいのデータを共有する場合など、解決策の一つとして頭に入れておくと良いでしょう。いずれにしてもテストプログラムなどで十分に動作を確認し、自分の望む動作の確認を行なってから使用することをお勧めします。