読者です 読者をやめる 読者になる 読者になる

けちゃぶろぐ

Golang とか Ruby とか Vim とか……。

Unix プロセスと Docker の罠

プロセスの親子関係

Unix 系 OS では、プロセスに親子関係があります。

子プロセスを生成する方法として、fork が挙げられます。 fork は、プロセスのコピーを生成するシステムコールで、コピーされた新しいプロセスを子プロセス、fork の呼び出し元のプロセスを親プロセスと呼びます。

Ruby では Kernel.fork を呼び出すことで fork を実行できます。

if fork
  # 親プロセス
else
  # 子プロセス
end

または、

fork do
  # 子プロセス
  # ブロックの末尾で子プロセスは終了する
end

プロセスの子プロセスが更に子プロセスを生成して……というように、プロセスは家系図のように枝分かれしていきます。 プロセスには、自分を生み出した親プロセスが必ず存在します。

ps コマンドを使って、親プロセスを確認することができます。

$ ps -ho ppid,pid,comm

また、Ruby では、Process.ppid で親プロセスの PID を取得できます。

一番最初のプロセス

すべてのプロセスには親がいますが一つだけ例外があります。

すべてのプロセスの先祖となる init プロセスです。

init プロセスはカーネル起動時に生成され、PID は 1 になります。init の親プロセスは存在せず、親 PID として 0 が設定されます。

孤児プロセス

子プロセスよりも先に親プロセスが死んだとき、その子プロセスは道連れにはされません。

孤児となった子プロセスは init プロセスによって引き取られ、親が死んでも子は生き続けます。

子プロセスの結果を取得する

子プロセスが死んだとき、その終了した結果(終了コード)を親プロセスは知ることができます。

親プロセスはいつでも子プロセスを監視しているわけではないので、子プロセスが死んだ時、その情報は一旦キューに格納されます。

親プロセスがキューから子プロセスの終了情報を読取ると、キューはクリアされ、子プロセスの痕跡は消え去ります。

Ruby では Process.wait を使用すると、子プロセスの終了を待機することができます。 また、Process.wait2 を使用すると子プロセスの終了結果も取得できます。

fork do
  sleep 3
end

pid, status = Process.wait2
puts status.exitstatus

ゾンビプロセス

親プロセスが子プロセスの終了情報をキューから取り出さない限り、子プロセスの情報はシステムに残り続けます。

これがゾンビプロセスです。

ps コマンドで、ゾンビプロセスかどうか確認することができます。

$ ps -ho pid,stat,comm

ゾンビプロセスの場合、STAT に Z と表示されます。

ゾンビプロセスを残さないために、確実に Process.wait などを呼び出すか、Process.detach を呼び出して終了の監視を任せる必要があります。

Docker の罠

Docker のコンテナとして起動したプロセスにおいても、子プロセスを生成することができます。 子プロセスは孫プロセスを起動することができます。

親、子、孫の3プロセスが起動している状態で、親や子が孫より先に死んだ時、なにが起きるでしょうか?

Docker コンテナ内でない通常のプロセスであれば、親や子が死んでも、孫は init に引き取られ生き続けます。 ですが、Docker コンテナ内では異なる動きをします。

まず、親が死んだ場合。

よくある想定としては、docker stop コマンドを呼び出した時などで、親プロセスに SIGTERM が送信されます。 通常、SIGTERM を受け取った親プロセスは終了処理を実行して終了します。このとき、子プロセスを正しく管理しているのであれば、子プロセスを終了させるための処理を行うでしょう。子プロセスは孫プロセスを終了させるかもしれません。

しかし、子プロセスを終了させずに放置した場合、子プロセスや孫プロセスは問答無用で殺されます。 親が死んでも生き続ける子や孫を、Docker さんは認めません。

つぎに、親が死なずに子が死んだ場合。

子プロセスが孫を生成するだけ生成してとっとと死んだ場合などが考えられるでしょうか。 期待する動きとしては、孤児となった孫プロセスを、init プロセスが引き取る、と言った感じでしょうか。

しかし。 Docker コンテナ内では init が存在しません。なので Docker さんは、孫プロセスの引き取り手として、親プロセスを選びます。

これで安心……のように見えますが、ここに罠があります。 Docker さんが孫プロセスの引き取り手として親プロセスを選びましたが、その親プロセスはそのことを知らされません。

親プロセスが孫プロセスが生きてることをしらない状態で孫が死んだ時。孫はゾンビとなるのです。

ゾンビに安らかな眠りを与えるために

勝手に孫を養子にされた親は、孫の死を知ることができないのでしょうか?

実は、子が死んだ時、親プロセスは死亡通知を受け取ることができます。 その死亡通知は、SIGCHLD というシグナルとして送信されます。 このシグナルは、勝手に養子にされた孫が死んだ時にも送信されます。

つまり、親が死亡通知を受け取るための手続きをすることで、子供の最後を看取り、ゾンビ化させずに安らかなる眠りを与えることができるのです。

Ruby では以下のようにすると実現できます。

Signal.trap(:SIGCHLD) do
  begin
    while pid = Process.wait(-1, Process::WNOHANG)
      puts "Process #{pid} is dead."
    end
  rescue Errno::ECHILD
  end
end

不幸はゾンビを生まないために。 Docker コンテナ内でサブプロセスを起動する場合、これらのことに留意してアプリケーションを開発する必要があります。

サンプルコード

いろいろ試すためのサンプルコードを書いてみました。

docker-zombie

# ゾンビを生み出す
$ ruby parent.rb

# ゾンビを生み出さない
$ ruby parent.rb -c

# Docker Image ビルド
$ docker build -t docker-zombie .

# Docker コンテナー内でゾンビを生み出す
$ docker run -it --rm docker-zombie

# Docker コンテナー内でゾンビを生み出さない
$ docker run -it --rm docker-zombie -c

参考書籍

なるほどUnixプロセス ― Rubyで学ぶUnixの基礎