ホームページ  >  記事  >  バックエンド開発  >  PHPスクリプトのタイムアウト機構を解析する、PHPスクリプトを解析する_PHPチュートリアル

PHPスクリプトのタイムアウト機構を解析する、PHPスクリプトを解析する_PHPチュートリアル

WBOY
WBOYオリジナル
2016-07-12 08:59:09839ブラウズ

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 などが含まれます...

概略図は次のとおりです:

---->php_module_startup----------->php_request_startup---->
|-->zend_startup_extensions
|
-> 他のことをする



上で述べたように、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 の起動フェーズで何が起こるかを理解したので、実際にリクエストを処理するときに PHP がどのようにタイムアウトを管理するかを続けて見てみましょう。

php_request_startup 関数には次のコードがあります:

リーリー

php_request_startupのタイミングは非常に特殊です。

CGI を例にとると、php_request_startup は、php が元のリクエストと CGI からいくつかの CGI 環境変数を取得した後にのみ呼び出されます。実際に上記のコードを実行すると、リクエストは取得できているのでSG(request_info)は準備完了状態ですが、PHPの

のようなスーパーグローバル変数はまだ生成されていません。

コードから理解する:

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 で確認できます: $_GET$_POST$_FILE リーリー

OK、ここでコードが実行され、max_input_time タイムアウトが発生していない場合、max_execution_time のタイムアウトが再指定されます。

同样也是采取调用zend_set_timeout,并传入max_execution_time。特别注意一下,windows下面的需要显式调用zend_unset_timeout关闭原来的定时器,而linux下不需要。这是由于两个平台的定时器实现原理不同导致的,下文也会详细展开叙述。

最后用一张图表示超时控制的流程,左侧的case表明用户既配置了max_input_time,又配置了max_execution_time。而右侧的区别在于用户仅仅配置了max_execution_time:

 

zend_set_timeout

前文提到,zend_set_timeout函数用来设置定时器。具体来看下实现:

<span>void</span> zend_set_timeout(<span>long</span> seconds, <span>int</span> reset_signals) <span>/*</span><span> {{{ </span><span>*/</span><span>
{
    TSRMLS_FETCH();

    </span><span>//</span><span> 赋值</span>
    EG(timeout_seconds) =<span> seconds;

#ifdef ZEND_WIN32
    </span><span>if</span>(!<span>seconds) {
        </span><span>return</span><span>;
    }
    
    </span><span>//</span><span> 启动定时器线程</span>
    <span>if</span> (timeout_thread_initialized == <span>0</span> && InterlockedIncrement(&timeout_thread_initialized) == <span>1</span><span>) {
        </span><span>/*</span><span> We start up this process-wide thread here and not in zend_startup(), because if Zend
         * is initialized inside a DllMain(), you're not supposed to start threads from it.
         </span><span>*/</span><span>
        zend_init_timeout_thread();
    }
    
    </span><span>//</span><span> 向线程发送WM_REGISTER_ZEND_TIMEOUT消息</span>
<span>    PostThreadMessage(timeout_thread_id, WM_REGISTER_ZEND_TIMEOUT, (WPARAM) GetCurrentThreadId(),(LPARAM) seconds);
</span><span>#else</span>

    <span>//</span><span> linux平台下</span>
    <span>struct</span> itimerval t_r;        <span>/*</span><span> timeout requested </span><span>*/</span>
    <span>int</span><span> signo;

    </span><span>if</span><span> (seconds) {
        t_r.it_value.tv_sec </span>=<span> seconds;
        t_r.it_value.tv_usec </span>= t_r.it_interval.tv_sec = t_r.it_interval.tv_usec = <span>0</span><span>;

        </span><span>//</span><span> 设置定时器,seconds秒后会发送SIGPROF信号</span>
        setitimer(ITIMER_PROF, &<span>t_r, NULL);
    }
    signo </span>=<span> SIGPROF;

    </span><span>if</span><span> (reset_signals) {
        sigset_t sigset;

        </span><span>//</span><span> 设置SIGPROF信号对应的处理函数为zend_timeout</span>
<span>        signal(signo, zend_timeout);
        
        </span><span>//</span><span> 防屏蔽</span>
        sigemptyset(&<span>sigset);
        sigaddset(</span>&<span>sigset, signo);
        sigprocmask(SIG_UNBLOCK, </span>&<span>sigset, NULL);
    }
</span><span>#endif</span><span>
}</span>

上述实现基本上可以完全分成两种平台:

  • 先看linux:

linux下的定时器要容易许多,调用setitimer函数就行,此外,zend_set_timeout还设定了SIGPROF信号的handler为zend_timeout。

注意,调用setitimer的时候,将it_interval设置成0,表明这个定时器只触发一次,而不会每隔一段时间触发一次。setitimer可以以三种方式计时,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)。

相关代码还是很清晰的:

<span>static</span><span> LRESULT CALLBACK zend_timeout_WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    </span><span>switch</span><span> (message) {
        </span><span>case</span><span> WM_DESTROY:
            PostQuitMessage(</span><span>0</span><span>);
            </span><span>break</span><span>;
        
        </span><span>//</span><span> 生成一个定时器,开始计时</span>
        <span>case</span><span> WM_REGISTER_ZEND_TIMEOUT:
            </span><span>/*</span><span> wParam is the thread id pointer, lParam is the timeout amount in seconds </span><span>*/</span>
            <span>if</span> (lParam == <span>0</span><span>) {
                KillTimer(timeout_window, wParam);
            } </span><span>else</span><span> {
                SetTimer(timeout_window, wParam, lParam</span>*<span>1000</span><span>, NULL);
                EG(timed_out) </span>= <span>0</span><span>;
            }
            </span><span>break</span><span>;
        
        </span><span>//</span><span> 关闭定时器</span>
        <span>case</span><span> WM_UNREGISTER_ZEND_TIMEOUT:
            </span><span>/*</span><span> wParam is the thread id pointer </span><span>*/</span><span>
            KillTimer(timeout_window, wParam);
            </span><span>break</span><span>;
        
        </span><span>//</span><span> 超时了,也需关闭定时器</span>
        <span>case</span><span> WM_TIMER: {
                KillTimer(timeout_window, wParam);
                EG(timed_out) </span>= <span>1</span><span>;
            }
            </span><span>break</span><span>;
        </span><span>default</span><span>:
            </span><span>return</span><span> DefWindowProc(hWnd, message, wParam, lParam);
    }
    </span><span>return</span> <span>0</span><span>;
}</span>

根据上文描述,最终都是需要跳转到zend_timeout来处理超时的。那windows下如何进入zend_timeout呢?

window下仅在execute函数中(zend_vm_execute.h刚开始的地方),可以看到调用zend_timeout:

<span>while</span> (<span>1</span><span>) {
    </span><span>int</span><span> ret;
#ifdef ZEND_WIN32
    </span><span>if</span> (EG(timed_out)) {   <span>//</span><span> windows下的超时,执行每条opcode之前都判断是否需要调用zend_timeout</span>
        zend_timeout(<span>0</span><span>);
    }
</span><span>#endif</span>

    <span>if</span> ((ret = OPLINE->handler(execute_data TSRMLS_CC)) > <span>0</span><span>) {
    ...
    }
}</span>

上述代码可以看到:

在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

<span>void</span> zend_unset_timeout(TSRMLS_D) <span>/*</span><span> {{{ </span><span>*/</span><span>
{
#ifdef ZEND_WIN32
    
    </span><span>//</span><span> 通过发送WM_UNREGISTER_ZEND_TIMEOUT消息来关闭定时器</span>
    <span>if</span><span>(timeout_thread_initialized) {
        PostThreadMessage(timeout_thread_id, WM_UNREGISTER_ZEND_TIMEOUT, (WPARAM) GetCurrentThreadId(), (LPARAM) </span><span>0</span><span>);
    }
</span><span>#else</span>
    <span>if</span><span> (EG(timeout_seconds)) {
        </span><span>struct</span><span> itimerval no_timeout;
        no_timeout.it_value.tv_sec </span>= no_timeout.it_value.tv_usec = no_timeout.it_interval.tv_sec = no_timeout.it_interval.tv_usec = <span>0</span><span>;
        
        </span><span>//</span><span> 全置0,相当于关闭定时器</span>
        setitimer(ITIMER_PROF, &<span>no_timeout, NULL);
    }
</span><span>#endif</span><span>
}</span>

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 <span>void</span> zend_timeout(<span>int</span> dummy) <span>/*</span><span> {{{ </span><span>*/</span><span>
{
    TSRMLS_FETCH();

    </span><span>if</span><span> (zend_on_timeout) {
        zend_on_timeout(EG(timeout_seconds) TSRMLS_CC);
    }

    zend_error(E_ERROR, </span><span>"</span><span>Maximum execution time of %d second%s exceeded</span><span>"</span>, EG(timeout_seconds), EG(timeout_seconds) == <span>1</span> ? <span>""</span> : <span>"</span><span>s</span><span>"</span><span>);
}</span>

如前文所述,zend_timeout是实际处理超时的函数。它的实现也很简单。

如果有配置exit_on_timeout,则zend_on_timeout会尝试调用sapi_terminate_process关闭sapi进程。如果无需exit_on_timeout,则直接进入zend_error进行出错处理。大部分情况下,我们并不会设置exit_on_timeout,毕竟我们期望的是虽然一个请求超时了,但是进程仍然保留下来,服务下一个请求。

zend_error はエラー ログを出力するだけでなく、ロングジャンプを使用してボイアウトで指定されたスタック フレームにジャンプします。通常、そこに zend_end_try または zend_catch マクロが配置されます。走り幅跳びについては、別のトピックを開始できますが、この記事では詳しく説明しません。 php_execute_script では、zend_error によりプログラムが zend_end_try の場所にジャンプし、実行を継続します。実行が継続されるということは、php_request_shutdown などの関数が呼び出され、仕上げ作業が完了することを意味します。

ここまで、PHP スクリプトのタイムアウトメカニズムについてわかりやすく説明しました。

最後に、PHP カーネルのバグの疑いを見てみましょう。

Windows での Max_input_time のバグ

前に述べたように、Windows では zend_timeout が呼び出される場所は 1 か所だけで、正確には、各オペコードが実行される前の実行関数内です。

すると、max_input_time型のタイムアウトが発生した場合、サブスレッドがEG(timed_out)を1にしても、実行まで遅延させないとタイムアウト処理ができません。すべて順調のようです。

問題の鍵は、メインスレッドが実行されるときに EG (timed_out) が 1 のままであることを保証できないことです。実行に入る前に子スレッドによって EG(timed_out) が 0 に変更されると、max_input_time タイプのタイムアウトは処理されなくなります。

子スレッドによって 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タイムアウトは完全にカバーされています。

www.bkjia.com本当http://www.bkjia.com/PHPjc/1100148.html技術記事 PHP スクリプトのタイムアウト メカニズムを分析する PHP スクリプトを開発する場合、スクリプトのタイムアウトを制御するために max_input_time と max_execution_time が設定されることがよくあります。でも決して...
声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。