Kernel.#forkで作成した子プロセスをゾンビ化させない

# 背景

ジョブキューをさばくバッチ処理を開発していると、子プロセスを使って並列処理を実装したくなりました。
最大で10プロセスまで子プロセスを生成し10並列で処理できるように実装したは良いものの、
子プロセスがゾンビプロセス化してジョブキューをうまくさばくことができませんでした。

これまでなんとなくRubyでプロセスを扱ってきたので、これを機にいろいろ試してみました。

# 検証環境

- OS
  - Debian GNU/Linux 9 (stretch)
  - Dockerコンテナで検証しました。
- Ruby
  - 2.6.3

# 検証

以下、やったことを書いていきます。

## 親プロセス

手始めに親プロセスの確認。

```
root@05b9ab6e9fa3:~# irb
irb(main):001:0>
irb(main):002:0> Process.pid
=> 534
```

```
root@05b9ab6e9fa3:~# ps auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
:
root       529  1.2  0.1  18188  3216 pts/0    Ss   12:23   0:00 /bin/bash
root       534  3.7  0.8  90008 16596 pts/0    S+   12:23   0:00  \_ irb
:
```

親のpidが「534」であることが確認できました。

## 子プロセス

次に子プロセスの確認。

`Kernel.#fork` を使って、子プロセスを生成します。

```
irb(main):003:0> fork { sleep 60 }
=> 543
```

```
root@05b9ab6e9fa3:~# ps auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
:
root       529  0.4  0.1  18188  3216 pts/0    Ss   12:23   0:00 /bin/bash
root       534  0.9  0.8  90148 16596 pts/0    S+   12:23   0:00  \_ irb
root       543  0.0  0.5  90148 11396 pts/0    S+   12:25   0:00      \_ irb
:
```

子プロセスが生成できました。

## ゾンビ化する子プロセス

しばらく時間をおいてプロセスを確認すると、子プロセスが <defunct> になり、STATが Z+ になっていました。

```
root@05b9ab6e9fa3:~# ps auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
:
root       529  0.2  0.1  18188  3216 pts/0    Ss   12:23   0:00 /bin/bash
root       534  0.5  0.8  90148 16596 pts/0    S+   12:23   0:00  \_ irb
root       543  0.0  0.0      0     0 pts/0    Z+   12:25   0:00      \_ [irb] <defunct>
:
```

これは親プロセスが子プロセスを管理していないため、子プロセスがカーネルのキューに残り続けてしまっている状態のようです。

子プロセスをゾンビ状態から開放させてあげるためには、親プロセスにて `Process.wait` を読んであげると良いらしいのでやってみました。

```
irb(main):004:0> Process.wait
=> 543
```

```
root@05b9ab6e9fa3:~# ps auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
:
root       529  0.0  0.1  18188  3216 pts/0    Ss   12:23   0:00 /bin/bash
root       534  0.2  0.8  90148 16664 pts/0    S+   12:23   0:00  \_ irb
:
```

子プロセスが無事に開放されましたね。



## 子プロセスをゾンビにさせない

子プロセスが終了するより先に、`Process.#detach` を呼んでみます。

```
irb(main):007:0> fork { sleep 60 }
=> 555
irb(main):008:0> Process.detach(555)
=> #<Process::Waiter:0x000055ca51dad5b0 run>
irb(main):009:0>
```

子プロセスの処理が終了する前にプロセスの状況を確認してみると、先ほどとは変わらないですね。

```
root@05b9ab6e9fa3:~# ps auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
:
root       529  0.0  0.1  18188  3216 pts/0    Ss   12:23   0:00 /bin/bash
root       534  0.1  0.8 157740 16724 pts/0    Sl+  12:23   0:00  \_ irb
root       555  0.0  0.5  90148 11420 pts/0    S+   12:32   0:00      \_ irb
:
```

子プロセスの処理が終了した後にプロセスの状況を確認してみると...

```
root@05b9ab6e9fa3:~# ps auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
:
root       529  0.0  0.1  18188  3216 pts/0    Ss   12:23   0:00 /bin/bash
root       534  0.1  0.8 157740 16732 pts/0    S+   12:23   0:00  \_ irb
:
```

おー。ゾンビにならず無事に（？）子プロセスが終了しています。
ゾンビプロセスにさせたくないときは、子プロセスを生成した後すぐにデタッチしてあげると良いみたいですね。



### 子プロセスが終了した後に、`Process.#detach`  を呼ぶとどうなるか

まずは子プロセスの生成。

```
irb(main):018:0> fork { sleep 60 }
=> 562
```

子プロセスがゾンビ化するのを待ちます。

```
root@05b9ab6e9fa3:~# ps auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
:
root       529  0.0  0.1  18188  3216 pts/0    Ss   12:23   0:00 /bin/bash
root       534  0.0  0.8 157888 16836 pts/0    S+   12:23   0:00  \_ irb
root       562  0.0  0.0      0     0 pts/0    Z+   12:38   0:00      \_ [irb] <defunct>
:
```

親プロセスで `Process.#detach`  を呼び出します。

```
irb(main):020:0> Process.detach(562)
=> #<Process::Waiter:0x000055ca51dc8ec8 run>
irb(main):021:0>
```

プロセスの状態を確認すると

```
root@05b9ab6e9fa3:~# ps auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
:
root       529  0.0  0.1  18188  3216 pts/0    Ss   12:23   0:00 /bin/bash
root       534  0.0  0.8 157888 16860 pts/0    S+   12:23   0:00  \_ irb
:
```

子プロセスが消えてますね。
まあ、これはそうなるような気がしてました。

### スレッドの状態を確認する

```
irb(main):023:0> fork { sleep 60 }
=> 577
```

```
root@05b9ab6e9fa3:~# ps aux -L
USER       PID   LWP %CPU NLWP %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
:
root       529   529  0.0    1  0.1  18188  3216 pts/0    Ss   12:23   0:00 /bin/bash
root       534   534  0.0    1  0.8 157888 16860 pts/0    S+   12:23   0:00 irb
root       577   577  0.0    1  0.5 157888 11636 pts/0    S+   12:43   0:00 irb
```

子プロセスが終了する前に、`Process.#detach` を呼びます。

```
irb(main):024:0> Process.detach(577)
=> #<Process::Waiter:0x000055ca51dd4b60 run>

```

スレッドを確認すると、

```
root@05b9ab6e9fa3:~# ps aux -L
USER       PID   LWP %CPU NLWP %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
:
root       529   529  0.0    1  0.1  18188  3216 pts/0    Ss   12:23   0:00 /bin/bash
root       534   534  0.0    2  0.8 157888 16904 pts/0    Sl+  12:23   0:00 irb
root       534   580  0.0    2  0.8 157888 16904 pts/0    Sl+  12:43   0:00 irb
root       577   577  0.0    1  0.5 157888 11636 pts/0    S+   12:43   0:00 irb
:
```

PIDが534のプロセスが2プロセスになってます。
`ps` コマンドの `-L` オプションで スレッドも表示するようにしているためです。

子プロセスが終了すると、

```
root@05b9ab6e9fa3:~# ps aux -L
USER       PID   LWP %CPU NLWP %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
:
root       529   529  0.0    1  0.1  18188  3216 pts/0    Ss   12:23   0:00 /bin/bash
root       534   534  0.0    1  0.8 157888 16904 pts/0    S+   12:23   0:00 irb
:
```

子プロセスが消え、親プロセスも1スレッドに戻っていることが確認できました。

この状態から、親プロセスで `Process.#wait`  を呼ぶと

```
irb(main):026:0> Process.wait
Traceback (most recent call last):
        5: from /usr/local/bin/irb:23:in `<main>'
        4: from /usr/local/bin/irb:23:in `load'
        3: from /usr/local/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
        2:
       from (irb):26
        1: from (irb):26:in `wait'
Errno::ECHILD (No child processes)
```

`Errno::ECHILD` エラーが発生してますね。
子プロセスがいなくなっているのだから、当然ですね。



同じことを、Rubyのプログラムからも確認してみました。

```
irb(main):043:0> fork { sleep 60 }
=> 585
irb(main):044:0>
irb(main):045:0> Process.detach(585)
=> #<Process::Waiter:0x000055ca51b51230 run>
irb(main):046:0>
irb(main):047:0> Thread.list
=> [#<Thread:0x000055ca5174f2b8 run>, #<Process::Waiter:0x000055ca51b51230 sleep>]
irb(main):048:0>
irb(main):049:0>

# ここで子プロセスが終了。

irb(main):050:0> Thread.list
=> [#<Thread:0x000055ca5174f2b8 run>]
irb(main):051:0>
```

スレッドが2になり、子プロセスが終了するとスレッド数も1に戻っていることがわかります。



### `Process.#detach`  と　`Process.#wait`  を同時に呼ぶとどうなるか



```
irb(main):034:0> fork { sleep 60 }
=> 583
irb(main):035:0> Process.detach(583)
=> #<Process::Waiter:0x000055ca51dff518 run>
irb(main):036:0>
irb(main):037:0> Process.wait

Traceback (most recent call last):
        5: from /usr/local/bin/irb:23:in `<main>'
        4: from /usr/local/bin/irb:23:in `load'
        3: from /usr/local/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
        2: from (irb):37
        1: from (irb):37:in `wait'
Errno::ECHILD (No child processes)
```

`Process.wait` を読んだ直後は、待ち状態に入りました。
子プロセスの処理が終了したタイミングで `Errno::ECHILD` エラーが発生しました。

`Process.#detach`  と　`Process.#wait`  のどちらか一方を呼び出すようにするのが良さそうです。

## 結論

forkしたあと、すぐに `Process.#detach` を呼び出し、スレッド数を監視することで子プロセス数の制御するのが良さそうな気がしました。

このようなユースケースではどうやってプロセス管理するのがいいんでしょうね。

他の方法でやってる方がいらっしゃったら教えてください😀

## 参考

[プロセスの適切な扱い方を再確認した](https://blog.a-know.me/entry/2016/10/12/203210)