20
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2020-04-07

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

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

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

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

起動した子プロセスは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になる)

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

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

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

まず、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プロセスに割り当てられていることが分かります:

$ 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()するだけになるので注意です。

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とは別のものが割り当てられているのが確認できます:

$ 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が一緒に終了しているか確認してみましょう:

$ 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()で起動したプロセスを子子孫孫殺す方法についての説明でした。

20
9
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?