fork 関連のシステムコールのサンプルコード (C 言語)
[履歴] [最終更新] (2018/06/10 14:46:53)
最近の投稿
注目の記事

概要

以下のシステムコールに関する、C 言語のサンプルコードです。

  • fork
  • execv
  • exit
  • wait

子プロセスを fork で作成して execve でプログラムを実行

以下のプログラムでは、シェルで echo コマンドを実行したときと同じように、シェルに相当する親プロセスで fork で子プロセスを生成して、子プロセス内で execve によって echo プログラムを実行しています。外部プログラムを実行するためではなく、サーバプログラムの worker のように、複数のクライアントにサービスを行う目的で fork することもあり、必ずしも execve は実行しません。

main.c

#include <unistd.h> // fork(), execve()
#include <stdio.h>

int main() {

    // `man 2 fork` で確認できる、fork() の返り値を格納
    pid_t pid;

    // execve() の引数
    char *argv[3];
    extern char **environ; // プロセスの環境変数

    if((pid = fork()) < 0) { // 失敗時は -1 が返る
        perror("fork failed");
        return 1;
    }
    else if(pid == 0) { // 子プロセスでは pid は 0
        printf("ok from child\n");

        // execve() で実行するプログラムの引数を設定
        argv[0] = "echo"; // 実行プログラム名
        argv[1] = "message from echo program"; // 実行プログラムの引数
        argv[2] = NULL; // 実行プログラムの引数は NULL 終端する必要があります
        execve("/bin/echo", // 実行プログラムへの絶対パス (バイナリまたは実行可能なスクリプトファイル)
               argv,
               environ  // これも NULL 終端されています
               );

        // execve() に成功すると、プロセスID 等は変わらずに実行プログラムに処理が置き換わるため
        // 以下の処理は実行されません。

        _exit(1); // execve() に失敗するとここに到達
    }

    // 親プロセスの場合は、子プロセスの pid > 0
    printf("parent: child process pid = %d\n", pid);
    return 0;
}

実行例

$ gcc -Wall -O2 main.c && ./a.out
parent: child process pid = 12051
ok from child
message from echo program

environ について

外部リンケージをもつグローバル変数 environ にはプロセスの環境変数が格納されています。

extern char **environ; // プロセスの環境変数

int i = 0;
while(environ[i] != NULL) {
    printf("%s\n", environ[i++]);
}

main 関数の第三の引数にも同じ値が格納されているため、これを利用することもできます。

int main(int argc, char **argv, char **envp) {}
int main(int argc, char *argv[], char *envp[]) {}

execve 関連のライブラリ関数について

execve を内部的に利用する類似のライブラリ関数が存在します。

execve("/bin/echo", argv, environ);

e が名称に含まれない場合は環境変数は指定できず、現在のプロセスのものが引き継がれます。

execv("/bin/echo", argv);

l が名称に含まれている場合は、argv に相当する情報を直接引数として指定できます。

execl("/bin/echo", "echo", "message from echo program", NULL);

p が名称に含まれている場合は、実行プログラムを環境変数 $PATH から探すため、絶対パスを指定する必要がなくなります。

execvp("echo", argv);

v(e|p)l(e|p) で合わせて 6 種類存在することになります。

execle("/bin/echo", "echo", "message from echo program", NULL, environ);
execlp("echo", "echo", "message from echo program", NULL);

exit で返した終了ステータスを wait で受け取る

man 2 _exit で確認できるシステムコール exit でプログラムを終了すると、親プロセスは wait システムコールによって終了ステータスを受け取ることができます。親プロセスがシェルの場合は、特殊変数 $? で確認できる値です。子プロセスよりも先に親プロセスが終了した場合は、プロセスID 1 の init プロセスが子プロセスの親となり、終了ステータスを受け取ります。C ライブラリ関数の exit()_exit() を内部的に利用する関数で、親プロセスから引き継いだバッファを flush して出力する等の処理も行うため、親と子で重複して出力してしまう問題等を回避するためには _exit() を利用します。

main.c

#include <sys/wait.h> // wait()
#include <unistd.h>
#include <stdio.h>

int main() {
    pid_t pid;

    // exit 終了ステータスの情報を格納するための変数
    int status;

    if((pid = fork()) < 0) {
        perror("fork failed");
        return 1;
    }
    else if(pid == 0) {
        _exit(16); // 子プロセスをステータス 16 で終了
    }

    // wait() によって、親プロセスは子プロセスが終了するまで待機
    if((pid = wait(&status)) < 0) {
        perror("wait failed");
        return 1;
    }

    // wait() から取得できる status はそのままでは利用できません
    printf("status = %d\n", status);

    // マクロ関数 WIFEXITED() と WEXITSTATUS() で終了ステータスに変換します
    // シグナル等で終了した場合は偽、exit で終了した場合は真になります
    if(WIFEXITED(status)) {
        // exit で終了した場合の終了ステータスを取得します
        printf("pid = %d, exit status = %d\n", pid, WEXITSTATUS(status));
    }
    return 0;
}

実行例

$ gcc -Wall -O2 main.c && ./a.out
status = 4096
pid = 54472, exit status = 16

pid を指定して待ちたい場合

wait(&status) は、一つの子プロセスが終了するまで待機しますが、waitpid(pid, &status, 0) を利用すると、指定した子プロセスが終了するまで待機できます。第三引数を利用しない場合は 0 を指定します。待つ対象となるプロセスID を指定するためには引数の pid に正の値を指定します。自分と同じプロセスグループの子プロセスを待つためには 0 を指定します。

子プロセスが利用したリソース情報を取得したい場合

wait3() または wait4() を利用します。

struct rusage usage;

wait3(&status, 0, &usage)
wait4(pid, &status, 0, &usage); // pid を指定したい場合

// リソース例: ユーザ時間、システム時間 (実時間ではない)
usage.ru_utime.tv_sec // long 秒
usage.ru_utime.tv_usec // long マイクロ秒
usage.ru_stime.tv_sec
usage.ru_stime.tv_usec
関連ページ