Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PHPとシグナル その裏側

do_aki
October 08, 2017

PHPとシグナル その裏側

2017/10/08 PHPカンファレンス 2017

do_aki

October 08, 2017
Tweet

More Decks by do_aki

Other Decks in Programming

Transcript

  1. 環境について • この資料は php 7.1.10 のコードを元に書いて います • OS の処理については

    Linux Kernel 2.6.15 (Linuxカーネル2.6解読室)をもとに、4.12 の コードを参照しています(UNIX 系OSであれば大き く変わらないと思うけど、細部は異なるかも)
  2. プログラムは細切れに実行されている Process A Process B Process C Kernel(OS) 時間 タイマー割込み

    システムコール [プロセスに制御を戻す前に、シグナル処理を挟み込むことがある]
  3. シグナルの種類(一部) • SIGHUP (端末から切り離されるときなど) • SIGINT (キーボードによる割込み / Ctrl+C) •

    SIGTERM (終了) • SIGKILL (強制終了) • SIGSTOP (一時停止) • SIGCONT (再開) • SIGPIPE (パイプ破壊/切断ソケットへの出力等) • SIGUSR1/SIGUSR2 (ユーザ定義シグナル) • SIGWINCH (端末サイズ変更) • SIGALRM (タイマー) • SIGCHILD (子プロセスの状態変化) • SIGSEGV (セグメンテーションフォールト) • SIGFPE (浮動小数点演算例外/ゼロ除算など)
  4. シグナルの利用例 • デーモンや時間のかかるコマンドの制御 – Webサーバの安全な停止や再起動 • apache httpd や nginx

    に SIGWINCH を送ると、リクエスト の終了を待ってプログラムを終了 • ただし、例えば同じ SIGUSR1 でも、 apache htttpd の場合 は graceful restart するのに対し、 nginx は log re- open ということもあるので注意 – dd 実行中に SIGUSR1 を送ると進捗表示 • 子プロセスの管理 – 子プロセスの状態が変化すると親プロセスはSIGCHILDを 受信
  5. kill コマンド • プロセスにシグナルを送るコマンド • デフォルトで SIGTERM を送信 • 利用可能なシグナルは

    `kill –l` で確認できる • pid:1234にSIGUSR1を送信 => `kill –USR1 1234` • pid に -1 を指定すると pid:1 以外のすべてのプロ セスに送信 (危険!)
  6. シグナル受信時の デフォルトの動作(一部) • SIGHUP -> プログラム終了 • SIGINT -> プログラム終了

    • SIGTERM -> プログラム終了 • SIGKILL -> プログラム終了 • SIGPIPE -> プログラム終了 • SIGUSR1 -> プログラム終了 • SIGUSR2 -> プログラム終了 • SIGWINCH -> プログラム終了 • SIGALRM -> プログラム終了 • SIGCHILD -> 無視 • SIGSEGV -> コアダンプして終了 • SIGFPE -> コアダンプして終了 まぁ、だいたい終了するんだわ
  7. pcntl 拡張 • PHP でシグナルハンドリングするためには pcntl拡張 が必要 • Windows では使えない

    • `--enable-pcntl` コンパイルオプション を指定してコンパイルしてあること – コンパイル済みのバイナリの場合、CLI/CGI 以 外は無効になっている
  8. pcntl_signal 関数 • シグナル受信時の動作を設定する • $signo に シグナル番号 (SIGINT 定数な

    ど) を指定 bool pcntl_signal ( int $signo , callable|int $handler , [bool $restart_syscalls = true] )
  9. $handler • SIG_DFL (デフォルトの動作) • SIG_IGN (シグナルを無視する) • callable型 (PHPシグナルハンドラ)

    • PHPシグナルハンドラは以下の引数を受け取る関数 – 第1引数: シグナル番号(signo) – 第2引数: シグナル種類ごとの追加情報(siginfo) 7.1~
  10. $restart_syscalls • あまり気にする必要はない • 待機系のシステムコール実行中にシグナ ルを受信した際、そのシステムコールが 自動的に再開されるかどうか。 (true なら sigaction

    構造体の sa_flags に SA_RESTART が設定される) • 一部のシステムコールは常に失敗する • 普段何のシステムコールが呼ばれている かを気にしたことがなければ無用の長物 かなと
  11. // SIGUSR1 を無視 pcntl_signal(SIGUSR1, SIG_IGN); // SIGINT のシグナルハンドラを設定 $terminate =

    false; pcntl_signal( SIGINT, function ($signo, $siginfo) use(&$terminate) { $terminate = true; } ); while(true) { // 何らかの処理 if ($terminate) { exit(); // 安全なタイミングで終了 } }
  12. pcntl_signal 関数の動作 • $handler として callable型を渡した際、 pcntl_signal は、OS に対してシグナル受信時に $handler

    が呼ばれるように設定しているわけではな い • 実際に OS に登録されるシグナルハンドラは pcntl_signal_handler というC関数 • $handler は別途 php 内部のシグナルテーブルに記 録される
  13. 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]
  14. シグナルディスパッチ • pcntl拡張は、安全なタイミングで php のシグナルハンドラ ($handler)が実行されるように調整 • pcntl_signal_dispatch (C関数)で実装されている •

    pcntl_signal_dispatch は、何もしなければ呼ばれない => 実行のタイミングを PHPスクリプト側であらかじめ設 定しておく必要がある
  15. シグナルディスパッチ pcntl_signal_dispatch $handler(SIGINT, [$siginfo]) SIGHUP NULL SIGINT $handler SIGALRM NULL

    …… php signal table [PHP Script] [PHP Internal] signal queue signo 取り出したシグナ ル番号で参照、該 当する関数を実行
  16. 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
  17. tick • 1ステートメント実行する毎(※)に発生するイベント (php の機能) • `declare(ticks=N)`を宣言することで有効になり、 N回 tick するたびに、あらかじめ登録しておいた処

    理が実行される • php スクリプトからは register_tick_function を使って実行される関数やメソッドを登録できる ※厳密には、tick されないステートメントもあるが、大体はセミコロン毎と考えてよい
  18. tick を利用した シグナルディスパッチ • pcntl 拡張は初期化時、tickを利用するかどう かに関わらず pcntl_signal_dispatch (C関数) が実行されるよう登録している

    (4.3-) • tick が有効な範囲において Nステートメントご とにディスパッチされる • 現存するシグナルディスパッチの仕組みで最古
  19. 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(); 大体同じ
  20. tick の有効範囲 • declare は、ファイルの先頭に記述するか、あるい はブロックで指定 • ファイルを越えて有効になる ことはない (tick

    の有効性は コンパイル時に確定するため) • 関数を呼び出しても、呼び出し た先が tick 有効範囲外なら ば、ディスパッチされない declare(ticks=1) { // tick 有効 func(); } function func() { // tick 無効 }
  21. シグナルがディスパッチされない ことによる問題 • pcntl拡張 が保持できるシグナルは 32個だけ (signal queue の数が固定) •

    長時間ディスパッチされないと、超過した分のシグナルは捨 てられる • signal queue にシグナルがたまった状態で fork すると、 子プロセスで受け取っていないはずのシグナルを処理するこ とも
  22. pcntl_async_signals(true) • これを実行しておくと、`pcntl_async_signals(false)` しない限り、ほぼ常にディスパッチされるイメージ • (実際には、シグナルを受信したら PHP VM の各命令実行毎 にディスパッチされる)

    • 7.1 から利用可能 • tick よりも細かい粒度でディスパッチされるが、タイムア ウトを実装するための仕組みを流用しているため、低負荷。
  23. pcntl_async_signals(true)時の裏の動き • シグナルを受信すると、EG(vm_interrupt) フラグ を立てる • PHP VM が1命令実行するたびに、 EG(vm_interrupt)

    フラグをチェックし、立ってい れば、zend_interrupt_function を実行 • pcntl 拡張は、初期化時に zend_interrupt_function をフックし、そこで pcntl_signal_dispatch を呼んでいる
  24. pcntl_async_signals(true)時の シグナル受信 pcntl_signal_handler 内部のキューにシグナル番号を追加し、 vm_interrupt フラグを立てる [PHP Script] [PHP Internal]

    signo signal queue (シグナル受信時 PHP Script 側への影響は一切ない) vm_interrupt このフラグを PHP VM が都度確認している
  25. ベンチマーク php ソースコード付属の Zend/bench.php 1. そのまま実行 (normal) 2. `declare(ticks=1)` を先頭に付与して実行(tick)

    3. `pcntl_async_signals(true)` を先頭に付与して実行 (async) Total (合計秒) をスコアとし、10回計測した平均を比較した (VPS 上での実行なのであくまで参考程度に)
  26. 結果 • 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)
  27. 第2章まとめ • PHP では `pcntl_signal` でシグナルを制御できる • PHPのシグナルハンドラは、実行タイミングを調整す ることで安全に動作するように制御されている •

    歴史的な事情により、シグナルディスパッチのタイミ ングを制御する方法は複数ある • 7.1以降は `pcntl_async_signals(true)` のみで OK
  28. php のタイムアウト • max_execution_time や set_time_limit で設定できる • php スクリプト自身が使った処理時間が指定時間を経過する

    と `PHP Fatal error: Maximum execution time of X second exceeded` (Windows の場合は、処理時間ではなく経過した時間) • 他のプログラミング言語ではあまりみない
  29. php のタイムアウト2 • Linux 系ではインターバルタイマーのプロファイルという仕 組みを利用 • プログラムが一定時間CPUを使うと SIGPROF を受信

    • `set_time_limit(0)` してても、SIGPROF を受け取ると timeout • SIGPROF を 無視したり、シグナルハンドラを設定すると timeout しなくなる
  30. pcntl_alarm • 指定秒数後に SIGALRM が自身に送られてくるよ うに設定する • `pcntl_alarm(10)` • 同期処理のタイムアウトに利用できて便利

    • SIGALRM のデフォルトの動作はプログラム終了な ので、 SIGALRM を適切にハンドリングしておか ないとただの時限爆弾
  31. SIGBABY • pcntl拡張は OS (というか posix) に定義されてい ないシグナルが存在する • 番号としては

    SIGSYS と同じ • コミットログとは直接関係ない修正なので、誤って混 入したか、あるいはDerick氏によるjokeではないか とのこと – https://github.com/php/php- src/commit/ea83d64507b6470eb654fbd75e614319abb4 03ed#diff-7185d47849fdb94217f22977d69e85b1R158 – https://stackoverflow.com/a/18584728
  32. シグナルのマスク • 特定のシグナルを一時的に保留することができる (シグナルをマスクする) • `pcntl_sigprocmask` で設定 • pctrl 拡張ではなく

    OS がシグナルを保留する • 保留を解除すると保留中に受け取っていたシグナ ルが送信されてくる
  33. ZEND_SIGNAL • 7.1 以降は ZEND_SIGNAL がデフォルトで有効 • pcntl拡張 <-> ZEND_SIGNAL

    <-> OS • OS に設定するシグナルハンドラをさらにフック し、タイミングを制御 • php スクリプト側への影響はほとんどない – SIGKILL, SIGSTOP に対して pcntl_signal – 7.0まで:Warning、 7.1以降:Fatal Error
  34. 4.3以前のシグナルディスパッチ • ZEND_EXT_STMT のタイミングでディスパッチ • tick が tickを有効にしたときのみ ZEND_TICKS を挟み込むのに対し、こちらは(コ

    ンパイル時のオプションを設定することで、) コード全体に作用 • pcntl拡張を組み込むだけでコード全体が速度低 下していた
  35. $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(プロセス が停止)など
  36. $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
  37. $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)