
forkとexec──プロセスが生まれる瞬間
シェルでコマンドを打つとき、何が起きているか考えたことがあるか。
ls と打つ。結果が返ってくる。それだけに見える。でもその裏で、プロセスが生まれて、死んでいる。
この「生まれる」瞬間を理解すると、シェルの見え方が変わる。
forkとは何か
シェルが外部コマンドを実行するとき、まず自分自身を複製する。これが fork だ。
# このlsを実行するとき
ls -la
シェルは fork() システムコールを呼び、自分のコピー(子プロセス)を作る。親と子は、この瞬間まったく同じ状態だ。同じコード、同じ変数、同じファイルディスクリプタを持っている。
違いは一つだけ。fork() の戻り値だ。親プロセスには子のPIDが返り、子プロセスには0が返る。これで親と子は「自分がどちらか」を知る。
execとは何か
子プロセスは fork() の直後に exec() を呼ぶ。
exec() は、現在のプロセスのイメージを、別のプログラムで置き換える。ls を実行するなら、子プロセスのメモリ空間が ls のバイナリで上書きされる。
シェル(PID:100)
└─ fork() → 子プロセス(PID:101) ← シェルのコピー
└─ exec("ls") → ls(PID:101) ← 中身が置き換わる
PIDは変わらない。中身だけが変わる。
なぜforkしてからexecするのか
「最初からlsを実行すればいいのでは」と思うかもしれない。
forkしてからexecする設計には理由がある。forkした直後、子プロセスはシェルのすべてを引き継いでいる。その状態でexecする前に、「環境を整える」ことができる。
リダイレクトはその典型だ。
ls -la > output.txt
このとき子プロセスはexecの前に、標準出力のファイルディスクリプタを output.txt に付け替える。そしてexecする。ls は何も知らず、標準出力に書くだけだ。
forkとexecの間の「隙間」が、シェルの柔軟性の源泉だ。
シェルスクリプトで実感する
#!/bin/sh
echo "親: PID=$$"
# サブシェルを作る
(
echo "子: PID=$$"
echo "親のPID: $PPID"
)
echo "戻ってきた: PID=$$"
$$ は現在のプロセスのPIDだ。() でサブシェルを作ると、fork が起きる。子プロセスの $$ は親と異なる値になる。
sh縛りをしていたとき、このスクリプトを書いて動かした。「変数を変えたのに元に戻っている」という現象の原因が、このforkにあることをそこで理解した。
forkとexecを知ると、パイプがなぜ動くのかも見えてくる。
次回は、パイプの正体──ファイルディスクリプタでつながる世界の話をする。