Linuxのメモリまわり

ちょっと前から groonga を使うとプロセスサイズが肥大化するのが気になっていて、メモリ関係を色々調べていたのですが、そこでわかったことなどを書いときます。

malloc() しただけではOSのメモリは使用されない

メモリを1GB獲得するだけのこんなプログラムを作って実行してみます。

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
  char *p;
  char buf[1024];
  int i;
  p = malloc(1024*1024*1024);
  gets(buf);
  for (i=0; i<1024*1024; i++)
    memcpy(p+i*1024, buf, sizeof(buf));
  pause();
}

実行前:

% free
             total       used       free     shared    buffers     cached
Mem:       4043524    2207776    1835748          0     139028    1603780
-/+ buffers/cache:     464968    3578556
Swap:      1951860          0    1951860

実行中:

% free
             total       used       free     shared    buffers     cached
Mem:       4043524    2207804    1835720          0     139044    1603348
-/+ buffers/cache:     465412    3578112
Swap:      1951860          0    1951860

% ps up 3316         
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
tommy     3316  0.0  0.0 1050268  248 pts/2    S+   23:14   0:00 ./malloc

プロセスサイズが 1GB 超えてるのに OS の空きメモリはほとんど変化ありません。

プログラムを実行した端末でRETURNキーを押すと獲得したメモリに書き込みを行います。
その後の状態:

% free
             total       used       free     shared    buffers     cached
Mem:       4043524    3258232     785292          0     139044    1603296
-/+ buffers/cache:    1515892    2527632
Swap:      1951860          0    1951860

% ps up 3316
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
tommy     3316  8.4 25.9 1050268 1048852 pts/2 S+   23:14   0:01 ./malloc

メモリ使用量が増えました。

Linux で使用するまで実際にメモリが獲得されないのは memory overcommit という仕組みらしいです。OS の仮想メモリを超える分のメモリを malloc() で獲得することも可能のようです。

malloc() した領域をいざ使用しようとした時に、仮想メモリが不足していてメモリ獲得できなかった場合はどうなるかというと、なにかしらのプロセスが SIGKILL で殺されるようです(OOM Killer)。

これはちょっとひどい仕組みのような気がします。Solaris はたしか仮想メモリを超えるメモリの獲得は失敗すると思いました(うろ覚え)。

Linux 2.6 では sysctl で vm.overcommit_memory を変更すればこの振る舞いを変更できるようです。

mmap() もOSのメモリは使用されない

mmap() でファイルをプロセスのメモリにマッピングしてみます。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
  char *p;
  char buf[1024];
  int i;
  int fd;
  fd = open("/tmp/1GB", O_RDWR);
  p = mmap(0, 1024*1024*1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
  gets(buf);
  for (i=0; i<1024*1024; i++)
    memcpy(p+i*1024, buf, sizeof(buf));
  pause();
}

実行前:

% free
             total       used       free     shared    buffers     cached
Mem:       4043524    2237048    1806476          0     148092    1613264
-/+ buffers/cache:     475692    3567832
Swap:      1951860          0    1951860

実行中:

% free
             total       used       free     shared    buffers     cached
Mem:       4043524    2237136    1806388          0     148100    1613016
-/+ buffers/cache:     476020    3567504
Swap:      1951860          0    1951860

% ps up 3435
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
tommy     3435  0.0  0.0 1050264  248 pts/2    S+   00:30   0:00 ./mmap

malloc() 時と同じく、プロセスサイズは増えましたが、OSの使用メモリは増えません。

書き込み後:

% free
             total       used       free     shared    buffers     cached
Mem:       4043524    2238348    1805176          0     148184    1611888
-/+ buffers/cache:     478276    3565248
Swap:      1951860          0    1951860

% ps up 3435
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
tommy     3435  3.0 25.9 1050264 1048844 pts/2 S+   00:30   0:01 ./mmap

書き込み後も OS の使用メモリは増えてません。mmap() はファイルをメモリにマッピングするので、メモリに書き込んでもファイルに書き込むことになるからだと思います。

procfsで確認

malloc() と mmap() のどちらによってプロセスのメモリサイズが大きくなっているかは、/proc//status でわかるようです。

malloc()の場合:

% grep ^Vm /proc/3463/status
VmPeak:	 1050268 kB
VmSize:	 1050268 kB
VmLck:	       0 kB
VmHWM:	     248 kB
VmRSS:	     248 kB
VmData:	 1048612 kB
VmStk:	     136 kB
VmExe:	       4 kB
VmLib:	    1484 kB
VmPTE:	      20 kB
VmSwap:	       0 kB

mmap()の場合:

% grep ^Vm /proc/3455/status
VmPeak:	 1050264 kB
VmSize:	 1050264 kB
VmLck:	       0 kB
VmHWM:	     248 kB
VmRSS:	     248 kB
VmData:	      32 kB
VmStk:	     136 kB
VmExe:	       4 kB
VmLib:	    1484 kB
VmPTE:	      16 kB
VmSwap:	       0 kB

malloc() で獲得したメモリは VmData に表れますが、mmap() の場合は VmData は小さいままです。

groongaのプロセスサイズが大きくても気にするな

で、groonga ですが Twitter で @tasukuchan に教えてもらいました。

ファイルをたくさんmmapしているので、VIRTは増えちゃいますね。リークっぽく見えちゃうのも分かります。64bit専用としているのも、そのためです。

http://twitter.com/tasukuchan/status/27121532855

ということで groonga のプロセスサイズが大きいのは気にしなくても良さそうです。

実は groonga が無駄にメモリを消費しているのではないかと疑っていたのでした。すいません…。

mmap() のことはすっかり頭から抜けてました。最近は Ruby ばっかりで C でプログラム書くことはほとんどないので、だんだんダメになっていってるようです…。