Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
PHPとシグナル その裏側
Search
do_aki
October 08, 2017
Programming
0
57
PHPとシグナル その裏側
2017/10/08 PHPカンファレンス 2017
do_aki
October 08, 2017
Tweet
Share
More Decks by do_aki
See All by do_aki
PHP と SAPI と ZendEngine3 と
do_aki
0
49
PHP AST 徹底解説
do_aki
0
42
Mastering mysqlnd
do_aki
0
18
Other Decks in Programming
See All in Programming
AWS IaCの注目アップデート 2024年10月版
konokenj
3
3.3k
3 Effective Rules for Using Signals in Angular
manfredsteyer
PRO
1
100
NSOutlineView何もわからん:( 前編 / I Don't Understand About NSOutlineView :( Pt. 1
usagimaru
0
330
Kaigi on Rails 2024 〜運営の裏側〜
krpk1900
1
190
レガシーシステムにどう立ち向かうか 複雑さと理想と現実/vs-legacy
suzukihoge
14
2.2k
Generative AI Use Cases JP (略称:GenU)奮闘記
hideg
1
290
Creating a Free Video Ad Network on the Edge
mizoguchicoji
0
110
聞き手から登壇者へ: RubyKaigi2024 LTでの初挑戦が 教えてくれた、可能性の星
mikik0
1
130
Webの技術スタックで マルチプラットフォームアプリ開発を可能にするElixirDesktopの紹介
thehaigo
2
1k
Streams APIとTCPフロー制御 / Web Streams API and TCP flow control
tasshi
2
350
詳細解説! ArrayListの仕組みと実装
yujisoftware
0
580
TypeScriptでライブラリとの依存を限定的にする方法
tutinoko
2
660
Featured
See All Featured
Making Projects Easy
brettharned
115
5.9k
GitHub's CSS Performance
jonrohan
1030
460k
Product Roadmaps are Hard
iamctodd
PRO
49
11k
実際に使うSQLの書き方 徹底解説 / pgcon21j-tutorial
soudai
169
50k
Designing for humans not robots
tammielis
250
25k
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
6
410
Building Flexible Design Systems
yeseniaperezcruz
327
38k
Teambox: Starting and Learning
jrom
133
8.8k
Bootstrapping a Software Product
garrettdimon
PRO
305
110k
Building Your Own Lightsaber
phodgson
103
6.1k
Fantastic passwords and where to find them - at NoRuKo
philnash
50
2.9k
Stop Working from a Prison Cell
hatefulcrawdad
267
20k
Transcript
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)