本稿では、Node.jsにて、子プロセス、そこから派生した孫プロセス、さらにそこから派生したひ孫プロセス……を、一括して終了する方法を説明します。
※説明にあたって、実行環境はUNIX/Linuxを前提にしています。
子プロセスを殺しても、孫プロセスは死なない
Node.jsのchild_process.fork()は、子プロセスを起動できて便利です。子プロセスの中で、fork()
を使って、孫プロセスを起動することもでき、さらに、孫プロセスでfork()
して、ひ孫プロセスを、といった具合に子プロセスはネストして起動することができます。
起動した子プロセスはsubprocess.kill()で終了することができます。しかし、これは直接の子プロセスしか殺すことができません。どういうことかというと、
- oya.js が ko.js のプロセスを起動する。
- ko.js が mago.js のプロセスを起動する。
- このとき、 oya.js が ko.js のプロセスを
kill()
したとする。 - ko.js は終了する。
- mago.js は生存する。 (※このとき、 mago.js はinitプロセスの養子に出され、親pidは1になる)
といった事態が発生します。
孫プロセスが残存するサンプルコード
上のようなシナリオを再現できるコードを書いてみたいと思います。
まず、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')})// 子プロセスを起動constko=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:
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:
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()
するだけになるので注意です。
constgroupId=123456process.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
を指定します。
// 子プロセスを起動constko=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を子プロセス、孫プロセスを一括して終了できるように変更すると次のようになります:
console.log('oya.js: running')// SIGINTを受け付けたときprocess.on('SIGINT',()=>{console.log('oya.js: SIGINT')process.exit()})// プロセスが終了するときprocess.on('exit',()=>{console.log('oya.js: exit')})// 子プロセスを起動constko=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()
で起動したプロセスを子子孫孫殺す方法についての説明でした。