Previous Up Next

 3  プロセス

プロセスとはオペレーティングシステム上で実行されるプログラムのことです。プロセスはプログラム (機械語) とその状態 (現在の実行位置、変数の値、関数呼び出しスタック、開いているファイルディスクリプタなど) からなります。

この章では新しいプロセスを作ったり新しいプログラムを実行したりするための Unix のシステムコールを紹介します。

3.1  プロセスの作成

システムコール fork はプロセスを作成します。

val fork : unit -> int

fork を呼び出した 親プロセス のほぼ完璧な複製である 子プロセス が新しく作られます。二つのプログラムは同じプログラムを同じ実行位置 (fork から返った位置) から実行します。このとき全ての変数は同じ値を持ち、スタックは同一で、開かれているファイルディスクリプタも同じです。二つのプロセスを区別する唯一のものは fork の返り値です。子プロセスでは fork は 0 を返し、 親プロセスでは 0 でない整数を返します。fork の返り値を確認することで、プログラムは自分が親なのか子なのかを確認してそれによって動作を変えることができます。

match fork () with | 0 -> (* 子プロセスだけが実行するコード *) | pid -> (* 親プロセスだけが実行するコード *)

fork によって親プロセスに返される 0 でない整数は子プロセスの プロセス ID です。カーネルはプロセス IDを使ってプロセスを一意に識別します。プロセスは getpid 関数を呼ぶことでプロセス ID を取得できます。

子プロセスは親プロセスと同じ状態 (同じ変数の値、同じファイルディスクリプタ) に初期化されます。この状態は親と子で共有されるのではなく、 fork が呼ばれたときにコピーされます。例えば fork の前に定義した参照変数があった場合、 fork の後には親と子プロセスはこの参照を互いに影響を及ぼすこと無く独立に変更できます。

同様にファイルディスクリプタも fork が呼ばれたときにコピーされます。そのため一方を閉じたとしてももう一方は開いたままです。ただし二つのディスクリプタは (システムメモリにある) ファイルテーブル内の同じエントリを指すので、入出力の現在位置を共有します。親と子のどちらかが読み込みを行った場合、その次に読み込むのがどちらであっても読み込み位置は変化します。また lseek による入出力位置の変更はもう一方のプロセスにすぐに伝わります。

3.2  完全な例: leave コマンド

leave hhmm コマンドは時刻 hhmm に利用を終える時間だとユーザに報告するバックグラウンドプロセスをフォークしてすぐに終了します。このコマンドを作成します。

1 open Unix;; 2 3 let leave () = 4 let hh = int_of_string (String.sub Sys.argv.(1) 0 2) 5 and mm = int_of_string (String.sub Sys.argv.(1) 2 2) in 6 let now = localtime(time ()) in 7 let delay = (hh - now.tm_hour) * 3600 + (mm - now.tm_min) * 60 in 8 9 if delay <= 0 then begin 10 print_endline "Hey! That time has already passed!"; 11 exit 0 12 end; 13 if fork () <> 0 then exit 0; 14 sleep delay; 15 print_endline "\007\007\007Time to leave!"; 16 exit 0;; 17 18 handle_unix_error leave ();;

プログラムは最初にコマンドラインをパースして時刻を取得し、報告するまでの秒数を計算します (8 行目) 。time 関数はエポック (1970 年 1 月 1 日 午前 0 時 0 分 0 秒) から現在時刻までの経過秒数を返します。 localtime を使うとこの値から年、月、日、時、分、秒を計算することができます。プログラムはその後 fork で新しいプロセスを作ります。親プロセス (fork の返り値が 0 でないプロセス) はすぐに終了するため、leave を起動したシェルの制御はすぐにユーザに戻ります。子プロセス (fork の返り値が 0 のプロセス) の実行は続き、sleep を呼んで指定された時間まで待ってからメッセージを表示して終了します。

3.3  プロセスの終了を待つ

システムコール waitfork によって作られた子プロセスの一つが終了するまで待ち、そのプロセスがどのように終了したかについての情報を返します。これは親子間の同期メカニズムであり、子から親へのとても原始的な形のコミュニケーションです。

val wait : unit -> int * process_status val waitpid : wait_flag list -> int -> int * process_status

基礎となるシステムコールは waitpid であり、 wait () という呼び出しは waitpid [] (-1) の短縮形に過ぎません。 waitpid [] p の動作は p の値によって異なります。

返り値の最初の要素は wait によって終了を捕捉された子プロセスのプロセス ID です。2番目の要素は以下に示す process_status 型の値です:

WEXITED r子プロセスは exit が呼ばれるかプログラムの終端に達することによって通常の方法で終了した。r はリターンコード (exit の引数) を表す。
WSIGNALED s子プロセスはシグナル (ctrl+C, kill など。 4 章参照) によって終了した。s がシグナルの種類を表す。
WSTOPPED s子プロセスはシグナル s によって停止された。これが起こるのはあるプロセス (典型的にはデバッガ) が他のプロセスの実行を (ptrace を使って) モニターしているという特殊なケースに限られる。

子プロセスの一つが wait を呼んだ時点ですでに終了していた場合は呼び出しはすぐに返ります。そうでなければ親プロセスは子プロセスのどれかが終了するまでブロックします (“ランデブー” と呼ばれる動作です)。この子プロセスの終了を待つには waitn 回呼ぶ必要があります。

waitpid 関数は二つのオプショナルなフラグを第一引数に受け取ります。一つ目の WNOHANG フラグは終了していない子プロセスが無い場合に待たないことを指示します。子プロセスが無かった場合の返り値は第一要素が 0 で第二要素は未定義です。もう一つの WUNTRACED フラグは sigstop シグナルによって停止させられた子プロセスを返すことを指示します。waitpidp に該当する子プロセスがないとき(あるいは p-1 で現在のプロセスが子プロセスを持たないとき)には例外を出します。

以下の fork_search 関数は二つのプロセスを使って線形探索を行います。線形探索には simple_search 関数を使っています。

1 open Unix;; 2 exception Found;; 3 4 let simple_search cond v = 5 try 6 for i = 0 to Array.length v - 1 do 7 if cond v.(i) then raise Found 8 done; 9 false 10 with Found -> true;; 11 12 let fork_search cond v = 13 let n = Array.length v in 14 match fork () with 15 | 0 -> 16 let found = simple_search cond (Array.sub v (n/2) (n-n/2)) in 17 exit (if found then 0 else 1) 18 | _ -> 19 let found = simple_search cond (Array.sub v 0 (n/2)) in 20 match wait () with 21 | (pid, WEXITED retcode) -> found || (retcode = 0) 22 | (pid, _) -> failwith "fork_search";;

fork された子プロセスはテーブルの上半分を探索し、 cond を満たす要素を見つけた場合は 1 を、それ以外の場合は 0 をリターンコードとして終了します ( 16 行目と 17 行目)。親プロセスはテーブルの下半分を探索し、 wait を呼んで子プロセスと同期します ( 21 行目と 22行目)。 子プロセスが通常の方法で終了した場合、そのリターンコードとテーブルの下半分の探索結果を組み合わせます。そうでなければエラーが起こっているので、 fork_search 関数は失敗します。

* * *

wait はプロセス間の同期を行いますが、それ以外に子プロセスが持つリソースの完全な開放も行います。終了した子プロセスは “ゾンビ” 状態となり大部分のリソース (メモリなど) が開放されますが、子プロセスは wait を呼んだ親プロセスに返り値を伝える必要があるので、プロセステーブルのスロットには乗ったままです。親プロセスが wait を呼べば、子プロセスはプロセステーブルからも削除されます。このテーブルの大きさは固定なので、リークを防ぐためにもフォークした全てのプロセスを wait することが重要です。

親プロセスが子プロセスよりも先に終了した場合、子の親はプロセス ID が 1 のプロセス (通常は init) に移ります。このプロセスは wait の無限ループを含むので、子プロセスは終了するとすぐに回収されます。この仕組みによって “ダブルフォーク” という便利なテクニックが使えるようになります。このテクニックは子プロセスの終了をブロックして待つことができないときなどに使われます。

match fork () with | 0 -> if fork () <> 0 then exit 0; (* 子プロセスの処理を行う *) | _ -> wait (); (* 親プロセスの処理を行う *)

子プロセスは二回目のフォークの後すぐに終了します。これによって孫プロセスは親を失うので、init の養子となります。この方法ではゾンビプロセスが生まれることはありません。親はフォーク後すぐに wait を呼んで子を回収します。子はすぐに終了するので、この wait が長い間ブロックすることはありません。


3.4  プログラムの起動

システムコール execveexecv、 そして execvp は現在のプロセスでプログラムを起動します。現在のプログラムの実行を止めて新しいプログラムに移るので、エラーの場合を除いてこの呼び出しが返ることはありません。

val execve : string -> string array -> string array -> unit val execv : string -> string array -> unit val execvp : string -> string array -> unit

第一引数は実行するプログラムを含むファイルの名前です。execvp を使った場合ファイルの名前は (環境変数 PATH で指定される) 探索パスのディレクトリから探索されます。

第二引数はプログラムを実行するときに渡されるコマンドライン引数の配列です。実行するプログラムの中ではこの配列が Sys.argv となります。

execve を使うと第三引数にプログラムが実行される環境を渡すことができます。execvexecvp では現在の環境がそのまま使われます。

execveexecv、そして execvp が結果を返すことはありません。エラーが起こること無くプロセスが指定されたプログラムを実行するか、実行ファイルが見つからないなどのエラーが起きて呼び出し元のプログラムに Unix_error を出すかのどちらかです。

次の三つは同じ動作をします:

execve "/bin/ls" [|"ls"; "-l"; "/tmp"|] (environment ()) execv "/bin/ls" [|"ls"; "-l"; "/tmp"|] execvp "ls" [|"ls"; "-l"; "/tmp"|]
* * *

受け取った grep コマンドへの引数に -i オプション (大文字と小文字を区別しない) を追加して起動するための “ラッパー” コマンドは以下のように書けます:

open Sys;; open Unix;; let grep () = execvp "grep" (Array.concat [ [|"grep"; "-i"|]; (Array.sub Sys.argv 1 (Array.length Sys.argv - 1)) ]) ;; handle_unix_error grep ();;
* * *

emacs コマンドをターミナルのタイプを変えて起動するための “ラッパー” コマンドは以下のように書けます:

open Sys;; open Unix;; let emacs () = execve "/usr/bin/emacs" Sys.argv (Array.concat [ [|"TERM=hacked-xterm"|]; (environment ()) ]);; handle_unix_error emacs ();;
* * *

exec を呼んだプロセスは新しいプログラムを実行するプロセスと同じです。そのため新しいプログラムは exec を呼んだプログラムの実行環境の一部を引き継ぎ、以下に上げるものは同じになります:

3.5  完全な例: ミニシェル

次のプログラムは単純なコマンドインタープリターです。標準入力から行入力を読み、単語ごとに区切り、コマンドを起動し、標準入力から EOF を受け取るまでこれを繰り返します。文字列を単語のリストに分割する関数から始めます。このひどい処理についてはどうかノーコメントとさせてください。

open Unix;; open Printf;; let split_words s = let rec skip_blanks i = if i < String.length s & s.[i] = ' ' then skip_blanks (i+1) else i in let rec split start i = if i >= String.length s then [String.sub s start (i-start)] else if s.[i] = ' ' then let j = skip_blanks i in String.sub s start (i-start) :: split j j else split start (i+1) in Array.of_list (split 0 0);;

次はインタープリターのメイン処理です:

let exec_command cmd = try execvp cmd.(0) cmd with Unix_error(err, _, _) -> printf "Cannot execute %s : %s\n%!" cmd.(0) (error_message err); exit 255 let print_status program status = match status with | WEXITED 255 -> () | WEXITED status -> printf "%s exited with code %d\n%!" program status; | WSIGNALED signal -> printf "%s killed by signal %d\n%!" program signal; | WSTOPPED signal -> printf "%s stopped (???)\n%!" program;;

exec_command 関数がコマンドを実行とエラーの対処を行います。リターンコード 255 はコマンドが実行されなかったことを意味します (これは通常の慣習ではありません。リターンコード 255 で終了するプログラムはほとんど無いはずだという想定からこのようにしています)。print_status は終了プロセスが返した状態をデコードして出力します。

let minishell () = try while true do let cmd = input_line Pervasives.stdin in let words = split_words cmd in match fork () with | 0 -> exec_command words | pid_son -> let pid, status = wait () in print_status "Program" status done with End_of_file -> () ;; handle_unix_error minishell ();;

input_line 関数は EOF に達すると End_of_file を出すので、これをもってループの終了とします。その後は行入力を単語に区切ってから fork を呼び出します。子プロセスは exec_command を呼んでコマンドを実行します。親プロセスは wait を使ってコマンドの終了を待った後、wait の返す子プロセスの状態を出力します。

練習問題 10

コマンドの最後に & が付いている場合にコマンドをバックグラウンドで実行する機能を追加してください。 解答

* * *

Previous Up Next