k-tokitoh

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'

ディスクリプタも同様。

標準入出力とは

$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

ゾンビプロセス

このいずれもが起こらずに、「実行終了したけれどプロセステーブルには残っている」プロセスを、ゾンビプロセスと呼ぶ。

実際に確認しよう。

親プロセスの 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
(出力なし)

この例では一時的にゾンビプロセスが生じたものの、親プロセスがすぐに終了したため、ゾンビプロセスも削除することができた。

しかし親プロセスがデーモンとして存続する場合、ゾンビプロセスは半永久的に残ってしまう。リソースを握ったまま解放してくれないと悲しい気持ちになる。

孤児プロセス

孤児プロセスとは、自身を実行している途中に親プロセスが実行終了したプロセス。

本来の親プロセスを失った時点で、孤児プロセスは 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
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 している。