大量のメモリを使用するプログラムからコマンドを実行する方法
[頂いた回答・コメント、その後の考察によって得た結論を自己回答として投稿しました。]
ターゲットとなるディストリビューション: CentOS 6.2 x86-64 版。ただし、他のディストリビューション -- 特に新しめのもの -- についての情報も歓迎です。
背景
Linux において、プログラム中から、何か別コマンドを実行したい場合、以下のいずれかの方法がよく使われると思います。
fork()
+exec系()
+waitpid()
(その場で完了待ちしたい場合)fork()
+exec系()
。SIGCHILD を受けてwait系()
(親と並列に実行させたい場合)system()
※ その場で完了待ちしたい場合と、親と並列に実行させたい場合の2通りを挙げましたが、今回必要としているのは前者。とはいえ、後者の場合でも問題は共通なので列挙しました。
ところが、大量にメモリを使用するプログラムの場合、 fork()
呼び出し時に、親プロセスが現在使用しているのと同じだけの空きメモリがなければ、 ENOMEM
で失敗することがあります (sysctl vm.overcommit_memory
= 0 または 2 の場合)。
system()
を使う場合でも、内部で fork()
(あるいは clone()
あたり) を行っているため、事情は同じです。
サンプル (fork()
以外のエラー処理は端折っています):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define ALLOC_SIZE (10ull << 30) // 空きメモリの半分以上を占める程度の大きさ
int main(int argc, char *argv[])
{
void *p = malloc(ALLOC_SIZE);
memset(p, 0x55, ALLOC_SIZE); // 実ページが確保されるよう、何か書き込む。
pid_t pid = fork();
if(pid == -1)
{
perror("fork()"); // ALLOC_SIZE が十分大きいと、 ENOMEM でここに到達。
return 1;
}
else if(pid == 0)
{
execlp("echo", "echo", "Hello!", (char *)NULL);
exit(1);
}
else
{
// この例ではその場で完了待ち
int status;
waitpid(pid, &status, 0);
printf("status = %d\n", status);
}
return 0;
}
質問
この場合の対処はどうしたらよいでしょうか。
以下の 4 種類の方法を思い付き、今のところ 4 番目を採用しようと思っていますが、この判断に自信がありません。
なお、メモリを大量に使用せざるを得ない事情があるため、今回、メモリ使用量を抑えるという選択肢はありません。
1. sysctl vm.overcommit_memory=1
一番の手抜き方法。プログラムは一切変更する必要がありません。
ただし、システム全体に影響し、本当にメモリが足りない時でも構わず成功してしまい、OOM Killer が走る致命的な事態となるため、できれば使いたくない方法です。
2. fork()
の代わりに vfork()
を使う。
fork()
の代わりに、後に exec系()
することが前提の vfork()
に置き換え、 exec系()
失敗時の exit()
を _exit()
に置き換えるだけ。
CentOS 6.2 および、Gentoo (kernel: 3.17.8, glibc: 2.19) でそれっぽく動いていることを確認しました。
ただし以下の懸念があります。
vfork()
が、fork()
のように、親プロセスが使用しているだけのメモリを必要とすることがないという確証が見付かっていない。- 元々過渡的な API であり、POSIX.1-2001 では廃止予定、POSIX.1-2008 では実際に廃止されているので、使うのが躊躇われる。
3. posix_spawn()
/ poisx_spawnp()
を使う。
これも別コマンド実行に特化した関数なので、このような問題をうまく捌けることが期待され、vfork()
と同様に、それっぽく動いていることも確認できました (現状、内部的に vfork()
を使っているようです)。
しかし、親プロセスが使用しているだけのメモリを必要とすることがないという確証が取れていないのも vfork()
と同様です。
4. コマンド実行用子プロセスを fork()
しておく。
今回思い付いた中では最も確実。ただしやや面倒。
大量のメモリの確保を行う前に、予め、親プロセスとパイプなどで通信できるようにした子プロセスを fork()
しておきます。
その子プロセスは、親からコマンド実行要求があると、そこから、system()
なり、fork()
+ exec系()
+ waitpid()
なりでコマンドを実行します。
この方法は、今回は問題にしていませんが、 FD_CLOEXEC
を設定していない開きっぱなしのファイルがある場合についての問題も同時に回避できるという長所があります。
4 のサンプル (fork()
以外のエラー処理や通知手法・内容は手抜き):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
// コマンド実行用子プロセスのメイン処理
_Noreturn void spawn_loop(int in_pipe, int out_pipe)
{
for(;;)
{
char c;
ssize_t read_size = read(in_pipe, &c, 1); // 親からのリクエスト待ち
if(read_size <= 0)
{
break;
}
pid_t pid = fork();
if(pid == -1)
{
perror("fork()");
}
else if(pid == 0)
{
// 子プロセス (大本から見ると孫プロセス) でコマンド実行。
execlp("echo", "echo", "Hello!", (char *)NULL);
exit(1);
}
else
{
int status;
waitpid(pid, &status, 0);
printf("status = %d\n", status);
}
write(out_pipe, &c, 1); // 完了通知
}
exit(0);
}
// pid 子プロセスのPIDが格納される。
// in_pipe コマンド実行の完了を検知するディスクリプタ。
// コマンド実行が完了すると何か1バイト書き込まれる。
// out_pipe 子プロセスへコマンド実行要求を書き込むディスクリプタ。
// 何か1バイト書き込むと子プロセスがコマンドを実行する。
void create_spawn_child(pid_t *restrict pid, int *restrict in_pipe, int *restrict out_pipe)
{
int p2c_pipe[2]; // 親 -> 子方向のパイプ
int c2p_pipe[2]; // 子 -> 親方向のパイプ
pipe(p2c_pipe);
pipe(c2p_pipe);
*pid = fork();
if(*pid == -1)
{
perror("create_spawn_child()");
exit(1);
}
else if(*pid == 0)
{
close(p2c_pipe[1]);
close(c2p_pipe[0]);
fcntl(p2c_pipe[0], F_SETFD, FD_CLOEXEC);
fcntl(c2p_pipe[1], F_SETFD, FD_CLOEXEC);
spawn_loop(p2c_pipe[0], c2p_pipe[1]);
}
else
{
close(p2c_pipe[0]);
close(c2p_pipe[1]);
*in_pipe = c2p_pipe[0];
*out_pipe = p2c_pipe[1];
}
}
// コマンド実行用子プロセスの終了
void end_spawn_child(pid_t pid, int in_pipe, int out_pipe)
{
close(in_pipe);
close(out_pipe);
waitpid(pid, NULL, 0);
}
#define ALLOC_SIZE (10ull << 30) // 空きメモリの半分以上を占めるだけの大きさ
int main(int argc, char *argv[])
{
// 先に子プロセスを作っておいてから...
pid_t pid;
int in_pipe, out_pipe;
create_spawn_child(&pid, &in_pipe, &out_pipe);
// ...巨大メモリ確保。
void *p = malloc(ALLOC_SIZE);
memset(p, 0x55, ALLOC_SIZE); // 実ページが確保されるよう、何か書き込む。
// 実行してみる。
char c = 1; // この例では、値に特に意味はない。
write(out_pipe, &c, 1); // 実行を要求して
read(in_pipe, &c, 1); // 完了を待つ。
end_spawn_child(pid, in_pipe, out_pipe);
return 0;
}