以下のシステムコールに関する、C 言語のサンプルコードです。
以下のプログラムでは、シェルで 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 にはプロセスの環境変数が格納されています。
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("/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);
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
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