技術的雑談-プロセスの生成と後始末(Linux C++編)
環境
- OS : CentOS 3.5 (kernel 2.4.21-32.EL)
- glibc : 2.3.2-95-33
- gcc : 3.2.3 20030502
ちょっと古いよなぁ…さすがに。(汗)
目的
- Native(C/C++)アプリケーション上でプロセスの生成を行なう。
- 基本的な待ち合わせを行なう。
流れ
C/C++でプロセスの作成、起動、後処理のおおまかな流れは次のようになります。
- fork()を呼び出し、自プロセスを複製する
- 複製したプロセス(子プロセス)でexec*()を呼び出し、実行イメージ(プログラム)を読み込み、実行する
- 子プロセスの実行が完了する。(子プロセスがexit()で終わる)
- 子プロセスがゾンビプロセスとなる
- 親プロセスでwait*()を呼び出し、子プロセスのリターンコードを受け取る。子プロセスのゾンビは開放される。
自プロセスの複製
Linuxでは基本的に「自プロセスを複製する」事でしかプロセスを生成できません。
fork()を実行すると、その時点の自プロセスが複製され、複製されたプロセス(子プロセス)でも同じ場所からプログラムの実行が始まります。
fork()直後はプログラミング的には自分が親プロセスなのか子プロセスなのか判断しなくてはなりません。
それはfork()の戻り値で判断可能です。
- 0 : 子プロセス
- 1〜: 親プロセス(番号は子プロセスのプロセスID(PID))
- -1 : 親プロセス fork()失敗
普通はfork()の後でswitch(){}構文を使い、case 0を子プロセスの処理、case -1をエラー処理、defaultを親プロセス処理にします。
fork()を使うには「#include <sys/types.h>」「#include <unistd.h>」が必要らしい。
新しいプログラムの実行
今では普通fork()を使う目的は、「プログラムから他のプログラムを呼び出して使う」事だと思います。
(同じプログラムを並列処理させたいならfork()するよりもthreadを使うほうが一般的でしょう…^^;)
新しいプログラム(プログラムイメージ)を実行するには、exec*()を呼び出して自プロセスのプログラムイメージを上書きします。
exec()のファミリーにはexecl()、execlp()、execle()などのバリエーションがあり、それぞれイメージの検索方法などに違いがあります。(詳しくはman参照^^;)
例えばexeclp()を使った場合、
execlp("/bin/echo", "echo", "aaa", NULL)
というように使い、
- 第一引数 : プログラムイメージのパス。(exec()ファミリーで「p」の付くメソッドはPATH環境変数を参照して探す。)
- 第二引数 : 伝統的にプログラム名を入れるらしい。
- 第三引数以降 : プログラムのコマンドライン引数
- 最後はNULLポインタ。
と、引数を指定する。
exec()ファミリーで「e」が付くものは、同時に環境変数も与える事ができるらしい。
exec()ファミリーで「v」が付くものは第二引数に可変長の文字列配列へのポインターを与えられるらしい。多分、プログラムで可変長個数の引数を生成して渡す時に使うらしい。(「v」がつかない場合はプログラム的に固定個数の引数しか渡せない。)
exec()を呼ぶとそのプログラムの次の行には制御が行かず、新しいプログラムイメージの開始ポイントから処理が始まる。
逆に、exec()が戻ってきた時は何かエラーがあった時で、-1が戻り値として戻り、大域変数のerrnoにエラーコードが入る。
(ソースの先頭の方で「#include <errno.h>」しておく必要がある。)
それと併せて、exec()を使うには「#include <unistd.h>」が必要らしい。
子プロセスの終了とゾンビプロセス
Linuxでは、実行の終わったプロセスは「ゾンビプロセス」となる。
psコマンドなどで見ると
[abcd<defunct>]
などと書かれているものである。
この状態のときに実際にメモリーを消費しているかはわからないが、少なくともPID1個を消費し、exit()で与えられた生前の戻り値を保持している。
子プロセスは以下の条件で消滅する。
- 親プロセスが終了する。
- 親プロセスでwait*()が呼ばれ、戻り値が回収される。
逆に言うと、それを行なわない限りはゾンビは残り続ける。
遺言を聞いてあげないと成仏しないのである。
遺言は子プロセスがexit()で自然死しようと、killで殺害されようと必ず残る。
つまり、子プロセスをkillのSIGKILLで殺しても遺言を拾ってあげない限りはゾンビができる。
(私は「SIGKILLで殺せば跡形もなく消滅する」と思い込んでいて失敗した…orz)
(Unix系のOSではPID=1には「init」という特殊な(?)プログラムが必ずいて、親のいなくなったゾンビの親PIDは「1」に付け直される。initは自分の子プロセスを監視し、ゾンビになっていたら成仏させる。)
子プロセスの戻り値の収集
子プロセスの遺言を聞いてあげるには普通はwait*()を使う。
wait()そのものは「子プロセス全部」の面倒を見てしまって使い勝手が悪いので、kernel 2.4以降は「waitpid()」で子PIDを指定して監視するのが便利。
基本的にwait*()は監視対象の子プロセスが臨終するまで親プロセスの実行を停止するが、WNOHANGオプションをつけることにより、wait*()で止まらずに次に進めることができる。
また、マスクしていない場合は子が死ぬと「SIGCHILD」というシグナルが飛んでくる。
この訃報をハンドルするシグナルハンドラを書いてあげれば子が死んだ時に遺言を拾う割り込みルーチンを作る事ができ、その分親は動く事ができる。
(でもシグナルハンドラを使った実装はまだやったことがない…。)
waitpid()で子PIDを指定し、さらにWNOHANGをつけた場合は、waitpid()の戻り値は以下のようになる。
- 0 : 指定した子はまだ死んでない
- -1 : エラーが起きた。(大域変数errnoに理由が書いてある。大体はwaitpid()の呼び出しパラメータが間違っている。)
- その他 : 死んだ子のPID
なので、特定の子PIDの臨終を待ち続けるにはwhile文で「waitpid()戻り値=目的の子PID」が成立するまでループしてあげればよい。
(但し、安全策としてwaitpid()が-1を返してきた時は逃げるようにしておくべき。)
子プロセスの戻り値の判定方法
子プロセスの遺言を読むには以下のマクロを使用する必要がある。
(そのままだと、子プロセス生成そのもののエラーと子プロセスそのものの戻り値が同じ値の中に入ってしまっている。)
- WIFEXITED() : wait()そのものが失敗した時は0以外となる。
- WEXITSTATUS() : 子プロセスの戻り値を取得する。
子プロセスの戻り値はwaitpid()関数の第2引数にint型変数へのポインタとして与えた変数の中身に入っています。
ポインタとしてNULLを与えると子プロセスの戻り値は破棄されます。
その他
子プロセスでは実行中の変数の値(ヒープ?)はそのまま親から引き継がれるとの事だが、fork()の説明によるとファイルディスクリプタは全てcloseされ、標準入出力と標準エラーが再オープンされる。
参考
Linux JM Project
fork() : http://www.linux.or.jp/JM/html/LDP_man-pages/man2/fork.2.html
execl() : http://www.linux.or.jp/JM/html/LDP_man-pages/man3/execl.3.html
wait() : http://www.linux.or.jp/JM/html/LDP_man-pages/man2/wait.2.html
履歴
2007/2/14 -- 初版
技術的雑談へ戻る