mallocとは

mallocはC言語の標準ライブラリの関数で、メモリの動的確保をするためのものです。

void *malloc(size_t size);

引数sizeで指定されたサイズのメモリ領域を割り当て、その先頭のアドレスを返します。 メモリの割り当てに失敗した場合はNULLを返します。 入門書などで次のような使い方を見たこともあることでしょう。

char *p = (char *) malloc(100);
if (p == NULL) {
   // メモリ確保失敗
}

メモリはどれだけ確保できるのか

ここに、ひたすらmallocを続けるプログラムを用意しました。 (実験プログラムのためfreeの呼び出しは省略しています)

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

#define ONE_GB (1024*1024*1024)

int main(void){
	int total = 0;

	for(;;){
		void *p = malloc(ONE_GB);
		if(p==NULL){
			break;
		}
		total++;
	}

	printf("%d GB allocated", total);

	return 0;
}

ほとんどの環境で、そのPCが搭載しているサイズのメモリよりはるかに大きい領域のメモリが確保できたと思います。 16GBしか搭載していない手元の環境(Ubuntu 18.04)では次のような結果となりました。なかなかの迫力ですね。

131062 GB allocated

過剰に確保したメモリ領域を使うとどうなるか

次に、搭載メモリより多く確保できたメモリ領域に書き込んでみます。 *注意*: このプログラムを実行するとシステムが不安定になることがあります

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

#define ONE_GB   (1024*1024*1024)
#define PAGESIZE (4*1024)
#define MAX_GB   (100)    // もし100GB以上搭載している場合は変更してください

int main(void){
	static char *ptrs[MAX_GB];
	int total = 0;
	int i;

	for(i=0; i<MAX_GB; i++){
		void *p = malloc(ONE_GB);
		if(p==NULL){
			break;
		}
		ptrs[i] = (char *)p;
		total++;
	}

	printf("%d GB allocated", total);
	fgetc(stdin);

	for(i=0; i<total; i++){
		int j;
		for(j=0; j<ONE_GB; j+=PAGESIZE){
			ptrs[i][j] = 0;
		}
	}

	return 0;
}

もうお気づきかもしれませんが、mallocしただけの状態ではシステムのメモリ使用量は増えていません。 そしてメモリ領域に書き込んで初めてシステムのメモリ使用量が増加します。 手元のLinux環境では、物理メモリが足りなくなったあたりでOOM Killerによりこのプログラムは強制終了されました。

このことから、mallocの戻り値のNULLチェックだけではメモリ不足は防げないことがわかります。

なぜ搭載メモリ以上のメモリ領域を確保できたのか

Linuxに限らずほとんどの近代的なOSではメモリのオーバーコミットという機能があり、 実際に存在するメモリ(物理メモリ+スワップ)より大きな領域をプロセスに割り当てることができます。

各プロセスから見えるメモリのアドレスは、物理メモリのアドレスではなく、仮想アドレス空間のものです。 仮想アドレスへの物理メモリの割り当てはOSによって管理されています。

今回のプログラムではmallocが呼ばれたとき、仮想アドレス空間の領域は確保されますが、物理メモリはまだ割り当てられません。 この時点で仮想アドレスは確定するのでmallocは戻り値を返すことができます。 また、物理メモリはまだ使われていないので、メモリ使用量は増えません。 確保したメモリ領域へ書き込んだときにはじめて、物理メモリが割り当てられてメモリ使用量が増加します。

ここまではWindowsやmacOSでも同様の挙動になると思います。 Linuxの場合1は割り当てる物理メモリが足りない場合、OOM Killerがプロセスを掃除することでメモリを確保しようとします。 最終的に今回のプログラムがOOM Killerの対象となり強制終了しました。 つまり、メモリ不足のため確保したはずのメモリが使えませんでした。

まとめ

mallocが成功したとしても、実際に書き込んでみないと本当にメモリが使えるのかわからないという例を紹介しました。 これをプログラム内で検出するのは厄介で、システムのメモリの状態を観察して予想するくらいしかなさそうです。 Linuxの場合はオーバーコミットを無効にもできますが、メモリ効率が極端に悪化するためあまり現実的ではないと思います。

大量のメモリを最初に確保したいような場合は気をつけましょう。

  1. 大抵のLinuxディストリビューションのデフォルトの挙動です。OOM Killerの設定も変えられますし、メモリのオーバーコミットをしない設定もあります。