一覧OS-05-2 · プロセスとスレッド

fork・execと子プロセスの終了処理

生成、プログラムの置換、終了状態の回収を、親子の出力とソースで確認します。

説明

Unix系OSではforkで子プロセスを作り、execで実行するプログラムを置き換え、waitで子の終了情報を回収できます。この三つはそれぞれ別の処理です。

短いPythonプログラムを使い、子だけの変数変更、execの成功と失敗、待たずに戻るwait、終了コードの復号を順に試します。各実行は親が子を回収して終了し、同じプログラムの分岐を変えて結果を比較します。

1forkする前の変数とパイプ

lifecycle.pyは親子の処理を表示する短いプログラムです。まずforkまでを読みます。valueは10、パイプは子の開始を親が知らせるために使います。

sed -n '1,13p'forkまでのソース lifecycle.py実験プログラム

os.fork()は成功すると親と子でそれぞれ戻ります。親の戻り値は子のPID、子の戻り値は0です。二つの戻り値を使って同じソース内の別の分岐を実行します。

2子の変数変更と親の値

forkモードで子だけvalueを99へ変更します。親の表示を先に出し、パイプで子を進め、終了を待ってから親の値を表示する順序です。

python3 lifecycle.py親子の実験 fork変数を変更する分岐

子は99、親は10です。fork直後に内容を引き継いでも、通常の私有メモリは別々のアドレス空間として扱われます。出力順はスケジューラ任せではなく、プログラム内の通知とwaitpidによって定めています。

3execでプログラムを置き換える

execモードでは子が同じPython実行ファイルをtarget引数で起動し直します。元のPIDを環境変数で渡し、置換後のPIDと比較します。

python3 lifecycle.py同じ実験の起動 exec子のプログラムを置換

same PID=Trueで、引数helloが新しいプログラムに渡っています。execは新しい子を増やす操作ではなく、呼び出したプロセスのプログラムを置き換えます。成功時には元の呼出し位置へ戻らず、新しいプログラムが終了コード7で終わります。

4execが失敗した場合

badexecモードでは存在しない実行ファイルを指定します。失敗を捕捉した子がメッセージを出し、終了コード127で終了します。

python3 lifecycle.py失敗時の分岐を起動 badexec存在しないパスを指定

old program continuesと表示されます。置換に失敗したため元のプログラムが動いており、127はこの実験コードが選んだ終了コードです。exec失敗時にOSが一律に127でプロセスを終了させるという仕様ではありません。

5子がまだ実行中なら待たずに戻る

waitモードは、子がパイプで待っている間にWNOHANG付きwaitpidを呼びます。その後、子を進めて終了を回収します。

python3 lifecycle.py待機と回収の実験 waitWNOHANGと再回収を比較

最初の(0, 0)は、指定した子がまだ回収可能な終了状態にないことを示します。子を進めた後のwaitpidは対象の子を回収し、二回目のwaitでは残る子がないと分かります。子の終了後から回収まで、OSは親が終了状態を得られるよう管理情報を保持します。

6終了ステータスを復号

出力のraw status=1792は終了コードそのものではありません。正常終了したかと終了コードをAPIで取り出します。

python3 -c終了状態を解析 'import os; status=1792; print(os.WIFEXITED(status), os.WEXITSTATUS(status), os.waitstatus_to_exitcode(status))'正常終了の判定とコード取得

True、7、7です。シグナル終了などでは同じビットの読み方が変わるため、まず終了の種類を判定します。Pythonのwaitstatus_to_exitcodeは通常終了ならコード、シグナル終了なら負のシグナル番号を返します。

7回収処理と資源の解放

最後に親側のソースを読み、待つ対象と終了状態の受取り箇所を確かめます。

sed -n '31,47p'親側の待機と表示 lifecycle.py実験ソース

waitpid(pid, 0)が特定の子の終了を回収します。通常、終了した子が未回収の間はゾンビとして終了情報が残ります。ゾンビはプログラムがCPUを使って走り続けている状態ではありません。ここでは親が必ず回収する構成です。

まとめ

forkの親子は異なる戻り値で分岐し、私有メモリの変更は独立します。

exec成功時はPIDを保ってプログラムが置き換わり、失敗時は元の処理が続けられます。

waitは終了コードを受け取るとともに、終了した子の管理情報を回収します。

操作履歴と解説を振り返れます。

シェル / プロセス

1
sed -n '1,13p' lifecycle.py
"""Small deterministic process experiments; the parent always reaps its child."""
import os
import sys
mode = sys.argv[1]
if mode == 'target':
    print('exec: same PID =', os.getpid() == int(os.environ['SAVED_PID']), flush=True)
    print('exec: argument =', sys.argv[2], flush=True)
    raise SystemExit(7)
value = 10
read_end, write_end = os.pipe()
pid = os.fork()
if pid == 0:
    os.close(write_end)
2
python3 lifecycle.py fork
parent: fork return > 0 = True
child: fork return = 0; value = 99
parent: reaped expected child = True
parent: value = 10
parent: raw status = 1792 ; exit code = 7
3
python3 lifecycle.py exec
parent: fork return > 0 = True
exec: same PID = True
exec: argument = hello
parent: reaped expected child = True
parent: value = 10
parent: raw status = 1792 ; exit code = 7
4
python3 lifecycle.py badexec
parent: fork return > 0 = True
child: exec failed; old program continues
parent: reaped expected child = True
parent: value = 10
parent: raw status = 32512 ; exit code = 127
5
python3 lifecycle.py wait
parent: fork return > 0 = True
wait before release = (0, 0)
parent: reaped expected child = True
parent: value = 10
parent: raw status = 1792 ; exit code = 7
second wait: no child remains
6
python3 -c 'import os; status=1792; print(os.WIFEXITED(status), os.WEXITSTATUS(status), os.waitstatus_to_exitcode(status))'
True 7 7
7
sed -n '31,47p' lifecycle.py
print('parent: fork return > 0 =', pid > 0, flush=True)
if mode == 'wait':
    print('wait before release =', os.waitpid(pid, os.WNOHANG), flush=True)
os.write(write_end, b'!')
os.close(write_end)
reaped, status = os.waitpid(pid, 0)
print('parent: reaped expected child =', reaped == pid, flush=True)
print('parent: value =', value, flush=True)
print('parent: raw status =', status, '; exit code =', os.waitstatus_to_exitcode(status), flush=True)
if mode == 'wait':
    try:
        os.waitpid(pid, os.WNOHANG)
    except ChildProcessError:
        print('second wait: no child remains', flush=True)

演習完了