PHPスクリプト実行時のタイムアウトの仕組みを詳しく解説, PHPスクリプトの仕組みを詳しく解説
PHP開発を行う際、スクリプトのタイムアウトを制御するためにmax_input_timeとmax_execution_timeを設定することが多いです。しかし、その背後にある原理については考えたこともありませんでした。
この 2 日間の自由時間を利用して、この問題を学習してください。
タイムアウト設定
PHP の ini 設定がどのように機能するかはよくあるトピックです。
まず、php.iniで設定します。 php が起動すると (php_module_startup 段階)、ini ファイルを読み取って解析しようとします。簡単に言うと、解析プロセスは、ini ファイルを分析し、正当なキーと値のペアを抽出して、configuration_hash テーブルに保存することです。
OK、その後、php はさらに zend_startup_extensions を呼び出して各モジュール (php コア モジュールとロードする必要があるすべての拡張機能を含む) を開始します。各モジュールのスタートアップ関数では、REGISTER_INI_ENTRIES アクションが完了します。 REGISTER_INI_ENTRIES は、configuration_hash テーブルからモジュールに対応するいくつかの構成を取り出し、処理関数を呼び出し、最後に処理された値をモジュールのグローバル変数に格納する役割を果たします。
max_input_time、max_execution_time これら 2 つの設定は php Core モジュールに属します。 php Core の場合、REGISTER_INI_ENTRIES は引き続き php_module_startup で発生します。 php Core モジュールにも属する設定には、expose_php、display_errors、memory_limit などが含まれます...
概略図は次のとおりです。
リーリー上で述べたように、REGISTER_INI_ENTRIES は構成ごとに異なる関数を呼び出します。 max_execution_time に対応する関数を直接見てみましょう:
リーリーPHP の起動フェーズに注目するだけなので、今のところ前半だけを見てください。この関数の動作は非常に単純で、max_execution_time は EG (timeout_秒) に格納されます。
max_input_time に関しては、特別な処理関数はありません。デフォルトでは、max_input_time は PG (max_input_time) に格納されます。
REGISTER_INI_ENTRIES が完了すると、次のことが起こります:
max_execution_time ----> EG(timeout_秒)に保存します
max_input_time ----> PG(max_input_time)に保存します
リクエストタイムアウト制御
php_request_startup 関数には次のコードがあります:
リーリー
php_request_startupのタイミングは非常に特殊です。CGI を例にとると、php_request_startup は、php が元のリクエストと CGI からいくつかの CGI 環境変数を取得した後にのみ呼び出されます。実際に上記のコードを実行すると、リクエストは取得できているのでSG(request_info)は準備完了状態ですが、PHPの$_GET、$_POST、$_FILEなどのスーパーグローバル変数はまだ生成されていません。
コードから理解する:
1. ユーザーが max_input_time を -1 に設定するか、設定しない場合、スクリプトのライフサイクルは EG (timeout_秒) によってのみ制限されます。
2. それ以外の場合、リクエスト起動フェーズのタイムアウト制御はPG(max_input_time)の対象となります。
3. zend_set_timeout 関数はタイマーを設定します。指定した時間が経過すると、タイマーが PHP プロセスに通知します。 zend_set_timeout については、以下で詳しく分析します。
php_request_startupが完了すると、phpの実際の実行フェーズ、つまりphp_execute_scriptに入ります。 php_execute_script で確認できます:
リーリー
OK、ここでコードが実行され、max_input_time タイムアウトが発生していない場合、max_execution_time のタイムアウトが再指定されます。zend_set_timeout を呼び出して max_execution_time を渡すことによっても同じことが行われます。 Windows では元のタイマーをオフにするには、zend_unset_timeout を明示的に呼び出す必要があるが、Linux ではそうではないことに特に注意してください。これは、2 つのプラットフォームでのタイマーの実装原理が異なるためです。これについては、以下で詳しく説明します。
最後に、図はタイムアウト制御プロセスを表すために使用されます。左側のケースは、ユーザーが max_input_time と max_execution_time の両方を設定したことを示しています。右側の違いは、ユーザーが max_execution_time のみを設定したことです:
zend_set_timeout
リーリー
上記の実装は基本的に 2 つのプラットフォームに完全に分割できます:
まず Linux について見てみましょう:
setitimer を呼び出すときは、it_interval を 0 に設定して、このタイマーが 2 回おきではなく 1 回だけトリガーされることを示すことに注意してください。 setitimer は 3 つの方法で時間を測定できます。PHP では、ユーザー コードとカーネル コードの実行時間を同時に計算する ITIMER_PROF を使用します。時間が経過すると、SIGPROF 信号が生成されます。
PHP プロセスが SIGPROF シグナルを受信すると、現在実行中の内容に関係なく、zend_timeout にジャンプします。 zend_timeout は実際にタイムアウトを処理する関数です。
もう一度 Windows を見てください:
首先会启动一个子线程,该线程主要用于设置定时器,同时维护EG(timed_out)变量。
子线程一旦生成,主线程便会向子线程发送一条消息:WM_REGISTER_ZEND_TIMEOUT。子线程接收到WM_REGISTER_ZEND_TIMEOUT之后,产生一个定时器并开始计时。同时,子线程会设置EG(timed_out) = 0。这很重要!windows平台下正是通过判断EG(timed_out)是否为1,来决定是否超时。
如果定时器到时间了,子线程收到WM_TIMER消息,则取消定时器,并且设置EG(timed_out) = 1。
如果需要关闭定时器,则子线程会收到WM_UNREGISTER_ZEND_TIMEOUT消息。关闭定时器,并不会改变EG(timed_out)。
相关代码还是很清晰的:
static LRESULT CALLBACK zend_timeout_WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_DESTROY: PostQuitMessage(0); break; // 生成一个定时器,开始计时 case WM_REGISTER_ZEND_TIMEOUT: /* wParam is the thread id pointer, lParam is the timeout amount in seconds */ if (lParam == 0) { KillTimer(timeout_window, wParam); } else { SetTimer(timeout_window, wParam, lParam*1000, NULL); EG(timed_out) = 0; } break; // 关闭定时器 case WM_UNREGISTER_ZEND_TIMEOUT: /* wParam is the thread id pointer */ KillTimer(timeout_window, wParam); break; // 超时了,也需关闭定时器 case WM_TIMER: { KillTimer(timeout_window, wParam); EG(timed_out) = 1; } break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; }
根据上文描述,最终都是需要跳转到zend_timeout来处理超时的。那windows下如何进入zend_timeout呢?
window下仅在execute函数中(zend_vm_execute.h刚开始的地方),可以看到调用zend_timeout:
while (1) { int ret; #ifdef ZEND_WIN32 if (EG(timed_out)) { // windows下的超时,执行每条opcode之前都判断是否需要调用zend_timeout zend_timeout(0); } #endif if ((ret = OPLINE->handler(execute_data TSRMLS_CC)) > 0) { ... } }
上述代码可以看到:
在windows下,每执行完成一条opcode指令,就会进行一次超时判断。
因为主线程执行opcode的同时,子线程可能已经发生超时,而windows并没有什么机制可以让主线程停止手头的工作,直接跳入zend_timeout。所以只好利用子线程先将EG(timed_out)设置为1,然后主线程在等到当前opcode执行完成、进入下一条opcode之前,判断一下EG(timed_out)再调用zend_timeout。
因此准确的讲,windows的超时,其实是有一点点延时的。至少在某一个opcode执行的过程中,无法被打断。当然,正常情况下,单条opcode的执行时间会很短。但是可以很容易人为构造出一些很耗时的函数,使得function call需要等待较长时间。此时,如果子线程判断出超时了,则还需要经过漫长的等待,直到主线程完成该条opcode之后,才能调用zend_timeout。
zend_unset_timeout
void zend_unset_timeout(TSRMLS_D) /* {{{ */ { #ifdef ZEND_WIN32 // 通过发送WM_UNREGISTER_ZEND_TIMEOUT消息来关闭定时器 if(timeout_thread_initialized) { PostThreadMessage(timeout_thread_id, WM_UNREGISTER_ZEND_TIMEOUT, (WPARAM) GetCurrentThreadId(), (LPARAM) 0); } #else if (EG(timeout_seconds)) { struct itimerval no_timeout; no_timeout.it_value.tv_sec = no_timeout.it_value.tv_usec = no_timeout.it_interval.tv_sec = no_timeout.it_interval.tv_usec = 0; // 全置0,相当于关闭定时器 setitimer(ITIMER_PROF, &no_timeout, NULL); } #endif }
zend_unset_timeout同样分成两种平台的实现。
先看linux:
linux下的关闭定时器也很简单。只要将struct itimerval中的4个值都设置为0,就行了。
再看windows:
由于windows是利用一个独立的线程来计时。因此,zend_unset_timeout会向该线程发送WM_UNREGISTER_ZEND_TIMEOUT消息。WM_UNREGISTER_ZEND_TIMEOUT对应的动作是去调用KillTimer来关闭定时器。注意,线程本身并不退出。
前文留下了一个问题,在php_execute_script中,windows下面要显示调用zend_unset_timeout来关闭定时器,而linux下不需要。因为对于一个linux进程来说,只能存在一个setitimer定时器。也就是说,重复调用setitimer,后面的定时器会直接覆盖前面的。
zend_timeout
ZEND_API void zend_timeout(int dummy) /* {{{ */ { TSRMLS_FETCH(); if (zend_on_timeout) { zend_on_timeout(EG(timeout_seconds) TSRMLS_CC); } zend_error(E_ERROR, "Maximum execution time of %d second%s exceeded", EG(timeout_seconds), EG(timeout_seconds) == 1 ? "" : "s"); }
如前文所述,zend_timeout是实际处理超时的函数。它的实现也很简单。
如果有配置exit_on_timeout,则zend_on_timeout会尝试调用sapi_terminate_process关闭sapi进程。如果无需exit_on_timeout,则直接进入zend_error进行出错处理。大部分情况下,我们并不会设置exit_on_timeout,毕竟我们期望的是虽然一个请求超时了,但是进程仍然保留下来,服务下一个请求。
zend_error除了会打印错误日志,还会利用longjump跳转到boilout指定的栈帧,一般是zend_end_try或者zend_catch宏所在的地方。关于longjump,可以另起一个话题,本文就不具体叙述了。在php_execute_script里面,zend_error会使得程序跳转到zend_end_try的位置然后继续执行。继续执行是指,会调用php_request_shutdown等函数来完成收尾工作。
直到这里,php脚本的超时机制算是讲清楚了。
最后来看一个疑似php内核的bug。
windows下max_input_time的bug
回忆一下,之前有提到windows下只有一个地方调用了zend_timeout,就是execute函数里,准确讲是每条opcode执行之前。
那么,假如发生max_input_time类型的超时,即使子线程将EG(timed_out)被置为1,也得延迟到execute中才能进行超时处理。貌似一切正常。
而问题的关键之处便在于,我们并不能保证主线程执行到execute时,EG(timed_out)任然为1。一旦进入execute之前,EG(timed_out)被子线程修改成0,那么max_input_time类型的超时就永远不会被handle了。
为何EG(timed_out)会被子线程又修改为0呢?原因在于:php_execute_script中,调用了zend_set_timeout(INI_INT("max_execution_time"), 0)来设置定时器。
zend_set_timeout は、WM_REGISTER_ZEND_TIMEOUT メッセージを子スレッドに送信します。子スレッドがこのメッセージを受信すると、タイマーの作成に加えて、EG(timed_out) = 0 も設定されます (詳細については、上でインターセプトした zend_timeout_WndProc コード スニペットを参照してください)。スレッドの実行は不確実であるため、メインスレッドの実行時に子スレッドがメッセージを受信したかどうかを判断して EG (timed_out) を 0 に設定することはできません。
写真に示すように、
赤線の時点でexecuteの判定が発生した場合、EG(timed_out)が1となり、executeはzend_timeoutを呼び出してタイムアウト処理を行います。
青線の時点で実行判定が発生した場合、EG(timed_out)は0にリセットされており、max_input_timeタイムアウトは完全にカバーされています。
興味があるかもしれない記事:
- PHPにおけるいくつかの一般的なタイムアウト処理の包括的なまとめ
- PHP file_get_contentsのタイムアウト処理方法の設定
- 厳密なPHPセッションのセッションタイムアウト設定方法
- phpcurlのタイムアウト設定例
- 設定方法PHPページ関数のタイムアウト制限
- PHPページ関数のタイムアウト時間を設定する方法

php把负数转为正整数的方法:1、使用abs()函数将负数转为正数,使用intval()函数对正数取整,转为正整数,语法“intval(abs($number))”;2、利用“~”位运算符将负数取反加一,语法“~$number + 1”。

实现方法:1、使用“sleep(延迟秒数)”语句,可延迟执行函数若干秒;2、使用“time_nanosleep(延迟秒数,延迟纳秒数)”语句,可延迟执行函数若干秒和纳秒;3、使用“time_sleep_until(time()+7)”语句。

php除以100保留两位小数的方法:1、利用“/”运算符进行除法运算,语法“数值 / 100”;2、使用“number_format(除法结果, 2)”或“sprintf("%.2f",除法结果)”语句进行四舍五入的处理值,并保留两位小数。

判断方法:1、使用“strtotime("年-月-日")”语句将给定的年月日转换为时间戳格式;2、用“date("z",时间戳)+1”语句计算指定时间戳是一年的第几天。date()返回的天数是从0开始计算的,因此真实天数需要在此基础上加1。

php判断有没有小数点的方法:1、使用“strpos(数字字符串,'.')”语法,如果返回小数点在字符串中第一次出现的位置,则有小数点;2、使用“strrpos(数字字符串,'.')”语句,如果返回小数点在字符串中最后一次出现的位置,则有。

方法:1、用“str_replace(" ","其他字符",$str)”语句,可将nbsp符替换为其他字符;2、用“preg_replace("/(\s|\ \;||\xc2\xa0)/","其他字符",$str)”语句。

php字符串有下标。在PHP中,下标不仅可以应用于数组和对象,还可应用于字符串,利用字符串的下标和中括号“[]”可以访问指定索引位置的字符,并对该字符进行读写,语法“字符串名[下标值]”;字符串的下标值(索引值)只能是整数类型,起始值为0。

在PHP中,可以利用implode()函数的第一个参数来设置没有分隔符,该函数的第一个参数用于规定数组元素之间放置的内容,默认是空字符串,也可将第一个参数设置为空,语法为“implode(数组)”或者“implode("",数组)”。


ホットAIツール

Undresser.AI Undress
リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover
写真から衣服を削除するオンライン AI ツール。

Undress AI Tool
脱衣画像を無料で

Clothoff.io
AI衣類リムーバー

AI Hentai Generator
AIヘンタイを無料で生成します。

人気の記事

ホットツール

ドリームウィーバー CS6
ビジュアル Web 開発ツール

SecLists
SecLists は、セキュリティ テスターの究極の相棒です。これは、セキュリティ評価中に頻繁に使用されるさまざまな種類のリストを 1 か所にまとめたものです。 SecLists は、セキュリティ テスターが必要とする可能性のあるすべてのリストを便利に提供することで、セキュリティ テストをより効率的かつ生産的にするのに役立ちます。リストの種類には、ユーザー名、パスワード、URL、ファジング ペイロード、機密データ パターン、Web シェルなどが含まれます。テスターはこのリポジトリを新しいテスト マシンにプルするだけで、必要なあらゆる種類のリストにアクセスできるようになります。

Safe Exam Browser
Safe Exam Browser は、オンライン試験を安全に受験するための安全なブラウザ環境です。このソフトウェアは、あらゆるコンピュータを安全なワークステーションに変えます。あらゆるユーティリティへのアクセスを制御し、学生が無許可のリソースを使用するのを防ぎます。

EditPlus 中国語クラック版
サイズが小さく、構文の強調表示、コード プロンプト機能はサポートされていません

mPDF
mPDF は、UTF-8 でエンコードされた HTML から PDF ファイルを生成できる PHP ライブラリです。オリジナルの作者である Ian Back は、Web サイトから「オンザフライ」で PDF ファイルを出力し、さまざまな言語を処理するために mPDF を作成しました。 HTML2FPDF などのオリジナルのスクリプトよりも遅く、Unicode フォントを使用すると生成されるファイルが大きくなりますが、CSS スタイルなどをサポートし、多くの機能強化が施されています。 RTL (アラビア語とヘブライ語) や CJK (中国語、日本語、韓国語) を含むほぼすべての言語をサポートします。ネストされたブロックレベル要素 (P、DIV など) をサポートします。
