プロセスとはオペレーティングシステム上で実行されるプログラムのことです。プロセスはプログラム (機械語) とその状態 (現在の実行位置、変数の値、関数呼び出しスタック、開いているファイルディスクリプタなど) からなります。
この章では新しいプロセスを作ったり新しいプログラムを実行したりするための Unix のシステムコールを紹介します。
システムコール fork はプロセスを作成します。
fork
を呼び出した 親プロセス のほぼ完璧な複製である 子プロセス が新しく作られます。二つのプログラムは同じプログラムを同じ実行位置 (fork
から返った位置) から実行します。このとき全ての変数は同じ値を持ち、スタックは同一で、開かれているファイルディスクリプタも同じです。二つのプロセスを区別する唯一のものは fork
の返り値です。子プロセスでは fork
は 0 を返し、 親プロセスでは 0 でない整数を返します。fork
の返り値を確認することで、プログラムは自分が親なのか子なのかを確認してそれによって動作を変えることができます。
fork
によって親プロセスに返される 0 でない整数は子プロセスの プロセス ID です。カーネルはプロセス IDを使ってプロセスを一意に識別します。プロセスは getpid 関数を呼ぶことでプロセス ID を取得できます。
子プロセスは親プロセスと同じ状態 (同じ変数の値、同じファイルディスクリプタ) に初期化されます。この状態は親と子で共有されるのではなく、 fork
が呼ばれたときにコピーされます。例えば fork
の前に定義した参照変数があった場合、 fork
の後には親と子プロセスはこの参照を互いに影響を及ぼすこと無く独立に変更できます。
同様にファイルディスクリプタも fork
が呼ばれたときにコピーされます。そのため一方を閉じたとしてももう一方は開いたままです。ただし二つのディスクリプタは (システムメモリにある) ファイルテーブル内の同じエントリを指すので、入出力の現在位置を共有します。親と子のどちらかが読み込みを行った場合、その次に読み込むのがどちらであっても読み込み位置は変化します。また lseek
による入出力位置の変更はもう一方のプロセスにすぐに伝わります。
leave hhmm
コマンドは時刻 hhmm
に利用を終える時間だとユーザに報告するバックグラウンドプロセスをフォークしてすぐに終了します。このコマンドを作成します。
プログラムは最初にコマンドラインをパースして時刻を取得し、報告するまでの秒数を計算します (8 行目) 。time 関数はエポック (1970 年 1 月 1 日 午前 0 時 0 分 0 秒) から現在時刻までの経過秒数を返します。 localtime を使うとこの値から年、月、日、時、分、秒を計算することができます。プログラムはその後 fork
で新しいプロセスを作ります。親プロセス (fork
の返り値が 0 でないプロセス) はすぐに終了するため、leave
を起動したシェルの制御はすぐにユーザに戻ります。子プロセス (fork
の返り値が 0 のプロセス) の実行は続き、sleep
を呼んで指定された時間まで待ってからメッセージを表示して終了します。
システムコール wait
は fork
によって作られた子プロセスの一つが終了するまで待ち、そのプロセスがどのように終了したかについての情報を返します。これは親子間の同期メカニズムであり、子から親へのとても原始的な形のコミュニケーションです。
基礎となるシステムコールは waitpid であり、 wait ()
という呼び出しは waitpid [] (-1)
の短縮形に過ぎません。
waitpid [] p
の動作は p
の値によって異なります。
p
> 0 ならば、プロセス ID が p
である子プロセスの終了を待つ。
p
= 0 ならば、同じグループ ID を持つ任意の子プロセスの終了を待つ。
p
= −1 ならば、任意の子プロセスの終了を待つ。
p
<−1 ならば、グループ ID が -p
である子プロセスの終了を待つ。
返り値の最初の要素は wait
によって終了を捕捉された子プロセスのプロセス ID です。2番目の要素は以下に示す process_status 型の値です:
WEXITED r | 子プロセスは exit が呼ばれるかプログラムの終端に達することによって通常の方法で終了した。r はリターンコード (exit の引数) を表す。 |
WSIGNALED s | 子プロセスはシグナル (ctrl+C, kill など。 4 章参照) によって終了した。s がシグナルの種類を表す。 |
WSTOPPED s | 子プロセスはシグナル s によって停止された。これが起こるのはあるプロセス (典型的にはデバッガ) が他のプロセスの実行を (ptrace を使って) モニターしているという特殊なケースに限られる。
|
子プロセスの一つが wait
を呼んだ時点ですでに終了していた場合は呼び出しはすぐに返ります。そうでなければ親プロセスは子プロセスのどれかが終了するまでブロックします (“ランデブー” と呼ばれる動作です)。この子プロセスの終了を待つには wait
を n 回呼ぶ必要があります。
waitpid
関数は二つのオプショナルなフラグを第一引数に受け取ります。一つ目の WNOHANG
フラグは終了していない子プロセスが無い場合に待たないことを指示します。子プロセスが無かった場合の返り値は第一要素が 0 で第二要素は未定義です。もう一つの WUNTRACED
フラグは sigstop
シグナルによって停止させられた子プロセスを返すことを指示します。waitpid
は p
に該当する子プロセスがないとき(あるいは p
が -1
で現在のプロセスが子プロセスを持たないとき)には例外を出します。
以下の fork_search
関数は二つのプロセスを使って線形探索を行います。線形探索には simple_search
関数を使っています。
fork
された子プロセスはテーブルの上半分を探索し、 cond
を満たす要素を見つけた場合は 1 を、それ以外の場合は 0 をリターンコードとして終了します ( 16 行目と 17 行目)。親プロセスはテーブルの下半分を探索し、 wait
を呼んで子プロセスと同期します ( 21 行目と 22行目)。 子プロセスが通常の方法で終了した場合、そのリターンコードとテーブルの下半分の探索結果を組み合わせます。そうでなければエラーが起こっているので、 fork_search
関数は失敗します。
wait
はプロセス間の同期を行いますが、それ以外に子プロセスが持つリソースの完全な開放も行います。終了した子プロセスは “ゾンビ” 状態となり大部分のリソース (メモリなど) が開放されますが、子プロセスは wait
を呼んだ親プロセスに返り値を伝える必要があるので、プロセステーブルのスロットには乗ったままです。親プロセスが wait
を呼べば、子プロセスはプロセステーブルからも削除されます。このテーブルの大きさは固定なので、リークを防ぐためにもフォークした全てのプロセスを wait
することが重要です。
親プロセスが子プロセスよりも先に終了した場合、子の親はプロセス ID が 1 のプロセス (通常は init
) に移ります。このプロセスは wait
の無限ループを含むので、子プロセスは終了するとすぐに回収されます。この仕組みによって “ダブルフォーク” という便利なテクニックが使えるようになります。このテクニックは子プロセスの終了をブロックして待つことができないときなどに使われます。
子プロセスは二回目のフォークの後すぐに終了します。これによって孫プロセスは親を失うので、init
の養子となります。この方法ではゾンビプロセスが生まれることはありません。親はフォーク後すぐに wait
を呼んで子を回収します。子はすぐに終了するので、この wait
が長い間ブロックすることはありません。
システムコール execve、 execv、 そして execvp は現在のプロセスでプログラムを起動します。現在のプログラムの実行を止めて新しいプログラムに移るので、エラーの場合を除いてこの呼び出しが返ることはありません。
第一引数は実行するプログラムを含むファイルの名前です。execvp
を使った場合ファイルの名前は (環境変数 PATH
で指定される) 探索パスのディレクトリから探索されます。
第二引数はプログラムを実行するときに渡されるコマンドライン引数の配列です。実行するプログラムの中ではこの配列が Sys.argv
となります。
execve
を使うと第三引数にプログラムが実行される環境を渡すことができます。execv
と execvp
では現在の環境がそのまま使われます。
execve
と execv
、そして execvp
が結果を返すことはありません。エラーが起こること無くプロセスが指定されたプログラムを実行するか、実行ファイルが見つからないなどのエラーが起きて呼び出し元のプログラムに Unix_error
を出すかのどちらかです。
次の三つは同じ動作をします:
受け取った grep
コマンドへの引数に -i
オプション (大文字と小文字を区別しない) を追加して起動するための “ラッパー” コマンドは以下のように書けます:
emacs
コマンドをターミナルのタイプを変えて起動するための “ラッパー” コマンドは以下のように書けます:
exec
を呼んだプロセスは新しいプログラムを実行するプロセスと同じです。そのため新しいプログラムは exec
を呼んだプログラムの実行環境の一部を引き継ぎ、以下に上げるものは同じになります:
次のプログラムは単純なコマンドインタープリターです。標準入力から行入力を読み、単語ごとに区切り、コマンドを起動し、標準入力から EOF を受け取るまでこれを繰り返します。文字列を単語のリストに分割する関数から始めます。このひどい処理についてはどうかノーコメントとさせてください。
次はインタープリターのメイン処理です:
exec_command
関数がコマンドを実行とエラーの対処を行います。リターンコード 255 はコマンドが実行されなかったことを意味します (これは通常の慣習ではありません。リターンコード 255 で終了するプログラムはほとんど無いはずだという想定からこのようにしています)。print_status
は終了プロセスが返した状態をデコードして出力します。
input_line
関数は EOF に達すると End_of_file
を出すので、これをもってループの終了とします。その後は行入力を単語に区切ってから fork
を呼び出します。子プロセスは exec_command
を呼んでコマンドを実行します。親プロセスは wait
を使ってコマンドの終了を待った後、wait
の返す子プロセスの状態を出力します。