今回は、mmap(2)で共有メモリを実装すると処理能力の向上が見込めること、そしてその実装方法にはいくつもの選択肢があることを紹介し、次回以降の比較につなげていく。(編集部)
第5回では、プロセス間でデータをやりとりする方法としてパイプpipe(2)と共有メモリmmap(2)の双方を取り上げ、それぞれの利点・欠点について紹介した。
パイプpipe(2)を使う方法は実装がシンプルだし、概念としても分かりやすい。どのタイミングでロックがかかるかも明確だ。一方、小さいサイズのデータを何度もやりとりするようなケースでは、大量のread(2)/write(2)システムコールが発生し、効率が悪いことも紹介した。
共有メモリmmap(2)を使う方法は、pipe(2)/read(2)/write(2)と比べて実行速度の面で利点がある。共有メモリを使うと、それぞれ独立したアドレス空間から同一のメモリ空間にアクセスできるようになる。アクセスにあたってカーネルを経由する必要がないため、速度上の利益が見込める。一方で共有メモリを使う場合、ロックを自分で実装する必要があることと、いずれかのプロセスが誤った書き込みを行うと、共有しているすべてのプロセスに影響が及ぶという弱さがあることも紹介した。
今回は、mmap(2)で共有メモリを実装する場合、前回の方法以外にもいくつも選択肢があることを示したい。それぞれどういった違いが現れるのかは次回以降紹介するが、その前段階となる準備を説明していく。
まず、前回使った共有メモリを使うプロセス間通信のソースコードを見てみよう。
この実装は、mmap(2)でバックエンドにファイルを持たない共有メモリ領域を確保し、fork(2)によってプロセスを分離することで2つのプロセスからアクセスできるようにした。なお、繰り返しの回数を1000万回から10億回に変更してある。今回はmmap(2)の実装しか比較しないので、このくらいの方が比較しやすい。
#include <unistd.h> #include <sys/mman.h> #include <stdio.h> int main(void) { int i, rp=1000000000; char *s; s = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED, -1, 0); *s = '0'; if (0 == fork()) { for (i = 0; i < rp; i++) { while ('0' == *s) ; *s = '0'; } } else { for (i = 0; i < rp; i++) { while ('1' == *s) ; *(s+1) = 'a'; *s = '1'; } } }
このように、実体として対応するファイルが持たないところが特徴といえる。ファイルディスクリプタには-1を指定しているし、フラグとしてMAP_ANONを指定している。
mmap(2)は通常、ファイルをマッピングする用途で利用する。仮にUFS上のファイルを利用するコードを書くと、次のようになる。ここでは/tmp/shmを共有ファイルとして使っている。なお/tmpはUFSファイルシステムの上にある。
#include <unistd.h> #include <fcntl.h> #include <sys/mman.h> int main(void) { int fd, i, rp=1000000000; char *s; fd = open("/tmp/shm", O_RDWR); s = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); *s = '0'; for (i = 0; i < rp; i++) { while ('1' == *s) ; *(s+1) = 'a'; *s = '1'; } }
動作の基本はmmap(2)+fork(2)と同じだが、fork(2)するわけではないので、データを送信する側とデータを受信する側、2つのコードを作成する。
#include <unistd.h> #include <fcntl.h> #include <sys/mman.h> int main(void) { int fd, i, rp=1000000000; char *s; fd = open("/tmp/shm", O_RDWR); s = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); for (i = 0; i < rp; i++) { while ('0' == *s) ; *s = '0'; } }
UFSのファイルシステム上に、共有に利用するためのファイルを次のように作っておく。
% dd if=/dev/zero of=/tmp/shm bs=4096 count=1
4.4BSD mmap(2)では本来的には無名のマップ領域を作るのではなく、このようにバックエンドのファイルを指定しての共有メモリという使い方が想定されていた。
バックエンドにUFS上のファイルを使うと、アクセスが遅くなるような印象を受ける。当初は、こうした用途ではメモリファイルシステムを活用して、メモリ上のファイルをバックエンドに指定する方法が想定されていたようだ。コードは次のようにパスを変えるだけでよい。/tmp1がメモリファイルシステムになっている。
#include <unistd.h> #include <fcntl.h> #include <sys/mman.h> int main(void) { int fd, i, rp=1000000000; char *s; fd = open("/tmp1/shm", O_RDWR); s = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); *s = '0'; for (i = 0; i < rp; i++) { while ('1' == *s) ; *(s+1) = 'a'; *s = '1'; } }
#include <unistd.h> #include <fcntl.h> #include <sys/mman.h> int main(void) { int fd, i, rp=1000000000; char *s; fd = open("/tmp1/shm", O_RDWR); s = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); for (i = 0; i < rp; i++) { while ('0' == *s) ; *s = '0'; } }
メモリファイルを作成する方法はいくつかある。FreeBSDであれば典型的にはmdconfig(8)/mdmfs(5)を使用する。mdconfig(8)は採用実績が長く、高負荷状態でも堅牢に動作するという特徴がある。次のように作業すれば/tmp1/以下はメモリ上に確保される。ここではサイズはそれほど必要ないので、10MBを指定して作ってある。
# mdconfig -a -t malloc -s 10M -u 0 # newfs /dev/md0 # mkdir /tmp1 # mount /dev/md0 /tmp1 # chmod 1777 /tmp1
% dd if=/dev/zero of=/tmp1/shm bs=4096 count=1
mdconfig(8)では、メモリ確保の方法を複数の選択肢から選ぶことができる。-t mallocを指定した場合にはカーネルアドレス空間内のメモリをmalloc(9)経由で確保して利用するようになる。-t swapにすれば、スワップをページアウトの対象領域とし、バッファメモリを使用するようになる。例えば、次のようにして作成できる。
# mdconfig -a -t swap -s 10M -u 1 # newfs /dev/md1 # mkdir /tmp2 # mount /dev/md1 /tmp2 # chmod 1777 /tmp2
% dd if=/dev/zero of=/tmp2/shm bs=4096 count=1
#include <unistd.h> #include <fcntl.h> #include <sys/mman.h> int main(void) { int fd, i, rp=1000000000; char *s; fd = open("/tmp2/shm", O_RDWR); s = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); *s = '0'; for (i = 0; i < rp; i++) { while ('1' == *s) ; *(s+1) = 'a'; *s = '1'; } }
#include <unistd.h> #include <fcntl.h> #include <sys/mman.h> int main(void) { int fd, i, rp=1000000000; char *s; fd = open("/tmp2/shm", O_RDWR); s = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); for (i = 0; i < rp; i++) { while ('0' == *s) ; *s = '0'; } }
malloc(9)を使う方法とスワップを使う方法はそれぞれ特徴が異なるので、ケースバイケースで適切な方を選択することになる。今回のコードではどちらを選んでもほとんど変わりはない。
Copyright © ITmedia, Inc. All Rights Reserved.