目录
剖析php脚本的超时机制,剖析php脚本
超时配置
请求超时控制
zend_set_timeout
zend_unset_timeout
zend_timeout
windows下max_input_time的bug
首页 后端开发 php教程 剖析php脚本的超时机制,剖析php脚本_PHP教程

剖析php脚本的超时机制,剖析php脚本_PHP教程

Jul 12, 2016 am 08:59 AM
php

剖析php脚本的超时机制,剖析php脚本

在做php开发的时候,经常会设置max_input_time、max_execution_time,用来控制脚本的超时时间。但却从来没有思考过背后的原理。

趁着这两天有空,研究一下这个问题。

超时配置

php的ini配置如何起作用,这是一个老生常谈的话题了。

首先,我们在php.ini里进行配置。当php启动的时候(php_module_startup阶段),会尝试读取ini文件并解析。解析过程简单来说,是分析ini文件,提取出其中合法的键值对,并保存到configuration_hash表。

OK,然后php会进一步调用zend_startup_extensions来启动各个模块(包含php Core模块,以及所有需要加载的扩展)。各个模块的启动函数中,会完成REGISTER_INI_ENTRIES动作。REGISTER_INI_ENTRIES负责将模块对应的一些配置从configuration_hash表取出,然后调用处理函数,最终将处理完的值存入模块的globals变量。

max_input_time、max_execution_time这两个配置属于php Core模块。对于php Core来说,REGISTER_INI_ENTRIES依然发生在php_module_startup中。同样属于php Core模块的配置还有expose_php、display_errors、memory_limit等等...

示意图如下:

---->php_module_startup----------->php_request_startup---->
        |
        |
        |-->REGISTER_INI_ENTRIES
        |
        |
        |-->zend_startup_extensions
        |          |
        |          |-->zm_startup_date
        |          |         |-->REGISTER_INI_ENTRIES
        |          |
        |          |-->zm_startup_json
        |          |         |-->REGISTER_INI_ENTRIES
        |
        |
        |-->do otherthings

 

上面说到对于不同的配置,REGISTER_INI_ENTRIES会调用不同的函数来处理。我们直接来看max_execution_time对应的函数:

<span>static</span><span> PHP_INI_MH(OnUpdateTimeout)
{
    </span><span>//</span><span> php启动阶段走这里</span>
    <span>if</span> (stage ==<span> PHP_INI_STAGE_STARTUP) {
        </span><span>//</span><span> 将超时设置保存到EG(timeout_seconds)中</span>
        EG(timeout_seconds) =<span> atoi(new_value);
        </span><span>return</span><span> SUCCESS;
    }

    </span><span>//</span><span> php执行过程中的ini set则走这里</span>
<span>    zend_unset_timeout(TSRMLS_C);
    EG(timeout_seconds) </span>=<span> atoi(new_value);
    zend_set_timeout(EG(timeout_seconds), </span><span>0</span><span>);
    </span><span>return</span><span> SUCCESS;
}</span>
登录后复制

暂时只看上半截,因为我们目前只需关注php的启动阶段,该函数行为很简单,将max_execution_time存入了EG(timeout_seconds)。

至于max_input_time,并没有特殊的处理函数,默认是会将max_input_time存入存入PG(max_input_time)。

因此,当REGISTER_INI_ENTRIES完成,发生的是:

max_execution_time ----> 存入EG(timeout_seconds)

max_input_time ----> 存入PG(max_input_time)

请求超时控制

现在我们搞清楚php的启动阶段发生了什么,继续来看php在实际处理请求的时候,如何管理超时。

在php_request_startup函数中有如下代码:

<span>if</span> (PG(max_input_time) == -<span>1</span><span>) {
    zend_set_timeout(EG(timeout_seconds), </span><span>1</span><span>);
} </span><span>else</span><span> {
    zend_set_timeout(PG(max_input_time), </span><span>1</span><span>);
}</span>
登录后复制

php_request_startup的时机很讲究。

以cgi为例,只有当php已经从CGI拿到了原始请求以及一些CGI的环境变量之后,php_request_startup才会被调用。上面这段代码实际执行的时候,由于请求已经拿到,所以SG(request_info)处于准备就绪状态,但是php中的$_GET$_POST$_FILE等超全局变量尚未生成。

从代码上理解:

1、如果用户将max_input_time配做-1,或没有配置,那么脚本的生命周期就只受EG(timeout_seconds)约束。

2、否则,请求启动阶段的超时控制,受PG(max_input_time)约束。

3、zend_set_timeout函数负责设置定时器。一旦指定时间过去,定时器会通知php进程。zend_set_timeout下文会具体分析。

php_request_startup完成,则进入php的实际执行阶段,即php_execute_script。在php_execute_script中可以看到:

<span>//</span><span> 设定执行超时</span>
<span>if</span> (PG(max_input_time) != -<span>1</span><span>) {
#ifdef PHP_WIN32
    zend_unset_timeout(TSRMLS_C); </span><span>//</span><span> 关闭之前的定时器</span>
<span>#endif</span><span>
    zend_set_timeout(INI_INT(</span><span>"</span><span>max_execution_time</span><span>"</span>), <span>0</span><span>);
}

</span><span>//</span><span> 进入执行</span>
retval = (zend_execute_scripts(ZEND_REQUIRE TSRMLS_CC, NULL, <span>3</span>, prepend_file_p, primary_file, append_file_p) == SUCCESS);
登录后复制

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除了会打印错误日志,还会利用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代码片段)。由于线程执行的不确定性,因此不能够判断主线程执行到execute的时候,子线程是否已接收到消息并设置EG(timed_out)为0。

如图所示,

如果execute中的判断发生在红线标注的时间点,则EG(timed_out)为1,execute会调用zend_timeout做超时处理。

如果execute中的判断发生在蓝线标注的时间点,则EG(timed_out)已被重置为0,max_input_time超时被彻底掩盖。

 

www.bkjia.comtruehttp://www.bkjia.com/PHPjc/1100148.htmlTechArticle剖析php脚本的超时机制,剖析php脚本 在做php开发的时候,经常会设置max_input_time、max_execution_time,用来控制脚本的超时时间。但却从来没有...
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解锁Myrise中的所有内容
4 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

适用于 Ubuntu 和 Debian 的 PHP 8.4 安装和升级指南 适用于 Ubuntu 和 Debian 的 PHP 8.4 安装和升级指南 Dec 24, 2024 pm 04:42 PM

PHP 8.4 带来了多项新功能、安全性改进和性能改进,同时弃用和删除了大量功能。 本指南介绍了如何在 Ubuntu、Debian 或其衍生版本上安装 PHP 8.4 或升级到 PHP 8.4

讨论 CakePHP 讨论 CakePHP Sep 10, 2024 pm 05:28 PM

CakePHP 是 PHP 的开源框架。它的目的是使应用程序的开发、部署和维护变得更加容易。 CakePHP 基于类似 MVC 的架构,功能强大且易于掌握。模型、视图和控制器 gu

CakePHP 文件上传 CakePHP 文件上传 Sep 10, 2024 pm 05:27 PM

为了进行文件上传,我们将使用表单助手。这是文件上传的示例。

如何设置 Visual Studio Code (VS Code) 进行 PHP 开发 如何设置 Visual Studio Code (VS Code) 进行 PHP 开发 Dec 20, 2024 am 11:31 AM

Visual Studio Code,也称为 VS Code,是一个免费的源代码编辑器 - 或集成开发环境 (IDE) - 可用于所有主要操作系统。 VS Code 拥有针对多种编程语言的大量扩展,可以轻松编写

CakePHP 快速指南 CakePHP 快速指南 Sep 10, 2024 pm 05:27 PM

CakePHP 是一个开源MVC 框架。它使开发、部署和维护应用程序变得更加容易。 CakePHP 有许多库可以减少大多数常见任务的过载。

您如何在PHP中解析和处理HTML/XML? 您如何在PHP中解析和处理HTML/XML? Feb 07, 2025 am 11:57 AM

本教程演示了如何使用PHP有效地处理XML文档。 XML(可扩展的标记语言)是一种用于人类可读性和机器解析的多功能文本标记语言。它通常用于数据存储

在PHP API中说明JSON Web令牌(JWT)及其用例。 在PHP API中说明JSON Web令牌(JWT)及其用例。 Apr 05, 2025 am 12:04 AM

JWT是一种基于JSON的开放标准,用于在各方之间安全地传输信息,主要用于身份验证和信息交换。1.JWT由Header、Payload和Signature三部分组成。2.JWT的工作原理包括生成JWT、验证JWT和解析Payload三个步骤。3.在PHP中使用JWT进行身份验证时,可以生成和验证JWT,并在高级用法中包含用户角色和权限信息。4.常见错误包括签名验证失败、令牌过期和Payload过大,调试技巧包括使用调试工具和日志记录。5.性能优化和最佳实践包括使用合适的签名算法、合理设置有效期、

php程序在字符串中计数元音 php程序在字符串中计数元音 Feb 07, 2025 pm 12:12 PM

字符串是由字符组成的序列,包括字母、数字和符号。本教程将学习如何使用不同的方法在PHP中计算给定字符串中元音的数量。英语中的元音是a、e、i、o、u,它们可以是大写或小写。 什么是元音? 元音是代表特定语音的字母字符。英语中共有五个元音,包括大写和小写: a, e, i, o, u 示例 1 输入:字符串 = "Tutorialspoint" 输出:6 解释 字符串 "Tutorialspoint" 中的元音是 u、o、i、a、o、i。总共有 6 个元

See all articles