PHP のシグナルハンドラのいろいろ

ここ数年、PHP でシグナルを使う機会が多かったので、気づいた点などを整理してみます。

本当のシグナルハンドラと PHP のシグナルハンドラ

pcntl_signal() で登録する PHP のシグナルハンドラは、本当の意味でのシグナルハンドラではありません。なので、シグナルハンドラから非同期シグナルで安全では無い関数でも呼び出すことが出来ます。

<?php
declare (ticks = 1);

pcntl_signal(SIGTERM, function ($signo) {
    printf("%d -> %s\n", $signo, "だが断る");
});

posix_kill(posix_getpid(), SIGTERM);

exit(0);

本当のシグナルハンドラは次の箇所で定義されており、所謂リンクドリストに受信したシグナル番号を記録しています。

pcntl.c:1216

static void pcntl_signal_handler(int signo)
{
	 :
	 
	PCNTL_G(spares) = psig->next;

	psig->signo = signo;
	psig->next = NULL;
	
	 :
}

非同期シグナルで安全な関数 とは、シグナルハンドラから呼び出すことが可能な関数のことで、詳細は下記に書かれています。

PHP のシグナルハンドラの呼び出し

pcntl_signal() で登録した PHP のシグナルハンドラは、次の関数の実行時にリストに保存されたシグナル番号が順番に呼び出されます。

pcntl.c:1229

void pcntl_signal_dispatch()
{
	:
}

こは関数は PHPpcntl_signal_dispatch() 関数としても定義されており、PHP のコードから明示的に呼び出すことが出来ます(PHP 5.3)。

pcntl.c:907

PHP_FUNCTION(pcntl_signal_dispatch)
{
	pcntl_signal_dispatch();
	RETURN_TRUE;
}

もしくは、モジュールの初期化処理で tick 関数として登録されているので、declare (ticks = 1) とかしておけば 1 ステップごとに自動的に実行されます。

pcntl.c:502

PHP_MINIT_FUNCTION(pcntl)
{
	 :
	 
	php_add_tick_function(pcntl_signal_dispatch);
	
	 :
}

よって、次のコードは大体同じ結果になります。

<?php
$term = false;

pcntl_signal(SIGINT, function ($signo) use (&$term) {
    $term = true;
});

declare (ticks = 1)
{
    while (!$term)
    {
        echo ".";
        usleep(10000);
    }
}
<?php
$term = false;

pcntl_signal(SIGINT, function ($signo) use (&$term) {
    $term = true;
});

for (; !$term; pcntl_signal_dispatch())
{
    echo ".";
    usleep(10000);
}

後者だと ループの繰り返し時にしか pcntl_signal_dispatch() が呼ばれませんが、それ以外のタイミングでシグナルハンドラが呼ばれても余り意味ありませんよね?($termtrue にセットされるのが usleep の直前でも直後でも結果は変わらない)

大量にシグナルを受けると取りこぼす

リストに記録できるシグナルの数は有限個(32)です。そのため pcntl_signal_dispatch() の呼び出しの間隔に 33 個以上のシグナルを受けると取りこぼします。

本当のシグナルハンドラは実行されていたとしても、リストに空きが無いので受信したシグナル番号を保持できないのです。

pcntl.c:1211

	psig = PCNTL_G(spares);
	if (!psig) {
		/* oops, too many signals for us to track, so we'll forget about this one */
		return;
	}

例えば下記のコードだと、自分に USR1 を送っているので 1 ループで終了するはずですが、pcntl_signal_dispatch() の呼び出しの間隔で SIGTERM を 32 個受信しているため、リストがいっぱいになって USR1 のシグナルハンドラが実行されず、ループが終了しません。

<?php
$term = false;

pcntl_signal(SIGUSR1, function ($signo) use (&$term) {
    $term = true;
});

pcntl_signal(SIGTERM, function ($signo) {
    echo ".";
});

for (; !$term; pcntl_signal_dispatch())
{
    for ($i=0; $i<32; $i++)
    {
        posix_kill(posix_getpid(), SIGTERM);
    }

    posix_kill(posix_getpid(), SIGUSR1);

    usleep(10000);
}

SIGTERM の回数を 31 回にすれば 1 ループで止まります。

<?php
$term = false;

pcntl_signal(SIGUSR1, function ($signo) use (&$term) {
    $term = true;
});

pcntl_signal(SIGTERM, function ($signo) {
    echo ".";
});

for (; !$term; pcntl_signal_dispatch())
{
    for ($i=0; $i<31; $i++)
    {
        posix_kill(posix_getpid(), SIGTERM);
    }

    posix_kill(posix_getpid(), SIGUSR1);

    usleep(10000);
}

pcntl_signal() でハンドラを登録していないシグナルについては、リストにシグナル番号を保存する処理自体が無いので、上の例のような PHP 側の問題による取りこぼしは起こりません。

例えば、次のコードでは SIGHUP を PHP で処理していないので、SIGTERM を何回受信したとしても SIGHUP で終了します。

<?php
$term = false;

pcntl_signal(SIGUSR1, function ($signo) use (&$term) {
    $term = true;
});

pcntl_signal(SIGTERM, function ($signo) {
    echo ".";
});

for (; !$term; pcntl_signal_dispatch())
{
    for ($i=0; $i<64; $i++)
    {
        posix_kill(posix_getpid(), SIGTERM);
    }

    posix_kill(posix_getpid(), SIGHUP);

    usleep(10000);
}

シグナルハンドラ中はシグナルがブロックされえる

pcntl_signal_dispatch() の処理中は全てのシグナルがブロックされます。

pcntl.c:1239

void pcntl_signal_dispatch()
{
	 :
	
	/* Mask all signals */
	sigfillset(&mask);
	sigprocmask(SIG_BLOCK, &mask, &old_mask);
	
	 :
	
	/* return signal mask to previous state */
	sigprocmask(SIG_SETMASK, &old_mask, NULL);
}

そのため、次のコードで無敵モード中はシグナルを受けても死にません(SIGKILL や SIGSTOP はブロック出来ないので除きます)。

<?php
pcntl_signal(SIGUSR1, function ($signo) {

    echo "無敵モード\n";

    for ($i=5; $i>=0; $i--)
    {
        sleep(1);
        echo "$i\n";
    }
});

for (;;)
{
    echo "通常モード\n";

    for ($i=5; $i>=0; $i--)
    {
        sleep(1);
        echo "$i\n";
    }

    posix_kill(posix_getpid(), SIGUSR1);
    pcntl_signal_dispatch();
}

ただし、シグナルはペンディングになっているだけなので、無敵モードが解除された瞬間死にます。