Successfully reported this slideshow.
Your SlideShare is downloading. ×

PHPとシグナル、その裏側

Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Loading in …3
×

Check these out next

1 of 62 Ad
Advertisement

More Related Content

Viewers also liked (16)

Advertisement

Similar to PHPとシグナル、その裏側 (20)

Advertisement

Recently uploaded (20)

PHPとシグナル、その裏側

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

Editor's Notes

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

×