『なるほど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_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している。