開発中にプログラムが固まった時に kill プロセスID って打ったことがある方は多いかと思います
僕もそうで、 kill コマンドはプロセスを強制終了するためのコマンドだと思っていました

ですが puma のログローテートを設定している時に kill -HUP って出てきてなんだこれ?ってなりました
あれ?ログファイルを握り直すのに kill 使うの??プロセスを止めたいなんて思ってないよ???みたいな

なのでちゃんと調べてみました

TL;DR

  • kill コマンドはプロセスに対して「シグナル」を送信するためのコマンド
  • シグナルはプロセスがプロセス外でのイベントに対して対応するための機構
    • シグナルには定義があってそれごとにデフォルト動作がある
    • シグナルの動作は上書きすることも可能

今回のシチュエーション

logrotate で puma のログファイルをローテーションしたい、という状況でした
なので、いろいろとググって下記の設定をしました

lastaction
    puma_pid=/tmp/pids/puma.pid 
    test -s $puma_pid && /usr/bin/kill -HUP "$(cat $puma_pid)" 
endscript

これは logrotate がログファイルを切り替えた後に行う処理です
これをやらないと puma がログを吐き出すことができなくなってしまいます

今回のキモは kill -HUP ですね
puma を止めたいわけじゃない(ログを切り替えたい)のに kill してます
しかも謎の -HUP 指定もしてます

どういうことよ 🤔

kill コマンドの機能について

kill コマンドはプロセスを強制終了するためのコマンドだと思っていましたが、違う雰囲気を感じます
なのでちゃんと kill コマンドの man を見ました

kill コマンドは、指定したシグナルを指定したプロセスまたはプロセスグループへ送る。シグナルが指定されない場合、TERMシグナルを送る。

https://linuxjm.osdn.jp/html/util-linux/man1/kill.1.html

プロセスの強制終了なんて一言も書いてません
~~ なんて紛らわしいコマンド名でしょうか ~~
kill コマンドはシグナルをプロセスに送るコマンドです

「シグナル」と「プロセス」というキーワードが2つ出てきていますが
プロセスはあるプログラムを指すと理解しておきます
シグナルってなんぞや 🤔

シグナルとは

なんらかのイベントが起こったことをプロセスに通知するための機構です
イベントを受け取ったプロセスは、イベントの種類によってなんらかの処理をします
受け取る側のプロセスは自身の実行とは別で受け取ることができます

イメージは Javascript の addEventListener が近いと思います
なんらかのイベントが発生したらそれに対応する関数が実行されます
例えば下の場合です

var select = document.querySelector('select')  // L1
select.addEventListener('click', function() {  // L2
  console.log('clicked!')                      // L3
})
select.addEventListener('change', function() { // L5
  console.log('changed!')                      // L6
})

なんらかのイベントが起こったこと (L2) をプロセス (L1) に通知するための機構です
イベントを受け取ったプロセスは、イベントの種類 (L2, L5) によってなんらかの処理 ( L3, L6 ) をします

シグナルが登場する場面

普段の開発でシグナルの送信を意識していない方も多いかと思います
ですが、以外とシグナルを送信している機会は意外とあります

例にあげた kill もその1つです
オプションに -KILL-9 をつけて実行すると対象のプロセスに対して SIGKILL というシグナルを送信しています
kill (オプションなし) の場合は SIGTERM というシグナルを送信しています

また terminal でよく使う Ctrl-c もプロセスに対してシグナルを送信しています
Ctrl-c を入力するとプロセスに対して SIGINT というシグナルが送信されます
同じく Ctrl-zSIGTSTP を送信しています

これは javascript だとブラウザ上で何かのアクションが起きた時(クリックとかスクロールとか)をイメージしてもらうと近いかと思います
その場合イベントハンドラ console.log('clicked!') とかするところはどうなるのでしょうか 🤔

シグナル送信後の挙動

デフォルトの挙動

シグナルを受け取ったプロセスは、そのシグナルの種類によってなにかしらの処理をします
基本的にはシグナルごとに定義されているデフォルトの処理が行われます
一部を紹介するとこうなっています

シグナルの種類 デフォルトの動作 備考
SIGINT 終了 キーボードからの割り込み (Interrupt)
SIGTSTP 停止 terminal から入力される時停止
SIGKILL 終了 kill シグナル
SIGTERM 終了 termination 終了シグナル
SIGHUP 終了 terminal のハングアップや制御しているプロセスの死

http://linuxjm.osdn.jp/html/LDP_man-pages/man7/signal.7.html

プログラムを停止したい時に Ctrl-c を押せばいい、というのはこういうことですね
つまり Ctrl-c を押すと SIGINT シグナルがプロセスに送信され、デフォルトの挙動である「終了」処理を行うからです

デフォルト挙動の上書き

SIGINT を受け取ったプロセスは終了処理を行いますが、これはあくまでもデフォルトで定義された挙動でしかなく別の処理をさせる(キャッチ)こともできます

例えば less コマンドのの場合、 less を起動してから Ctrl-c を押下してもプロセスは終了しません
これはデフォルトの終了という挙動を上書きしているからです
つまり、あるシグナルに対してなにをするのかはそのプログラムの実装による、ということです
逆に、プログラムはシグナルを受け取ることで外部の任意のタイミングで規定の動作を受け付けることができる、とも言えます

javascript でいえば、 form の submit をフックして submit 前にバリデーションを挟む、的な挙動をさせるのに似てますね

var form = document.querySelector('form')
form.addEventListener('submit', function(evt) {
  if (do_validations() == false) {
    evt.preventDefault();
  }
})

補足ですが、シグナルはキャッチする以外にブロックしたり無視することができます
ただし SIGKILLSIGSTOP はキャッチ、ブロック、無視することはできません

kill -HUP の解読

さて、話を戻して今回のシチュエーション kill -HUP がなんなのかという件です
具体的には puma のログファイルに対する logrotate の中で、 puma にログファイルを握り直してもらうために kill -HUP {puma のプロセス} をします

まず kill -HUP です
kill コマンドを使って puma に対して SIGHUP のシグナルを送信しています
SIGHUP は前述の通り terminal のハングアップや制御しているプロセスの死を表し、デフォルトではプロセスは終了します

ですが実際には puma のプロセスは終了しないので意味不明です 🤔
なので落ち着いて puma の wiki を参照しましょう

Puma cluster responds to these signals:
HUP reopen log files defined in stdout_redirect configuration parameter.

puma は HUP を受け取るとログファイルをリオープンします
とあります

つまり puma は SIGHUP をキャッチして、プロセスの終了をする代わりにログファイルを握り直してくれます

ということで、これで理解できましたね
「logrotate の中で puma に対して SIGHUP シグナルを送信することで、
puma が SIGHUP シグナルに対して独自に定義した『ログファイルをリオープンする』という動作をさせる」
という設定でした

補足

シグナルの値について

シグナルには名前とともに値が割り振られています

シグナルの種類 デフォルトの動作 備考
SIGHUP 1 終了 terminal のハングアップや制御しているプロセスの死
SIGINT 2 終了 キーボードからの割り込み (Interrupt)
SIGKILL 9 終了 kill シグナル
SIGTERM 15 終了 termination 終了シグナル

kill コマンドを使ってプロセスを停止させるときに kill -KILL としたり kill -9 としたりすると思います
kill コマンドはオプションでシグナル名かシグナル値を受け取ります
なので -KILL-9 はどちらも SIGKILL を送信するので同じ挙動になります

シグナルをキャッチする

C 言語でシグナルをキャッチするためには sigaction 関数を使います
https://linuxjm.osdn.jp/html/LDP_man-pages/man2/sigaction.2.html
signal 関数もあるんですが、歴史的背景から非推奨なようです

ruby の場合は Signal.#trap でキャッチできます
https://docs.ruby-lang.org/ja/latest/method/Signal/m/trap.html
puma だとこんな感じ
https://github.com/puma/puma/search?q=signal+trap&unscoped_q=signal+trap

nginx の logrotate の場合

puma で logrotate をするように nginx でも logrotate していたのでついでに調べてみました
ec2 で nginx をインストールすると logrotate が自動で設定されていました

postrotate
    /etc/init.d/nginx reopen_logs
endscript

これだとわかりにくいので公式サイトを参照すると

NGINX will re-open its logs in response to the USR1 signal.
$ kill -USR1 `cat master.nginx.pid`

https://www.nginx.com/resources/wiki/start/topics/examples/logrotation/

もうわかりますね
SIGUSR1 シグナルを送信しています

SIGUSR1 は初出ですが、デフォルトの動作は「終了」でこれは「ユーザー定義シグナル1」というシグナルです
好きに使っていいよ、って感じですかね
nginx でログローテートする場合は puma より素直でに好きに使っていいよシグナルを介してログファイルを握り直してくれるみたいです

unicorn の logrotate の場合

puma と nginx を見てきたのでついでに unicorn も見てみます

公式サイトのシグナルハンドリングの項に書いてありました

USR1 - reopen all logs owned by the master and all workers See Unicorn::Util.reopen_logs for what is considered a log.

https://bogomips.org/unicorn/SIGNALS.html

ということで nginx と同じく USR1 シグナルを送信すれば良さそうですね

まとめ

シグナルと kill コマンドについて調べてみました
出会う機会は少なかったりなんか難しそうだったのでなんとなくで流してしまっていましたが、ちゃんと調べてみればなんてことはなかったです

参考