Instantly share code, notes, and snippets.

@murayama333 /process2.md Secret
Last active Jul 25, 2018

Embed
What would you like to do?

プロセスの生成(forkの話)

すべてのプロセスには親プロセスが要ることを説明した。

forkのイメージ

ここまでターミナルからrubyコマンドを実行してプロセスを生成してきたが、実行中のプロセスから新たにプロセスを生成することもできる。

カーネルにはプロセス生成するシステムコールforkが用意されている。


fork(すべてコピーされる)

まずforkという名前に注目してほしい。forkは分岐するという意味である。言われてみれば確かに、スプーン、フォークのフォークも先端が分岐している。

それではプロセスを分岐することと、プロセスが生成されることの関係を見ていこう。


Rubyでシステムコールforkを利用するにはforkメソッドを呼び出す。

pid = fork()  # forkメソッドで子プロセスを生成

if pid != nil
  puts "Parnt Process #{Process.pid}"
else
  puts "Child Process #{Process.pid}"
end

sleep 10

このプログラム(fork1.rb)実行してみると次のような結果になる。

$ ruby fork1.rb
Parnt Process 25635
Child Process 25661

ifブロックとelseブロックの両方が実行されている。ように見える。


pid = fork()

正しく説明すると、fork()メソッドによって、子プロセスが生成される。

この結果、処理が2つになる。

親プロセスはfork()メソッドの戻り値に子プロセスのIDを受け取る。つまり親プロセス側の処理においては、変数pidはnilではない。

一方、子プロセス側はfork()メソッドの戻り値がnilになる。この結果、親プロセスはifブロックを実行し、子プロセスはelseブロックを実行することになる。


プログラムをサスペンドして、psコマンドでプロセスの親子関係を見てみよう。

$ ps -o pid,ppid,comm,args
  PID  PPID COMMAND         COMMAND
24841 24840 bash            -bash
25635 24841 ruby            ruby fork1.rb
25661 25635 ruby            ruby fork1.rb
25667 24841 ps              ps -o pid,ppid,comm,args

子プロセス25661の親プロセスは25635であることがわかる。このように複数のプロセスを使うことで並行処理することができる。


鬼重要:forkはすべてコピーする

forkを使えば、かんたんに子プロセスを生成することができる。しかし、その仕組みを知らないで使うと大変な危険を伴う。

実はシステムコールforkは子プロセスを生成する際に親プロセス自身のコピーを作成している。


親プロセスをコピーするということはどういうことか。たとえば、次のプログラムを考えてみよう。

s = "a" * 50 * 1000 * 1000  # 無駄に50MB消費してみる

pid = fork()
if pid != nil
  puts "Parnt Process #{Process.pid}"
else
  puts "Child Process #{Process.pid}"
end

sleep 10

このプログラム(fork2.rb)では、forkメソッドを呼び出す前に、50,000,000桁の文字を生成している(つまりメモリを50MB使う)。


実行してみると次のようになる。

$ ruby fork2.rb
Parnt Process 25817
Child Process 25843

sleep中にプロセスをサスペンドして、psコマンドでメモリの使用量(VSZ:仮想メモリ、RSS:物理メモリ)を確認してみよう。


$ ps -o pid,ppid,vsz,rss,comm,args
  PID  PPID    VSZ   RSS COMMAND         COMMAND
24841 24840  10576  6784 bash            -bash
25817 24841  59160 53688 ruby            ruby fork2.rb
25843 25817  59160 51940 ruby            ruby fork2.rb
25846 24841   4900   704 ps              ps -o pid,ppid,vsz,rss,comm,args

結果をみると、親プロセス、子プロセスともに50MB程度のメモリを消費しているのがわかる。メモリを大量に消費しているプロセスをforkしてしまうと、同じだけのメモリを使用してしまう。

forkの仕組みを理解せずに使ってしまうとメモリを枯渇してしまう可能性があるので注意が必要だ。


forkのイメージ


forkは待たない

forkは親プロセスをコピーして子プロセスを生成するため、親プロセスと同じだけ子プロセスはメモリを消費すると説明した。しかし、子プロセスの生成は一瞬で完了する。

先ほどのプログラムの場合、50MBの領域が必要になるわけだが、子プロセスのメモリ領域が瞬時に割り当てられるのはなぜだろうか。


その仕組みはコピーオンライトという考え方で実装されている。

子プロセスも同じだけメモリを消費すると話したが、厳密には、子プロセスが誕生してすぐは、親プロセスのメモリと同じ領域を参照している。

コピーオンライトの考え方は、メモリへの書き込みが発生したときに初めてメモリがコピーされる。このようにコピー処理を遅延させることで、子プロセスを高速に生成することができるのである。


ファイルディスクリプタもコピーされる

forkはメモリ上の変数だけでなく、ファイルディスクリプタもコピーする。これについてはPIPEの仕組みと合わせて後述する。

forkのイメージ


親プロセスと子プロセスの同期

forkを上手く使えば処理を並行に進めることができる。

カーネルは、親プロセスが子プロセスの処理の完了を待つためのシステムコールwaitを提供している。

waitを使えば、親プロセスと子プロセスの処理について同期をとることができる。


wait 子プロセスの終了を待つ

Rubyでシステムコールwaitを使うには、Processクラスのwaitメソッドを使う。

fork do
  # 子プロセスの処理はendで終了する
  puts "Child start #{Process.pid}"
  sleep 3  # 処理に3秒かかるかんじ
  puts "Child end #{Process.pid}"
end

# 親プロセスの処理
puts "Parent start #{Process.pid}"
Process.wait()  # 子プロセスの完了を待つ
puts "Parent end #{Process.pid}"

上記のプログラム(fork3.rb)を実行すると、子プロセスは処理に3s程度かかる。親プロセスは、Process.waitメソッドを呼び出して子プロセスの完了を待つことになる。


実行結果は次のようになる。

$ ruby fork3.rb
Parent start 25983
Child start 26009
Child end 26009
Parent end 25983

実行結果を見ると、子プロセスが終了した後、親プロセスが終了していることがわかる。


ゾンビプロセス

親プロセスは通常、生成した子プロセスの終了をwaitで捕捉すべきである。なぜなら終了した子プロセスはすぐに破棄されないからである。

子プロセスの終了は親プロセスのwaitに関連付いている。そのためカーネルは終了した子プロセスを親プロセスがwaitを呼び出すまで保持しておく必要がある。

それでは親プロセスがwaitを呼び出すまでの間、終了した子プロセスはどうなっているのだろうか。


次のプログラムは子プロセスの終了をwaitで捕捉しない。

fork do
  # 子プロセスの処理はendで終了する
  puts "Child end #{Process.pid}"
end

# 親プロセスの処理
sleep 10  # Process.waitを呼んでいない
puts "Parent end"

上記のプログラム(fork4.rb)では子プロセスは間もなく終了するが、親プロセスはProcess.waitを呼び出さずに10s間sleepしている。

$ ruby fork4.rb
Child end 26157

このとき、プロセスをサスペンドしてpsコマンドでプロセスの状態を確認すると次のようになる。

$ ps -o pid,ppid,vsz,rss,stat,comm,args
  PID  PPID    VSZ   RSS STAT COMMAND         COMMAND
24841 24840  10576  6784 Ss   bash            -bash
26131 24841  10060  4636 Tl   ruby            ruby fork5.rb
26157 26131      0     0 Z    ruby <defunct>  [ruby] <defunct>
26160 24841   4900   704 R+   ps              ps -o pid,ppid,vsz,rss,stat,comm,args

子プロセスである26157に注目してほしい。処理はすでに終了しているため、メモリ領域(VSZ、RSS)は0となっている。しかし、プロセス自体は残っており、プロセスの状態はZとなっている。

このような状態のプロセスをゾンビプロセスと呼ぶ。ゾンビプロセスはリソースを解放しないので、有限のリソースを再利用できなくなってしまう。


逆に、親プロセスが先に終了すると

それでは逆の場合はどうだろうか。親プロセスが先に終了してしまうケースだ。

fork do
  # 子プロセスの処理はendまで
  sleep 10
  puts "Child #{Process.pid}"
end

# 親プロセスの処理
puts "Parent #{Process.pid}"

上記のプログラム(fork5.rb)を実行すると次のようになる。

$ ruby fork5.rb
Parent 26227

親プロセスは終了したため、端末の制御が戻る。子プロセスは処理中のため、10s待つと画面にChild 26523が表示される。子プロセスの実行中にpsコマンドを実行すると次のようになる。

$ ps -o pid,ppid,vsz,rss,stat,comm,args
  PID  PPID    VSZ   RSS STAT COMMAND         COMMAND
24841 24840  10576  6784 Ss   bash            -bash
26253     1  10056  2732 Sl   ruby            ruby fork6.rb
26256 24841   4900   704 R+   ps              ps -o pid,ppid,vsz,rss,stat,comm,args

子プロセス26253の親プロセスは既に終了したため、PPID(親プロセスID)が1になっている。プロセスIDが1のプロセスはinitプロセスと呼ばれるシステム起動時に生成されるプロセスである。親プロセスを失った子プロセスはinitプロセスの子プロセスとして管理されることになる。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment