みなさんこんにちは!
今回は fork(), execファミリー, exit(), wait() などの、
UNIXプログラミングの重要なテーマである『プロセス生成に関するシステムコール』について解説していきたいと思います。
プロセス生成に関係するシステムコールの種類
プロセスを生成するために関係するシステムコールにはいくつか種類があるのですが、
それらの中でも特に重要なものは大きく分けで以下の4種類があります。
・プロセスを複製するシステムコール
fork()など
・プロセスの変身をするシステムコール
execute()、execel()など
・プロセスを終了するためのシステムコール
exit()、_exit()など
・子プロセスの終了の待機をするシステムコール
wait()など
これらをうまく使うことでプロセスを自由を操作することができるようになります。
というわけで、
これらのシステムコールの使い方をソースコードと合わせて見ていくことにしましょう。
プロセスの複製をするシステムコール
fork()
プロセスを複製するためにはfork()を使います。
fork()は実行中のプロセスを複製するシステムコールで、自身をコピーして子プロセスを作成します。
たとえばsystem()などの関数は内部でfork()を使っています。
この関数の面白いところは、後ほど詳しく説明しますが、returnを二回することです。
用法は以下の通り。
1 2 3 4 |
#include <unistd.h> pid_t fork(void); |
<引数>
無し
<返り値>
親プロセスでは子プロセスのPIDを返し、子プロセスでは0を返す。エラーの際には−1を返す。
先ほど、
fork()は2度returnをするという説明をしましたが、これは一体どういうことなのでしょうか?
以下のソースコードを実行して挙動を見てみましょう。
1 2 3 4 5 6 7 8 9 |
#include <stdio.h> #include <unistd.h> int main(void) { pid_t pid = fork(); printf("PID: %d \n", pid); } |
このコードを実際にコンパイルして実行してみると、

というような出力になったと思います。
『どうしてprintf()が二回されているんだろう?しかも違う結果で・・・』
と思った方もいるかもしれませんが、これは最初のプロセスがfork()で子プロセスを生成しているからです。
fork()は子プロセスには返り値として0を返すことからも、fork()によってプロセスが複製されていることがわかりますよね。
プロセスの変身をするシステムコール
プロセスの変身をするシステムコールにはexec族というシステムコール族があります。
そもそも、
『どうしてプロセスを変身させるシステムコールなんてものがあるの?』という疑問が浮かび上がるかもしれませんが
これはプロセスに別のコマンドを実行させたいからです。
execファミリー
execファミリーはexecl、execv、execleなどの関数からなる関数族です。
まずはexecが何をする関数なのかを理解するために、execve()の利用法みてみましょう。
1 2 3 4 5 6 7 8 |
#include <unistd.h> int execve( const char *filename, char *const argv[], char *const envp[] ); |
<内容>
引数の”filename”によって指定されたプログラムを実行する関数
<引数>
filename: ファイル名
argv: コマンドの引数
envy: 環境変数のデータ
<返り値>
実行が成功した場合は何もリターンしない、エラー時は-1を返す。
この関数は実行が成功した場合には何もリターンしないのですが、
これはexecve()を実行した自身のプロセスが変身してしまうためです。
例えばexec関数を使って”ls”を実行するa.outを実行した場合、
プロセス自身は”ls”を実行するプロセスに変身してしまうので、a.outはそれ以降実行されなくなります(=実行が成功した場合には何もリターンしない)
execve()の利用法を理解したところで、次はexecファミリー全体を見てみましょう。
引数の渡し方 | 環境変数 | パス検索 | |
execl() | リスト | 引き継ぐ | しない |
execv() | ベクタ | 引き継ぐ | しない |
execle() | リスト | 引数で渡す | しない |
execve() | ベクタ | 引数で渡す | しない |
execlp() | リスト | 引き継ぐ | する |
execvp() | ベクタ | 引き継ぐ | する |
execについて詳しく知りたい方は以下のようなサイトなども参考にしてみてください。
https://linux.die.net/man/3/execv
execve()を実際に使ったソースコードを見てみましょう。
1 2 3 4 5 6 7 8 9 |
#include <unistd.h> int main(void) { char *argv[] = {"ls", NULL}; char *envp[] = {NULL}; execve("/bin/ls", argv, envp); } |
このコードを実行してみると、
実行したディレクトリでlsコマンドが実行されることがわかると思います。
system()で引数にコマンドを入れるとそのコマンドが実行できるということは、みなさんもご存知かと思いますが、
これはsystem()の内部でexecve()を使って引数で与えられたコマンドを実行しているからです。
プロセスを終了するためのシステムコール
プロセスを終了するためにはexit()という関数を使います。
exit()
これはc言語を書いている人なら誰でも使ったことがあるので、すぐに使い方がイメージできるのではないでしょうか。
exit()の他にも_exit()という関数があるのですが、どのような違いがあるのかというと、
・exit()はatexit()で登録した関数を呼び出すが、_exit()は呼び出さない。
・exit()は標準入出力の内容をフラッシュするが、_exit()はフラッシュしない。
・exit()は内部で_exit()を実行している。
atexit()はプログラムが正常終了した際に呼び出す関数を登録する関数です。
また、終了したプロセスはゾンビプロセスというものになります。(のちほど説明するwait()でゾンビプロセスを処理します。)
子プロセスの終了を待機するシステムコール
子プロセスの終了を待機するシステムコールにはwait()関数があります。
wait()
wait()はexit()を実行してゾンビプロセスになった子プロセスを処理するための関数です。
もし親プロセスがwait()をしなかったら、ゾンビプロセスがどんどん溜まっていき、最終的にはメモリが足りなくなってしまいます。
仮に親プロセスがゾンビプロセスを処理する前に終了してしまった場合は、PID1番のinit(launchd)のプロセスが親プロセスの代わりにwait()してくれます。
さっそくwait()の用法をみてみましょう。
1 2 3 4 |
#include <sys/wait.h> pid_t wait(int *status); |
<引数>
status: 子プロセスの終了ステータスがセットされる。必要ないならNULLを指定。
<返り値>
成功時には子プロセスのPIDが、エラー時には-1が帰ってくる。
実際にwait()を使ってみたソースコードが以下です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main(void) { int status; pid_t pid = fork(); if (pid == 0) { // 子プロセスが実行されている場合 char *argv[] = {"ls", NULL}; char *envp[] = {NULL}; execve("/bin/ls", argv, envp); } else { // 親プロセスが実行 wait(&status); // exitされたかどうかをチェック if (WIFEXITED(status)) { // exit()に渡されたステータスの一部を表示(正常終了なら0を表示) printf("Exit: %d\n", WEXITSTATUS(status)); } } } |
これを実際に実行してみると、lsコマンドを実行した後に”Exit: 0″と表示されていると思います。
まとめ
今回はプロセス生成に関するシステムコールについて解説しましたが、どうだったでしょうか?
fork()やexecファミリー、wait()や、馴染みのあるexit()などについて説明してきましたが
これらのシステムコールを理解することでsystem()の内部や、プログラムが終了したときに、
どのような処理がコンピュータの中で行われているのかを理解することができるようになると思います。
最後まで読んでいただきありがとうございました!