2019-07-13
『なるほどUnixプロセス』を読んだ
『なるほど 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_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_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)“になっている。)
10 秒経過時点
% ps u
(出力なし)
- 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_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](https://www.amazon.co.jp/Working-Sockets- English-Jesse-Storimer-ebook/dp/B00BPYT6PK)“でがっつりやるつもりなので省略する。
以下はパイプのサンプル。
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 している。