10330


**[頂いた回答・コメント、その後の考察によって得た結論を自己回答として投稿しました。]**  
_ターゲットとなるディストリビューション: 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()` 以外のエラー処理は端折っています):

[code]

    #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;
    }
    
[/code]

## 質問

この場合の対処はどうしたらよいでしょうか。  
以下の 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()` 以外のエラー処理や通知手法・内容は手抜き):

[code]

    #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;
    }
    
[/code]


CC BY-SA 3.0
2015-05-07T17:39:52.347
9920
2015-06-13T02:27:49.090
2015-05-18T15:22:59.610
7831
7831
question
linux c
大量のメモリを使用するプログラムからコマンドを実行する方法


その子プロセスと親プロセスとの関係がよくわからないので単に提案ですが

# ４の変形

apache httpd がやっているように  
最初に起動するプロセス P は「子プロセス起動専門デーモン」扱いとする（大量メモリ確保はしない）  
ってのはどうでしょう。

P が `fork` でも `spawn` でもよいから自己クローン Q を起動し、 Q は大量メモリを使う  
Q が子プロセス R を起動したくなったら P に依頼する  
P は消費メモリ量が小さいので成功するはず

子プロセス R の `ppid` が Q でなければならない、のであれば使えませんけど。


CC BY-SA 3.0
2015-05-07T23:07:44.567
9922
2015-05-07T23:07:44.567
8589
9920
answer


mattnさんがコメントされているように、今時は `posix_spawn(3)`
を使うのがスマートなのかなと思いますが、どのように実装されているのか詳しくないため、`vfork(2)` についての回答です。

> **2. fork() の代わりに vfork() を使う。**
>
> fork() の代わりに、後に exec系() することが前提の vfork() に置き換え、 exec系() 失敗時の exit() を
> _exit() に置き換えるだけ。  
>  CentOS 6.2 および、Gentoo (kernel: 3.17.8, glibc: 2.19) でそれっぽく動いていることを確認しました。  
>  ただし以下の懸念があります。
>
>   * これでうまくいく確証が見付かっていない。
>   * 元々過渡的な API であり、POSIX.1-2001 では廃止予定、POSIX.1-2008
> では実際に廃止されているので、使うのが躊躇われる。
>

ターゲットが Linux である前提なのであれば「`vfork(2)`
が親プロセスのメモリ空間をコピーせずに共有する」という動作は十分期待して良いのではないかと思います。`vfork(2)`
のマニュアルにも以下のように記載されています。

> **vfork() の呼び出しは、以下の flags を指定して clone(2) を呼び出す のと等価である。**
>
> CLONE_VM | CLONE_VFORK | SIGCHLD

`clone(2)` のマニュアルに記載されている `CLONE_VM` フラグの説明は以下の通りです。

> **CLONE_VM (Linux 2.0 以降)**
>
> CLONE_VM が設定された場合、呼び出し元のプロセスと子プロセスは同じメモリー空間で
> 実行される。特に、呼び出し元のプロセスや子プロセスの一方がメモリーに 書き込んだ内容はもう一方のプロセスからも見ることができる。さらに、
> 子プロセスや呼び出し元のプロセスの一方が mmap(2) や munmap(2) を使ってメモリーをマップしたりアンマップした場合、
> もう一方のプロセスにも影響が及ぶ。
>
> CLONE_VM が設定されていない場合、子プロセスは clone() が実行された時点での、親プロセスのメモリー空間をコピーした
> 別のメモリー空間で実行される。 一方のプロセスが行ったメモリーへの書き込みや ファイルのマップ/アンマップは、 fork(2)
> の場合と同様、もう一方のプロセスには影響しない。

求められている `確証` の度合いにもよりますが、マニュアルに記載されている上記の内容から「（Linux における）`vfork(2)`
は親プロセスのメモリ空間をコピーしない」と解釈するのが妥当だと思います。

また、そもそも `vfork(2)`
が既にレガシーなシステムコールであること（ある日突然、システムコールが廃止されても文句言えない）が懸念材料であるのなら、`clone(2)` を用いて
`vfork(2)` 相当の処理を実装するのも１つの手かもしれません。（それが筋が良いかどうかは別として..）

（`posix_spawn(3)` もライブラリレベルで `vfork(2)`
を使って実装されているなら、同様の解釈で良いのではないかと思います。Linux から `vfork(2)` が消えてなくなったら直接 clone(2)
を使った実装になるんじゃないかなぁと勝手に想像しています）


CC BY-SA 3.0
2015-05-08T06:38:17.323
9940
2015-05-08T06:38:17.323
1024
9920
answer


Linuxカーネル開発者として申し上げれば、Linuxでvfork()を廃止することはないので、clone()を直接使うのはやめてvfork()使ってください。


CC BY-SA 3.0
2015-05-16T22:03:59.027
10268
2015-05-16T22:03:59.027
9761
9920
answer


akrさんの日記からもリンクがありますが、[NetBSD ドキュメンテーション: なぜ伝統的な
vfork()を実装したのか](http://www.jp.netbsd.org/ja/docs/kernel/vfork.html)が理由としてはよくまとまっていますね。


CC BY-SA 3.0
2015-05-17T09:13:30.050
10286
2015-05-17T09:13:30.050
2379
9920
answer


頂いた回答・コメントや、その後調べた内容を元にして考察した結果、以下のふたつが推奨される方法であるという結論になりました。人によっては推奨する内容が異なるかもしれません。

## 推奨1: `posix_spawn()` / `posix_spawnp()` を使う。

### 長所:

この関数が目的に直接対応するため呼び出し方が素直かつ簡潔。  
他の手段では、最低限、プロセス生成+`exec`といった二段構えの実装が必要な上、シグナルや suid / sgid
も含めて真っ当に処理しようとすると、さらに面倒なことになる。

### 短所:

他の大半の方法も同様だが、強いて挙げるなら、`FD_CLOEXEC`がセットされていないディスクリプタが開きっぱなしになること。  
Linux 版ではなく、 [Darwin
版](https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man3/posix_spawnattr_setflags.3.html)では引数
`attrp` に `POSIX_SPAWN_CLOEXEC_DEFAULT` を設定することで、引数 `file_actions`
に設定されなかったディスクリプタを閉じることができるらしい。

### その他:

質問時には、`posix_spawn()`のドキュメントに、`fork()`のように親プロセスのメモリ使用量に比例する空きメモリを要求することがないという記述がどこにもないことが不安材料だったが、以下のように捉えることで問題ないと判断。(合ってる？)  
`posix_spawn()` の目的は新しいプログラムを実行することであり、その実現手段については言及されていない。  
そのため、仮に、`fork()`
と同様の空きメモリに関する問題発生し得るのならば、その旨が仕様に記述されるべきで、現実には何も言及がない以上、`fork()`のような問題は発生しない、と解釈すべき。

## 推奨2: プログラムの先頭付近で、プロセス起動専用プロセスを立ち上げる。

質問文の方法4や、回答にあった4の変形版です。

### 長所:

シグナルマスクやファイルディスクリプタの問題が起こりにくい。

### 短所:

やや煩雑。

### その他:

メインの処理をプロセス起動専用プロセスの親するか子にするかは設計・用途次第。

推奨1と2の合わせ技もありかもしれない。(プロセス起動専用プロセスから`posix_spawn()`)

* * *

以降の方法は、上記のいずれかで同等・あるいはそれ以上の効果を挙げられるか、煩雑さが目立つため次点としました。

## 次点1: 巨大メモリを共有メモリとして確保しておく。

共有メモリを確保する手間があるため煩雑になりがち。  
ただ、この方法だと、`fork()`でも問題なく成功してくれる。

## 次点2: `vfork()` を使う。

`vfork()` がいつ消えるかとびくびくしていたが、 kosaki さんの回答によると、Linux では、まず消えることがないとのこと。  
また、[akrさんの日記](http://www.a-k-r.org/d/2014-09.html#a2014_09_06)に、`vfork()`に関する様々な考察がある。

## 次点3: `clone()` を使う。

あまりお勧めできない。(OSやバージョンに依存しすぎるため？)

## 次点4 (個人的には論外): `sysctl vm.overcommit_memory=1`

`fork()` を成功させるためだけにここまでするのは影響範囲広すぎ。


CC BY-SA 3.0
2015-05-18T15:20:45.650
10330
2015-05-18T15:20:45.650
7831
9920
answer


次点1および推奨2について疑問があります。  
マニュアルの記述から、以下のように考えました。

fork()のマニュアルには、以下の記述があります。

> Linuxでは、fork()を書き込み時コピー(copy-on-write)ページを用いて実装している。  
>  したがって、forkを行うことの唯一のデメリットは、親プロセスのページテーブルを複製と  
>  子プロセス自身のタスク構造の作成のための時間とメモリーが必要なことである。

ページテーブルとは、仮想アドレスを実アドレスに変換するときに使用するテーブル、  
タスク構造とは、プロセスの情報を管理するものだと思います。

書き込み時コピーとは、メモリへの書き込みが発生した時点でメモリのコピーを作成し、  
参照のみの領域については、親子で同じ領域を参照する方式のことです。  
このため、fork()の実行時にプロセスの全領域のコピーは必要なく、ページテーブルと  
タスク構造のメモリが必要ということになります。

また、実際にメモリのコピーを作成しなくても親プロセスで使用しているメモリの  
量とマシンに搭載しているor空いているメモリ量から判断してエラーとする可能性を  
考えましたが、fork()のマニュアル中のメモリに関して発生するエラーについて  
記述した部分より無いと判断します。

> EAGAIN  
>  親プロセスのページテーブルのコピーと子プロセスのタスク構造に生成に必要な  
>  メモリーを fork() が割り当てることができなかった。  
>  ENOMEM  
>  メモリーが足りないために、fork()は必要なカーネル構造体を割り当てることが  
>  できなかった。

ここで、「次点1:巨大メモリを共有メモリとして確保しておく」ですが、プロセス内に  
閉じたメモリでも、共有メモリでもメモリサイズが同じであれば、ページテーブルの  
サイズは同程度となり、fork/execする場合の利点は無いと思います。

「推奨2:プログラムの先頭付近で、プロセス起動専用プロセスを立ち上げる」ですが、  
手間の割りに見返りが少ない感じがします。  
プロセスの使用メモリ量が減少すれば、ページテーブルのサイズは小さくなります。  
しかし、ページテーブルのサイズは、プロセスのメモリ量に比べてかなり小さい(仮想  
記憶の利便性を損ねない程度)と思われます。

また、vfork()のマニュアルには下記の記述から分かるように、ページテーブルの  
メモリを必要としません。

> vfork()は clone(2)の特殊な場合である。親プロセスのページテーブルのコピーを  
>  行わずに新しいプロセスを 作成するために使用する。

fork/execするために専用プロセスを立ち上げるのは、煩雑すぎるように思います。


CC BY-SA 3.0
2015-06-13T02:27:49.090
11305
2015-06-13T02:27:49.090
10122
9920
answer