Node.js: child_process.fork()で起動したプロセスを子子孫孫殺す方法

本稿では、Node.jsにて、子プロセス、そこから派生した孫プロセス、さらにそこから派生したひ孫プロセス……を、一括して終了する方法を説明します。

※説明にあたって、実行環境はUNIX/Linuxを前提にしています。


## 子プロセスを殺しても、孫プロセスは死なない

Node.jsの[child_process.fork()]は、子プロセスを起動できて便利です。子プロセスの中で、`fork()`を使って、孫プロセスを起動することもでき、さらに、孫プロセスで`fork()`して、ひ孫プロセスを、といった具合に子プロセスはネストして起動することができます。

[child_process.fork()]: https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options


起動した子プロセスは[subprocess.kill()]で終了することができます。しかし、これは直接の子プロセスしか殺すことができません。どういうことかというと、

1. oya.js が ko.js のプロセスを起動する。
2. ko.js が mago.js のプロセスを起動する。
3. このとき、 oya.js が ko.js のプロセスを`kill()`したとする。
4. ko.js は終了する。
5. mago.js は生存する。 (※このとき、 mago.js はinitプロセスの養子に出され、親pidは1になる)

といった事態が発生します。

[subprocess.kill()]: https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal

### 孫プロセスが残存するサンプルコード

上のようなシナリオを再現できるコードを書いてみたいと思います。

まず、oya.jsの実装:

```oya.js
console.log('oya.js: running')

// SIGINTを受け付けたとき
process.on('SIGINT', () => {
  console.log('oya.js: SIGINT')
  process.exit()
})

// プロセスが終了するとき
process.on('exit', () => {
  console.log('oya.js: exit')
})

// 子プロセスを起動
const ko = require('child_process')
  .fork(__dirname + '/ko.js')

// 3秒後にproc2.jsを終了する
setTimeout(() => {
  console.log('oya.js: ko.jsを終了させてます...')
  ko.kill('SIGINT')
}, 3000)

// ko.jsが終了したとき
ko.on('exit', () => {
  console.log('> Ctrl-Cを押してください...')
})

// このプロセスがずっと起動し続けるためのおまじない
setInterval(() => null, 10000)
```

oya.jsはko.jsを起動し、3秒後にko.jsを終了するコードになっています。ko.jsを`kill()`する際には、`SIGINT`シグナルを送るようにしています。Linuxのシグナルについては、ここでは詳しく説明しません。ここでは単に`SIGINT`シグナルはプロセス終了を指示するものと考えてください。

次に、ko.js:

```ko.js
console.log('ko.js: running')

// SIGINTを受け付けたとき
process.on('SIGINT', () => {
  console.log('ko.js: SIGINT')
  process.exit()
})

// プロセスが終了するとき
process.on('exit', () => {
  console.log('ko.js: exit')
})

// 孫プロセスを起動する
require('child_process')
  .fork(__dirname + '/mago.js')

// このプロセスがずっと起動し続けるためのおまじない
setInterval(() => null, 10000)
```

最後に、mago.js:

```mago.js
console.log('mago.js: running')

// SIGINTを受け付けたとき
process.on('SIGINT', () => {
  console.log('mago.js: SIGINT')
  process.exit()
})

// プロセスが終了するとき
process.on('exit', () => {
  console.log('mago.js: exit')
})

// このプロセスがずっと起動し続けるためのおまじない
setInterval(() => null, 10000)
```

このコードを実行してみます:

```
$ node oya.js
oya.js: running
ko.js: running
mago.js: running
oya.js: ko.jsを終了させてます...
ko.js: SIGINT
ko.js: exit
> Ctrl-Cを押してください...
```

3秒後にこのような出力がされ、oya.jsがko.jsを`kill()`し、ko.jsが終了したことが確認できます。

一方、mago.jsはまだ`SIGINT`を受け取っていませんし、終了もしておらず、残存しています。

ここで、Ctrl-Cを押すと、oya.jsとmago.jsに`SIGINT`が送信されます:

```
...
> Ctrl-Cを押してください...
^Coya.js: SIGINT
mago.js: SIGINT
mago.js: exit
oya.js: exit
```

このタイミングではじめて、mago.jsが終了することが分かります。

感想を言うと、ko.jsに`SIGINT`を送信したら、mago.jsにも`SIGINT`が伝搬されていくものと誤解していたので、この結果は意外でした。

## 起動したプロセスを子子孫孫殺す方法

では、起動した子プロセスを`kill()`したタイミングで、孫プロセスも終了になるようにするにはどうしたらいいのでしょうか？ それについて、ここで説明したいと思います。

### プロセスグループ = 「世帯」

まず、Linuxのプロセスの基本として、プロセスグループというものがあります。これはプロセスの「世帯」のような概念で、親プロセス、子プロセス、孫プロセスをグループ化するものです。たとえば、Bashでnodeプロセスであるoya.jsを起動すると、そこから`fork()`したko.jsやmago.jsは、同じプロセスグループに属し、同一のグループIDが与えられます。

`ps`コマンドでグループID(GPID)を確認すると、現に同じグループIDが3つのnodeプロセスに割り当てられていることが分かります:

```bash
$ ps -xo pid,ppid,pgid,command | grep node | grep .js
PID   PPID  GPID  COMMAND
17553  3528 17553 node oya.js
17554 17553 17553 node ko.js
17555 17554 17553 node mago.js
```

この結果をよく見ると分かりますが、GPIDはoya.jsのプロセスID(PID)と同じです。つまり、親のPIDが子孫のGPIDになるわけです。

### プロセスを「世帯」ごと殺す方法

Node.jsでは、グループIDを指定して、プロセスを終了させることができます。やりかたは、[process.kill()]にGPIDを渡すだけです。このとき、与える値は負の数にしてあげます。正の数を渡してしまうと、プロセスグループではなく個別のプロセスを`kill()`するだけになるので注意です。

[process.kill()]: https://nodejs.org/api/process.html#process_process_kill_pid_signal

```js
const groupId = 123456
process.kill(-groupId, 'SIGINT')
```

ちなみに、シェルでCtrl-Cを押したときに、親・子・孫がもろとも終了されるのは、Ctrl-Cが送る`SIGINT`が親プロセスに対してではなく、プロセスグループに対して送られているからです。(要出典)

### detached = 別世帯を作る

今回やりたいことは、oya.jsのプロセスは生かしつつ、ko.jsとmago.jsを`kill()`したいことです。しかし、GPIDを指定した`kill()`では、oya.jsまで終了してしまいます。三者とも同じGPIDだからです:

```
PID   PPID  GPID  COMMAND
17553  3528 17553 node oya.js
17554 17553 17553 node ko.js
17555 17554 17553 node mago.js
```

ko.jsとmago.jsを別のGPIDを割り振る必要があります。それをするには、`fork()`のオプションに`detached`を指定します。

```oya.js
// 子プロセスを起動
const ko = require('child_process')
  .fork(__dirname + '/ko.js', [], {detached: true})
```

これを指定すると、ko.jsとmago.jsがいわば「別世帯」になり、別のプロセスグループに属するようになります。GPIDもoya.jsとは別のものが割り当てられているのが確認できます:

```bash
$ ps -xo pid,ppid,pgid,command | grep node | grep .js
PID   PPID  GPID  COMMAND
21404  3528 21404 node oya.js
21405 21404 21405 node ko.js
21406 21405 21405 node mago.js
```

### プロセスを子子孫孫殺すoya.jsの完成形

以上を踏まえて、oya.jsを子プロセス、孫プロセスを一括して終了できるように変更すると次のようになります:

```oya.js
console.log('oya.js: running')

// SIGINTを受け付けたとき
process.on('SIGINT', () => {
  console.log('oya.js: SIGINT')
  process.exit()
})

// プロセスが終了するとき
process.on('exit', () => {
  console.log('oya.js: exit')
})

// 子プロセスを起動
const ko = require('child_process')
  .fork(__dirname + '/ko.js', [], {detached: true}) // 重要な変更箇所！

// 3秒後にko.jsを終了する
setTimeout(() => {
  console.log('oya.js: ko.jsを終了させてます...')
  process.kill(-ko.pid, 'SIGINT') // 重要な変更箇所！
}, 30000)

// ko.jsが終了したとき
ko.on('exit', () => {
  console.log('> Ctrl-Cを押してください...')
})

// このプロセスがずっと起動し続けるためのおまじない
setInterval(() => null, 10000)
```

最後に、このoya.jsを実行して、ko.jsとmago.jsが一緒に終了しているか確認してみましょう:

```bash
$ node oya.js
oya.js: running
ko.js: running
mago.js: running
oya.js: ko.jsを終了させてます...
mago.js: SIGINT
ko.js: SIGINT
mago.js: exit
ko.js: exit
> Ctrl-Cを押してください...
^Coya.js: SIGINT
oya.js: exit
```

期待通り、ko.jsとmago.jsは同じタイミングで`SIGINT`を受け取り終了しています。oya.jsはCtrl-Cを押すまで生存していることも分かります。

以上、Node.jsの`child_process.fork()`で起動したプロセスを子子孫孫殺す方法についての説明でした。