『なるほどUnixプロセス』が良書だった。
かいつまんでメモ。
子プロセスのつくり方
p Process.pid # => 31235
# 単にforkと書くと Kernel.#fork が呼ばれる。
fork { p Process.pid } # => 31248
# Process.#fork も Kernel.#fork と同じ。
Process.fork { p Process.pid } # => 31249
# バッククォートも子プロセスをつくっている
p `echo $$` # => "31250\n"
子プロセスは親プロセスのメモリのコピーを引き継ぐ
season = 'summer'
fork do
p season # => 'summer'
season.upcase!
p season # => 'SUMMER'
animal = 'giraffe'
end
Process.wait
p season # => 'summer'
p animal # => undefined local variable or method `animal'
環境変数やファイルディスクリプタも同様。
標準入出力とは
- 入力元/出力先を指定しない場合にデフォルトで入力元/出力先となるところ。
- Rubyでは例えば、標準入力は
Kernel.#gets
の入力元に、標準出力はKernel.#puts
の出力先になる。 -
$stdin
,$stdout
という変数によって表現される。 -
$stdin
のデフォルト値はSTDIN
という定数であり、これはキーボードからの入力を表すIOオブジェクト。 -
$stdout
のデフォルト値はSTDOUT
という定数であり、これは画面への出力を表すIOオブジェクト。
$stdin # => #<IO:<STDIN>>
$stdout # => #<IO:<STDOUT>>
STDIN.class #=> IO
STDOUT.class #=> IO
標準入力を変更してみる。
% echo hoge > in.txt
% ruby -e '$stdin = File.open("in.txt"); puts gets'
=> hoge
標準出力を変更してみる。
% ruby -e '$stdout = File.open("out.txt", "w"); puts "fuga"'
% cat out.txt
=> fuga
ゾンビプロセス
ps
でみれる情報を「プロセステーブル」と呼ぶ。-
プロセスの実行が終了した状態で、以下のいずれかが起こるとプロセステーブルから削除される。
- 親プロセスでwaitしてもらう(「終了したよ」という報告を親プロセスに受け取ってもらう)
- 親プロセスの実行が終了する
このいずれもが起こらずに、「実行終了したけれどプロセステーブルには残っている」プロセスを、ゾンビプロセスと呼ぶ。
実際に確認しよう。
親プロセスのwaitによってプロセステーブルから削除される場合
以下のスクリプトを用意する。
$PROGRAM_NAME = 'ruby_parent'
fork do
$PROGRAM_NAME = 'ruby_child'
sleep(5)
end
Process.wait
sleep(10)
ruby_childの実行は約5秒で終了し、ruby_parentはさらに約5秒たってから実行終了するはずだ。
スクリプト実行直後
% ps u | grep ruby
USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND
takashi 31931 0.0 0.1 4300776 7184 s001 S+ 10:58AM 0:00.13 ruby_parent
takashi 31944 0.0 0.0 4300520 836 s001 S+ 10:58AM 0:00.00 ruby_child
どちらもステータスは"S+"でスリープ状態になっている。
5秒経過時点
% ps u | grep ruby
USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND
takashi 31931 0.0 0.1 4300776 7188 s001 S+ 10:58AM 0:00.13 ruby_parent
ruby_childは、ruby_parentにwaitされたことによってプロセステーブルから削除された。
10秒経過時点
% ps u
(出力なし)
ruby_parentは、さらにその親プロセスであるシェルにwaitされたことによってプロセステーブルから削除されたのだと思う(確信がない)。
親プロセスの終了によってプロセステーブルから削除される場合
先程のスクリプトからProcess.wait
を削除する。
$PROGRAM_NAME = 'ruby_parent'
fork do
$PROGRAM_NAME = 'ruby_child'
sleep(5)
end
sleep(10)
スクリプト実行直後
% ps u | grep ruby
USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND
takashi 31880 0.0 0.0 4299496 832 s001 S+ 10:32AM 0:00.00 ruby_child
takashi 31867 0.0 0.1 4299752 7248 s001 S+ 10:32AM 0:00.07 ruby_parent
どちらもステータスは"S+"でスリープ状態になっている。
5秒経過時点
% ps u | grep ruby
USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND
takashi 31867 0.0 0.1 4299752 7248 s001 S+ 10:32AM 0:00.07 ruby_parent
takashi 31880 0.0 0.0 0 0 s001 Z+ 10:32AM 0:00.00 (ruby)
ruby_childはステータスが"Z+"でゾンビプロセスになっている。(プロセス名の表示は"ruby_child"から"(ruby)"になっている。)
10秒経過時点
% ps u
(出力なし)
- ruby_childは、ruby_parentの実行が終了したことによってプロセステーブルから削除された。
- ruby_parentは、さらにその親プロセスであるシェルにwaitされたことによってプロセステーブルから削除されたのだと思う。
この例では一時的にゾンビプロセスが生じたものの、親プロセスがすぐに終了したため、ゾンビプロセスも削除することができた。
しかし親プロセスがデーモンとして存続する場合、ゾンビプロセスは半永久的に残ってしまう。リソースを握ったまま解放してくれないと悲しい気持ちになる。
孤児プロセス
孤児プロセスとは、自身を実行している途中に親プロセスが実行終了したプロセス。
本来の親プロセスを失った時点で、孤児プロセスはinitプロセスの子プロセスへと移行する。
実際にみてみる。
$PROGRAM_NAME = 'ruby_parent'
fork do
$PROGRAM_NAME = 'ruby_child'
p Process.ppid
sleep(10)
p Process.ppid
loop { sleep(1) }
end
sleep(5)
ruby_parentは5秒で終了し、ruby_childは無限ループにより存続する。
スクリプト実行直後
% ps u | grep ruby
USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND
takashi 32018 0.0 0.1 4284392 7184 s001 S+ 11:29AM 0:00.13 ruby_parent
takashi 32031 0.0 0.0 4284136 1080 s001 S+ 11:29AM 0:00.00 ruby_child
- どちらもステータスは"S+"でスリープ状態になっている。
- この時点での
p Process.ppid
の結果として32018
が出力された。これは確かにruby_parentのpidである。
5秒経過時点
% ps u | grep ruby
USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND
takashi 32031 0.0 0.0 4284136 1080 s001 S 11:29AM 0:00.00 ruby_child
ruby_parentは実行終了し、さらにその親プロセスであるシェルにwaitされたことによってプロセステーブルから削除されたのだと思う。
10秒経過時点
% ps u | grep ruby
takashi 32031 0.0 0.0 4284136 1080 s001 S 11:29AM 0:00.00 ruby_child
この時点でのp Process.ppid
の結果として1
が出力された。これはinitプロセスのpidであり、本来の親プロセスであるruby_parentを失ったためにinitプロセスの子プロセスとなったことが分かる。
この例では、ruby_childがデーモン化して存続することとなった。一般に、デーモンプロセスとは意図的につくられた孤児プロセスである。
IPC(Inter-Process Communication)
主にパイプとソケットという2つの方法がある。
ソケットは"Working with TCP sockets"でがっつりやるつもりなので省略する。
以下はパイプのサンプル。
reader, writer = IO.pipe
fork do
reader.close
writer.puts 'message from child process'
end
writer.close
puts reader.read # => message from child process
単方向の通信であることに注意する。
親プロセスでIOオブジェクトのペアをつくって、それらを子プロセスにコピーするので4つのオブジェクトができるが、そのうち2つは不要。
writeする方のプロセスではreaderをcloseし、readする方のプロセスではwriterをcloseしている。