前回、プロセスとはOSが処理を切り替えるときの処理の単位だという話をしましたが、まずはプロセスの例を見てみましょう
ターミナルで、
$ ps
と入力してみるましょう。psは今実行中のプロセスの一覧を見ることができるコマンドです。オプションなしで実行すると自分が実行中のプロセスの一覧が見れます。で、psを実行してみると、(環境によって異なるかと思いますが)以下のような文字が出力されるかと思います。
PID TTY TIME CMD
4400 pts/2 00:00:00 bash
4419 pts/2 00:00:00 ps
一番右を見ると、(この場合は)bashというプロセスとpsというプロセスが実行されていることがわかります。bashはログインシェル、psはいまさっき打ったpsコマンドですね。ちなみに、一番左のPIDという列は、そのプロセスのidで、実行されているプロセスを一意に判別するために使われているものです。
では、今度は & つきでバックグラウンドでコマンドを実行してみましょう。
$ perl -e 'while(1){sleep}' &
ただsleepし続けるだけのperlのワンライナーです。この状態で、もう一度
$ ps
と入力してみると、
PID TTY TIME CMD
4420 pts/2 00:00:00 perl
のような、さっきは存在していなかったプロセスが新しく増えているのがわかると思います。これがさきほど実行した
$ perl -e 'while(1){sleep}' &
コマンドのプロセスです。新しく処理を始めたら新しくプロセスが生成されたのがわかるかと思います。
さて、バックグラウンドで実行中のsleepするだけのプロセスですが、今度は
$ fg
でフォアグラウンドに処理を戻して、 Ctrl+C かなんかで処理を止めましょう。その後再度 ps コマンドでプロセスの一覧を確認すると、perlのプロセスが無くなっていることが確認できるかと思います。
プロセスは、なんらかの方法で生成されたあとは、ぐんぐん処理を行っていき、処理が終わったり外部から止められたりすると消滅します。
生成 -> 処理中 -> 終了
というライフサイクルを持っているわけです。今簡単に「処理中」と書いてしまいましたが、大きくわけてこの「処理中」には3つの状態があります。
「えっ待ち状態とブロック中ってなにが違うの」という疑問を持ったかた、ごもっともです。でも、その違いは簡単です。「待ち状態」というのは、「もうすぐにでも処理できるよ!CPUさん、はやくわたしを処理して!」という状態のことです。一方、「ブロック中」というのは、たとえばファイルの読み込みを行うときにdisk I/Oを待っているなどで、「今CPUさんが私を処理しようとしても私まだIO待ちだから何もできないよ!」みたいな状態のことです。
さて、さきほど簡単に「プロセスをなんらかの方法で生成」と言いましたが、たとえば新しくコマンドを叩いて新しいプロセスが生成されるとき、中では何が起きてるのでしょうか?
通常、プロセスは、「親プロセス」がforkというシステムコールをOSに送ることによって生成されます。すると、OSは親プロセスをまるっと複製して、「子プロセス」を新しく生成します。このとき、メモリの状態は親プロセスから子プロセスにまるっとコピーされます1。コピーされて新しい環境が出来上がるため、親プロセスでなにか操作しても(変数に新しい値代入するとか新しくインスタンスを生成するとか)、その操作は子プロセスに影響を与えません。親でなんか変更したからといって、子にもその変更が伝わるみたいなことはないわけです。逆もまたしかりで、子プロセスでなにか操作しても、その変化は親プロセスに影響を与えません。
こうして、forkにより新しくプロセスが生まれると、OSによりそのプロセス専用の環境が用意されて、その中でいろんな処理が行えるようになるわけです。
こうしてforkによって、プロセスは生成されるため、基本的に全てのプロセスには「自分を生んだ親プロセス」が存在することになります。
ちなみに、forkは複数行うことができるので、「子だくさん」なプロセスというのも、あり得ます。preforkのサーバープロセスなんかは子供をたくさん作って、複数の接続のひとつひとつをそれぞれひとつの子供に処理させることで並列性を上げているわけですね。子供たちを酷使するひどいやつです。
さきほど「親プロセスがforkで子プロセス作るんだよ〜〜。だからみんな親がいるんだよ〜〜〜」ってゆるふわな感じで言いましたが、当然「えっじゃあ、その親プロセスは誰が作ったの?」という疑問がわいてきますよね。疑問にお答えしましょう。親プロセスは、「親プロセスの親プロセス」がforkで作ったのです。となると、当然「えっじゃあ、その『親プロセスの親プロセス』はだれが作ったの」いう疑問がわいてきますよね。もちろん、「親プロセスの親プロセスの親プロセス」がforkで作ったのです。となると当然(ry
というように、全てのプロセスはどんどんその「親」を辿って行くことができます。そんなわけで、全てのプロセスの祖先となる「最初のプロセス」というものが存在しないといけないわけです。このプロセスはブート時に生成されて、そのあと全てのプロセスがここを祖先としてforkされていきます。この「最初のプロセス」はPIDが1であり、Linuxの場合は init というプロセスがその実体となります。
$ ps ax | grep init
1 ? Ss 0:10 /sbin/init
このように、プロセスは親子関係の木構造を持っています。この親子関係を「プロセスツリー」と呼びます。プロセスツリーがどうなっているかを調べるためにpstreeというコマンドが使えますので、興味があればpstreeコマンドでどのようなプロセスツリーが生成されているか見てみるのもよいかと思います。ptreeコマンドの使いかたはmanで調べてください(丸投げ)
さて、「すべてのプロセスは祖先からforkされて生まれた」という話と「forkは親プロセスをまるっとコピーして子プロセスを作る」という話をしましたが、これ、なんかおかしいですね。そうです。このままでは、「親の複製のプロセス」しかなくって、すべてが同じことを行うプロセスになってしまいます!
そこで必要になるのが、execというシステムコールです。あるプロセスがexecというシステムコールを呼ぶと、OSはそのプロセスをexecの内容で書き換えてしまいます。つまり、execというのは、「自分自身の内容を別の内容で書き換えて実行してしまう」システムコールなんですね。くらくらしてきた!
まとめると、
- forkでプロセスを生成して、独立した環境を用意してあげる
- その環境に、execによって別の実行可能なものを読み込んで実行する
ことで、親プロセスとは違うプロセスをどんどん生成していくような仕組みになっているわけです。
「日本語だとよくわかんないよ、コードで書いてよ」という声がわたしの脳内から聞こえてきたので、コードで書きます。
use strict;
use warnings;
print "forking...\n";
# forkシステムコールを呼び出す
my $pid = fork;
# forkに失敗すると返り値はundef
die "fork failed." unless defined $pid;
# ここに来てるということは、正常にプロセスが複製された。
# この時点で親プロセスと子プロセスが *別々の環境で*
# 同時にこのプログラムを実行していることになる。
print "forked!\n";
# forkで生成された子プロセスでは、forkの返り値が 0 となる
# 親プロセスでは、生成された子プロセスのpidが入ってくる
if ($pid == 0){
#子プロセスはこっちを実行する
# execシステムコールで、perlのプロセスをrubyのプロセスに書き換えてしまう!
exec "ruby -e 'loop do;sleep;end'";
}
else{
#親プロセスはこっちを実行する
#子プロセスが終了するのを待つ
waitpid($pid,0);
}
上記のようなPerlスクリプトをfork_exec.plという名前で用意して、バックグラウンドで実行してみましょう。すると、以下のような出力が得られると思います。
$ perl ./fork_exec.pl &
forking...
forked!
forked!
なぜこうなるのか、説明しましょう。
print "forking!\n"; という行は、まだfork前なので、プロセスがひとつだけの状態です。なので、普通にひとつの"forking!"が出力されます。しかし、print "forked!\n"; という行は、forkシステムコールでプロセスが複製されたあとです。そのため、この行は親プロセスとそこから複製された子プロセスが、別のプロセスとして実行します。親プロセスは親プロセスで"forked!"という文字列を標準出力という場所に出力します(perlのprintという関数は、引数に渡された文字列を標準出力に出力します)、一方、別の環境で動いている子プロセスも、"forked!"という文字列を標準出力という場所に出力します。今回の場合、親プロセスも子プロセスも標準出力はターミナルを意味するので(このあたりの話はまたあとで詳しくやります)、ターミナルに親プロセスと子プロセスの二つ分のforked!が出力されるわけです。
さて、今バックグラウンドで実行したこのスクリプトですが、ではプロセスはどのようになっているでしょうか。psコマンドで確認して見ましょう。
$ ps
PID TTY TIME CMD
81996 ttys003 0:00.01 perl fork_exec.pl
81998 ttys003 0:00.01 ruby -e loop do;sleep;end
psコマンドの出力に、上記のようなふたつの行が見つかるかと思います。上の perl fork_exec.pl というプロセスが私たちがさっき「$ perl fork_exec.pl &」と実行したプロセスで、下の ruby -e loop do;sleep;end というプロセスが、forkされた子プロセスです。pstreeで見てみましょう。
$ pstree 81996 (さっきpsで確認した "perl fork_exec.pl" のPIDを指定)
-+= 81996 shinpeim perl fork_exec.pl
\--- 81998 shinpeim ruby -e loop do;sleep;end
というような出力が得られ、"perl fork_exec.pl" というプロセスから "ruby -e loop do;sleep;end" というプロセスが生成されているのがわかるかと思います。
さて、今バックグラウンドで実行しているプロセス(親プロセスです)を fg コマンドでフォアグランドに移して、Ctrl+Cで止めてしまいましょう。その後もう一度psコマンドを叩くと、子プロセスごと消えているのがわかるかと思います。なぜこうなるのかについては、シグナルについて見るときに説明しましょう。
今は、「forkで子プロセスを生成できて、execでそのプロセスの内容を書き換えられた」ということがわかれば十分です。コマンドを叩いて新しいプロセスを生成する場合とかも、内部ではこのようにforkでプロセスを生成して、確保された環境の内容をexecで書き換えるという形で生まれているのです。ちなみに、シェルからコマンドを叩いてプロセスを生成するときには、「親プロセス」に当たるのはシェルのプロセスになります。
- forkしたpidを看取る話と子供がゾンビになっちゃう話
- あらゆる入出力はファイルとして扱われてるよって話からの、forkした際の file descripter と open file description について
あたりを書きたい気持ちがある