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は終了コードを受け取るとともに、終了した子の管理情報を回収します。
操作履歴と解説を振り返れます。
シェル / プロセス
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)python3 lifecycle.py forkparent: 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
python3 lifecycle.py execparent: 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
python3 lifecycle.py badexecparent: 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
python3 lifecycle.py waitparent: 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
python3 -c 'import os; status=1792; print(os.WIFEXITED(status), os.WEXITSTATUS(status), os.waitstatus_to_exitcode(status))'True 7 7
sed -n '31,47p' lifecycle.pyprint('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)演習完了