pcntl 拡張と signal

この記事は 闇PHP Advent Calendar 2015 5日目 です

pcntl 拡張で signal を扱う

php で signal を扱うためには pcntl 拡張を利用します。

使い方は簡単で、pcntl_signal 関数で トラップしたいシグナル番号とコールバックされる関数(シグナルハンドラ)を登録するだけです。
ただ、現在の pcntl 拡張 では ZEND_TICKS と tick 関数 で解説した tick を用いて実装されているため、declare を用いて ticks を 1以上に設定する必要があります。


code.4 を実行して Ctrl+C を押すと、SIGINT をトラップし、"interrupted" をecho 後に終了します。

code.4
<?php
declare(ticks=1);
pcntl_signal(
    SIGINT, 
    function() {
        echo "interrupted", PHP_EOL;
        exit();
    }
);

while(true) {
    echo 1, 2, 3;
}

tick を使って実装していると言うことは、シグナルハンドラはステートメントステートメントの間でのみ呼ばれる (ステートメント実行中に呼ばれることはない) ということです。
つまり前述の code.4 は 1 や 2 が出力されたタイミングで終了することはなく、必ず 1 と 2 と 3 が出力された後に終了します。


一般的に signal というのは、プログラムの実行中どのようなタイミングでもトラップされる(割り込みが発生する)ものです。
しかし、php の signal 実装においては、トラップされたタイミングでシグナルハンドラを呼び出すのではなく、一時的にトラップしたシグナルを貯めておき、tick のタイミングでシグナルハンドラを呼び出す形になっています。

この仕組みによりデッドロックやレースコンディションの発生を抑制し、phpプログラムの誤動作を防いでいます。

php の signal 実装

では、php の signal はどのように実装されているのでしょうか。

pcntl_signal PHP関数 は、第2引数で渡されたシグナルハンドラをシグナル番号をキーとしたシグナルテーブル ( PCNTL_G(php_signal_table) ) に登録します。
また、OS に対して sigaction システムコールを発行し、第1引数で指定したシグナルが発生した際に pcntl_signal_handler C関数(pcntl 拡張内で実装されている) を呼び出すように登録します。
pcntl_signal_handler が行っているのは、ペンディングシグナルキュー という名の queue に、受信したシグナル番号を格納するだけです。

前述の通り、シグナルを受け取った際に直接シグナルハンドラがコールバックされるわけではないところが重要です。
ちなみにこの ペンディングシグナルキュー、最初に pcntl_signal を呼び出した際にメモリを確保するのですが、その数は32固定。空きがない状態でシグナルを受け取ると、そのシグナルは格納されず無視されてしまいます。


貯まる一方では意味がないので、このキューから値を取り出す関数も当然存在します。それが pcntl_signal_dispatch 関数。これも pcntl 拡張内で実装されたCの関数です。pcntl_signal_dispatch はpcntl拡張が読み込まれたとき*1 にtick 関数として登録*2されています。

pcntl_signal_dispatch は、ペンディングシグナルキュー に貯まっているシグナル番号を取り出し、シグナルテーブルに登録されているシグナルシグナルハンドラを順次コールバックします。
これが php の signal 実装の全体像です。


なお、シグナルハンドラのコード中も tick は有効です。しかし、pcntl_signal_dispatch は再入を検知して抑止する*3ため、シグナルハンドラの中でシグナルハンドラが呼ばれることはありません。

補足

実は signal 実装はとてもシビアな処理であり、上記の説明は細かい処理(シグナルのマスク処理等)を省略しています。
このあたりはまたいずれ。

*1:MINIT時

*2:php_add_tick_function

*3:PCNTL_G(processing_signal_queue) をフラグとして利用