PHPとシグナル
その裏側
2017/10/08
PHPカンファレンス 2017
do_aki
@do_aki
@do_aki
http://do-aki.net/
突然ですが
<?php
// loop.php
while(true) {
sleep(1);
}
// CLIで実行するとどうなる?
答え:無限ループ
※終了するにはCtrl+C
※終了するにはCtrl+C
(大事なことなのでもう一度)
なぜ、無限ループが
終了できるのか?
=> シグナルを受信したから
PHPとシグナルその裏側 目次
第1章
OSとシグナルのカンケイ
第2章
PHPにおけるシグナルハンドリング
第3章
PHPのシグナルにまつわる四方山話
環境について
• この資料は php 7.1.10 のコードを元に書いて
います
• OS の処理については Linux Kernel 2.6.15
(Linuxカーネル2.6解読室)をもとに、4.12 の
コードを参照しています(UNIX 系OSであれば大き
く変わらないと思うけど、細部は異なるかも)
OSとシグナルのカンケイ
「シグナルを受信する」ということ
• php 等のプログラムが受信する仕組みを用意
しているわけではない
• OS による強制的な割り込みによって
受信させられるというほうが近い
プログラムの実行
シグナル受信 OS による
割込み処理
どうやって割り込むのか
• 1つの CPU は同時に1つのコードしか実行でき
ないが、OS は複数のプログラムを同時に動かし
ているように見せるために短い時間でプロセスを
切り替えている
• OSは本来実行されるコードの合間にシグナルを処
理するためのコードを挟み込むことができる
プログラムは細切れに実行されている
Process A
Process B
Process C
Kernel(OS)
時間
タイマー割込み システムコール
[プロセスに制御を戻す前に、シグナル処理を挟み込むことがある]
シグナルとは
• (ほかの)プログラムやハードウェア等で発生した「何か」
を通知するための仕組み
• 起こらないかもしれない「何か」に備えて対策しておくのは
大変
– 割り込みという実装が便利
• 「何か」を番号と名前で区別
– 一般的に使われるシグナル番号は1-32
– ただし、番号は環境によって異なる場合がある
シグナルの種類(一部)
• SIGHUP (端末から切り離されるときなど)
• SIGINT (キーボードによる割込み / Ctrl+C)
• SIGTERM (終了)
• SIGKILL (強制終了)
• SIGSTOP (一時停止)
• SIGCONT (再開)
• SIGPIPE (パイプ破壊/切断ソケットへの出力等)
• SIGUSR1/SIGUSR2 (ユーザ定義シグナル)
• SIGWINCH (端末サイズ変更)
• SIGALRM (タイマー)
• SIGCHILD (子プロセスの状態変化)
• SIGSEGV (セグメンテーションフォールト)
• SIGFPE (浮動小数点演算例外/ゼロ除算など)
シグナルの利用例
• デーモンや時間のかかるコマンドの制御
– Webサーバの安全な停止や再起動
• apache httpd や nginx に SIGWINCH を送ると、リクエスト
の終了を待ってプログラムを終了
• ただし、例えば同じ SIGUSR1 でも、 apache htttpd の場合
は graceful restart するのに対し、 nginx は log re-
open ということもあるので注意
– dd 実行中に SIGUSR1 を送ると進捗表示
• 子プロセスの管理
– 子プロセスの状態が変化すると親プロセスはSIGCHILDを
受信
kill コマンド
• プロセスにシグナルを送るコマンド
• デフォルトで SIGTERM を送信
• 利用可能なシグナルは `kill –l` で確認できる
• pid:1234にSIGUSR1を送信 => `kill –USR1 1234`
• pid に -1 を指定すると pid:1 以外のすべてのプロ
セスに送信 (危険!)
シグナル受信時の
デフォルトの動作(一部)
• SIGHUP -> プログラム終了
• SIGINT -> プログラム終了
• SIGTERM -> プログラム終了
• SIGKILL -> プログラム終了
• SIGPIPE -> プログラム終了
• SIGUSR1 -> プログラム終了
• SIGUSR2 -> プログラム終了
• SIGWINCH -> プログラム終了
• SIGALRM -> プログラム終了
• SIGCHILD -> 無視
• SIGSEGV -> コアダンプして終了
• SIGFPE -> コアダンプして終了
まぁ、だいたい終了するんだわ
適切に制御しないと、
簡単に死ぬ
シグナル受信時の動作を制御
• プログラムは、シグナル受信時の動作をあらかじめ選択する
ことができる
– デフォルト
– 無視
– シグナルハンドラの実行(プログラム側で用意した処理を実行)
• ただし、 SIGKILL, SIGSTOP については動きを変えること
はできない(真の無限ループは作れないのです)
第1章まとめ
• シグナルは、OSによって制御されたソフトウェア
割込み
• 適切にハンドリングしないと、ほとんどのシグナ
ルは、プログラムを終了させる
• SIGKILL,SIGSTOP を除き、シグナルを受け取っ
た際の動作を デフォルト、無視、ユーザ定義 い
ずれかからあらかじめ指定することができる
PHPにおける
シグナルハンドリング
pcntl 拡張
• PHP でシグナルハンドリングするためには
pcntl拡張 が必要
• Windows では使えない
• `--enable-pcntl` コンパイルオプション
を指定してコンパイルしてあること
– コンパイル済みのバイナリの場合、CLI/CGI 以
外は無効になっている
pcntl_signal 関数
• シグナル受信時の動作を設定する
• $signo に シグナル番号 (SIGINT 定数な
ど) を指定
bool pcntl_signal (
int $signo ,
callable|int $handler ,
[bool $restart_syscalls = true]
)
$handler
• SIG_DFL (デフォルトの動作)
• SIG_IGN (シグナルを無視する)
• callable型 (PHPシグナルハンドラ)
• PHPシグナルハンドラは以下の引数を受け取る関数
– 第1引数: シグナル番号(signo)
– 第2引数: シグナル種類ごとの追加情報(siginfo) 7.1~
$restart_syscalls
• あまり気にする必要はない
• 待機系のシステムコール実行中にシグナ
ルを受信した際、そのシステムコールが
自動的に再開されるかどうか。 (true なら
sigaction 構造体の sa_flags に SA_RESTART が設定される)
• 一部のシステムコールは常に失敗する
• 普段何のシステムコールが呼ばれている
かを気にしたことがなければ無用の長物
かなと
// SIGUSR1 を無視
pcntl_signal(SIGUSR1, SIG_IGN);
// SIGINT のシグナルハンドラを設定
$terminate = false;
pcntl_signal(
SIGINT,
function ($signo, $siginfo) use(&$terminate) {
$terminate = true;
}
);
while(true) {
// 何らかの処理
if ($terminate) {
exit(); // 安全なタイミングで終了
}
}
[ 注意 ]
これだけでは、
PHPのシグナルハンドラは
実行されない
pcntl_signal 関数の動作
• $handler として callable型を渡した際、
pcntl_signal は、OS に対してシグナル受信時に
$handler が呼ばれるように設定しているわけではな
い
• 実際に OS に登録されるシグナルハンドラは
pcntl_signal_handler というC関数
• $handler は別途 php 内部のシグナルテーブルに記
録される
pcntl_signal 呼び出し時
PHP_FUNCTION
(pcntl_signal)
SIGINT のシグナルハンドラと
して pcntl_signal_handler
(Cの関数) をOSに登録
pcntl_signal(SIGINT, $handler)
SIGHUP NULL
SIGINT $handler
SIGALRM NULL
……
php signal table
内部のテーブルに
$hander を登録
[PHP Script]
[PHP Internal]
シグナル受信時
pcntl_signal_handler
内部のキューにシグナル番号を追加するだけ
[PHP Script]
[PHP Internal]
signo
signal queue
(シグナル受信時 PHP Script 側への影響は一切ない)
OSのシグナルハンドラの制約
• 安全に利用可能なシステムコールが制限
されている
– 安全でないシステムコールを呼び出した場合の動作
は未定義
• デッドロックやレースコンディションを
起こすことがある
– php スクリプトの動きが意図しない挙動になる可能
性がある
シグナルディスパッチ
• pcntl拡張は、安全なタイミングで php のシグナルハンドラ
($handler)が実行されるように調整
• pcntl_signal_dispatch (C関数)で実装されている
• pcntl_signal_dispatch は、何もしなければ呼ばれない
=> 実行のタイミングを PHPスクリプト側であらかじめ設
定しておく必要がある
シグナルディスパッチ
pcntl_signal_dispatch
$handler(SIGINT, [$siginfo])
SIGHUP NULL
SIGINT $handler
SIGALRM NULL
……
php signal table
[PHP Script]
[PHP Internal]
signal queue
signo
取り出したシグナ
ル番号で参照、該
当する関数を実行
シグナルディスパッチのタイミング
• pcntl_signal_dispatch (php関数)を
呼び出したとき
• N回 tick する毎 (declare(tick=N))
• pcntl_async_signals(true) を呼び出
した後、ほぼ常に
手動
自動
pcntl_signal_dispatch()
• PHP スクリプトから呼び出せる同名の関数が、 内部の
pcntl_signal_dispatch を呼ぶラッパーとなっている
• シグナルを処理したいタイミングに都度記述する必要がある
• 5.3 以上で利用可能になった
• tick を利用せずにディスパッチするための仕組みとして導
入された
https://marc.info/?l=php-internals&m=121716684606195
https://github.com/php/php-src/commit/204fcbe5d3ffb4a9c1383e39f7549b8326801894
tick
• 1ステートメント実行する毎(※)に発生するイベント
(php の機能)
• `declare(ticks=N)`を宣言することで有効になり、
N回 tick するたびに、あらかじめ登録しておいた処
理が実行される
• php スクリプトからは register_tick_function
を使って実行される関数やメソッドを登録できる
※厳密には、tick されないステートメントもあるが、大体はセミコロン毎と考えてよい
tick を利用した
シグナルディスパッチ
• pcntl 拡張は初期化時、tickを利用するかどう
かに関わらず pcntl_signal_dispatch (C関数)
が実行されるよう登録している (4.3-)
• tick が有効な範囲において Nステートメントご
とにディスパッチされる
• 現存するシグナルディスパッチの仕組みで最古
declare(ticks=1)declare(ticks=1);
echo 1;
echo 2;
echo 3;
pcntl_signal_dispatch();
echo 1;
pcntl_signal_dispatch();
echo 2;
pcntl_signal_dispatch();
echo 3;
pcntl_signal_dispatch();大体同じ
tick の有効範囲
• declare は、ファイルの先頭に記述するか、あるい
はブロックで指定
• ファイルを越えて有効になる
ことはない (tick の有効性は
コンパイル時に確定するため)
• 関数を呼び出しても、呼び出し
た先が tick 有効範囲外なら
ば、ディスパッチされない
declare(ticks=1) {
// tick 有効
func();
}
function func() {
// tick 無効
}
シグナルがディスパッチされない
ことによる問題
• pcntl拡張 が保持できるシグナルは 32個だけ (signal
queue の数が固定)
• 長時間ディスパッチされないと、超過した分のシグナルは捨
てられる
• signal queue にシグナルがたまった状態で fork すると、
子プロセスで受け取っていないはずのシグナルを処理するこ
とも
pcntl_async_signals(true)
• これを実行しておくと、`pcntl_async_signals(false)`
しない限り、ほぼ常にディスパッチされるイメージ
• (実際には、シグナルを受信したら PHP VM の各命令実行毎
にディスパッチされる)
• 7.1 から利用可能
• tick よりも細かい粒度でディスパッチされるが、タイムア
ウトを実装するための仕組みを流用しているため、低負荷。
pcntl_async_signals(true)時の裏の動き
• シグナルを受信すると、EG(vm_interrupt) フラグ
を立てる
• PHP VM が1命令実行するたびに、
EG(vm_interrupt) フラグをチェックし、立ってい
れば、zend_interrupt_function を実行
• pcntl 拡張は、初期化時に
zend_interrupt_function をフックし、そこで
pcntl_signal_dispatch を呼んでいる
pcntl_async_signals(true)時の
シグナル受信
pcntl_signal_handler
内部のキューにシグナル番号を追加し、
vm_interrupt フラグを立てる
[PHP Script]
[PHP Internal]
signo
signal queue
(シグナル受信時 PHP Script 側への影響は一切ない)
vm_interrupt
このフラグを PHP VM
が都度確認している
ベンチマーク
php ソースコード付属の Zend/bench.php
1. そのまま実行 (normal)
2. `declare(ticks=1)` を先頭に付与して実行(tick)
3. `pcntl_async_signals(true)` を先頭に付与して実行 (async)
Total (合計秒) をスコアとし、10回計測した平均を比較した
(VPS 上での実行なのであくまで参考程度に)
結果
• normal と async の差は誤差範囲内(のはずだけどなぜか async のほうが
早くなることが多い。。。)
• tick が明らかに遅いことが明白
1.9944
(100%)
2.5078
(126%)
1.9558
(98%)
0
0.5
1
1.5
2
2.5
3
normal tick async
bench.php (Total)
第2章まとめ
• PHP では `pcntl_signal` でシグナルを制御できる
• PHPのシグナルハンドラは、実行タイミングを調整す
ることで安全に動作するように制御されている
• 歴史的な事情により、シグナルディスパッチのタイミ
ングを制御する方法は複数ある
• 7.1以降は `pcntl_async_signals(true)` のみで
OK
PHPのシグナルにまつわる
四方山話
php のタイムアウト
• max_execution_time や set_time_limit で設定できる
• php スクリプト自身が使った処理時間が指定時間を経過する
と `PHP Fatal error: Maximum execution time of X
second exceeded`
(Windows の場合は、処理時間ではなく経過した時間)
• 他のプログラミング言語ではあまりみない
php のタイムアウト2
• Linux 系ではインターバルタイマーのプロファイルという仕
組みを利用
• プログラムが一定時間CPUを使うと SIGPROF を受信
• `set_time_limit(0)` してても、SIGPROF を受け取ると
timeout
• SIGPROF を 無視したり、シグナルハンドラを設定すると
timeout しなくなる
php からシグナルを送る方法
• posix 拡張 (Windows環境を除く)
• 自分自身に送ることもできる
`posix_kill(getmypid(), SIGPROF)`
bool posix_kill (int $pid , int $sig)
pcntl_alarm
• 指定秒数後に SIGALRM が自身に送られてくるよ
うに設定する
• `pcntl_alarm(10)`
• 同期処理のタイムアウトに利用できて便利
• SIGALRM のデフォルトの動作はプログラム終了な
ので、 SIGALRM を適切にハンドリングしておか
ないとただの時限爆弾
SIGBABY
• pcntl拡張は OS (というか posix) に定義されてい
ないシグナルが存在する
• 番号としては SIGSYS と同じ
• コミットログとは直接関係ない修正なので、誤って混
入したか、あるいはDerick氏によるjokeではないか
とのこと
– https://github.com/php/php-
src/commit/ea83d64507b6470eb654fbd75e614319abb4
03ed#diff-7185d47849fdb94217f22977d69e85b1R158
– https://stackoverflow.com/a/18584728
シグナルのマスク
• 特定のシグナルを一時的に保留することができる
(シグナルをマスクする)
• `pcntl_sigprocmask` で設定
• pctrl 拡張ではなく OS がシグナルを保留する
• 保留を解除すると保留中に受け取っていたシグナ
ルが送信されてくる
ZEND_SIGNAL
• 7.1 以降は ZEND_SIGNAL がデフォルトで有効
• pcntl拡張 <-> ZEND_SIGNAL <-> OS
• OS に設定するシグナルハンドラをさらにフック
し、タイミングを制御
• php スクリプト側への影響はほとんどない
– SIGKILL, SIGSTOP に対して pcntl_signal
– 7.0まで:Warning、 7.1以降:Fatal Error
最後に
• php におけるシグナル制御を中心にシグ
ナルについて話しました
• php でシグナルを扱う必要が生まれた時
の一助になれば幸い
• もっと深い話は闇PHP勉強会等で……
(blank)
4.3以前のシグナルディスパッチ
• ZEND_EXT_STMT のタイミングでディスパッチ
• tick が tickを有効にしたときのみ
ZEND_TICKS を挟み込むのに対し、こちらは(コ
ンパイル時のオプションを設定することで、)
コード全体に作用
• pcntl拡張を組み込むだけでコード全体が速度低
下していた
$siginfo について
• PHPシグナルハンドラの第2引数として渡
される (7.1-)
• 環境によっては常に null
• シグナル種別ごとに異なる情報が array
となってやってくる
$siginfo について2
• 共通
– signo: int シグナル番号 (si_signo)
– errno: int エラー番号 (si_errno)
– code: int シグナルが送信された理由 (si_code)
• SI_USER(killコマンド等ユーザランドから送信)
• SI_KERNEL(カーネルから送信) など
• SIGCHLD の場合は、CLD_EXITED(子プロセスが通常終了),
CLD_KILLED(子プロセスがkill), CLD_STOPPED(プロセス
が停止)など
$siginfo について3
• SIGCHLD のみ
– status: int (si_status)
• 終了ステータス あるいは 状態が変化する原因となったシグ
ナル番号
– utime: float (si_utime)
– stime: float (si_stime)
– pid: int (si_pid) 子プロセスのpid
– uid: int (si_uid) 子プロセスの実ユーザID
$siginfo について4
• SIGUSR1/SIGUSR2 のみ
– pid: int (si_pid) 送信したプロセスのpid
– uid: int (si_uid) 送信したユーザID
• SIGILL/SIGFPE/SIGSEGV/SIGBUS のみ
– addr: float (si_addr) fault の発生したアドレス(なぜかzend_long でキャス
トしてから add_assoc_double_ex)
• SIGPOLL のみ
– band: int (si_band)
– fd: int (si_fd)

PHPとシグナル、その裏側

Editor's Notes

  • #46 いろんなマイクロベンチスクリプトを集めたもの。