前言:前几天做项目的时候遇到了PHP上传文件超时的错误,并且我都把PHP配置中的超时时间设为0(无限制)还是错,后来Google到了这篇文章,说还有一种原因可能是我们服务器的设置,我用的是Apache,还是在Windows下的,这篇文章着重讲的是Linux/Unix下,虽然问题没解决,不过感觉文章还不错,讲到了网上解决超时没有讲到的地方,所以翻译过来给大家分享一下~
这篇文章告诉我们:出现PHP超时,有可能问题并不出在PHP本身
昨天,在phpc的freenode上,有人贴出了一个奇怪的错误:
PHP Fatal error: Maximum execution time of 0 seconds exceeded
笑话之余,他们已经耗尽了这个月分配给PHP的时间,在12月只有8个小时。这是一个非常奇怪的错误信息:PHP的set_time_limit 函数与在ini配置文件中分配给max_execution_time的值都为0,意味着没有限制。一个没有运行时间限制的脚本本不应该被命中于0秒的执行时间!
然而我们一些人头脑风暴了一下问题可能的原因,此外也提出了一些细节上的问题,包括那个脚本、在unix上运行PHP7,在php-fpm下运行,在程序运行了将近2个小时后2次被中断。
我打算着手查看一下PHP的源码,确信那里将会有一些信息,可能会为解开这个谜题提供一些线索。
让我们调查一下PHP内部
我做的第一件事是在PHP源码中搜索”Maximun execution time”,希望能够准确地找到它,忽略掉测试用例,在zend_execute_API.c文件的zend_timeout() 函数里,那个字眼的确出现了一次。
当 zend_timeout()函数被调用时,PHP会调用一个被SAPI(在PHP与网站服务器之间的接口)指定的函数,如果它被定义的话,然后就会报错并退出。
现在知道了这个错误信息是哪里生成的(并且它只从一个地方生成),我现在需要知道是什么调用了zend_timeout(),所以我在代码中搜索那个函数的名字。除了函数自身的定义,以及在头文件中的声明,共有4个结果,其中两个是只针对Windows系统的,然后它们可以被忽略。
另外两个在zend_set_timeout() 中放置的位置很近,容易被找到,它们两个都使用了回掉函数指向一个signal handler。
这意味着,在通常的使用场景下,脚本执行超时的信息只能够在收到信号之后,作为回应,才产生。
信号
在类Unix操作系统中,信号提供了一种内部进程交流的形式,这是一种发送和接收一个带有用数字作为标识符的中断的形式。在维基百科上的信号词条提供了另外一些细节。
在C语言的信号机制中,定义了六种信号,但是大多数操作系统定义并且使用许多额外的信号。
一些传送给进程的信号导致它无条件终止(例如 SIGKILL)或者使其他操作系统产生系统层面的效果(比如SIGSTOP和SIGCONT)。大多数其他的信号能够被目标进程捕获,如果有安装一个signal handler来接收信号的话。(如果没有安装signal handler,通常情况下,但不绝对,会使用默认的signal handler,结果是进程自身的中断。)
PHP是如何执行时间限制功能的
所以,然后是什么触发了zend_timeout()呢?有一些代码指明了答案:SIGPROF。(另外,当在Windows上运行Cygwin时,使用的是SIGALRM。)
SIGPROF对我来说是新的知识。维基百科是这么解释的:
SIGALRM、SIGVALRN和SIGPROF信号会被发送到进程,当在调用一个带有指定时间的预设置提醒函数(例如setitimer)时间到的时候。在真实时间或者钟表时间到的时候,SIGALRM信号发出。当进程使用CPU时间到的时候,SIGVTALRM信号发出。当CPU时间被进程使用,并且被代表进程的系统使用时间到的时候,SIGPROF发出。
确实,PHP在SIGPROF上加载signal handler之前,调用了setitimer()。在setitimer()的主页上描述了如何使用它来设置一个计算进程耗时的计时器,在时间到之后如何触发一个SIGPROF。
在查找了更多的PHP源码之后,整个进程是如何工作的就变得更加清晰了:当PHP加载的时候,set_time_limit()被调用,或者max_execution_time被改变,PHP清空计时器(如果设置了的话),然后重置计时器(如果超时设置是一个非零的值。)此外,当请求启动的时候,在SIGPROF上的signal handler就被加载(无论timeout是否确实被设置)。
当计时器通过settimer设置时,SIGPROF handler,即zend_timeout()运行,显示带有秒数的错误信息,这个秒数是用在配置文件中的值来填充的,然后就退出。
言外之意
上节回答了超时错误是如何 显示出来的。但是它没有回答为什么:我们可以假设因为计时器没有设置,操作系统就不会发送SIGPROF,然后脚本就不会被终止。
这个答案就在UNIX系统信号机制的深入细节的字里行间中:任何 进程能够给任何 进程发送任何 信号。(除了被安全策略禁止的进程,例如被另外一个用户持有的进程。)
这意味着脚本并不需要实际到达超时时间,并且被杀死。PHP只要告诉它超时了就可以了。
你可以像下面这个例子一样进行测试。
$ php -r 'posix_kill(getmypid(), SIGPROF);'
Fatal error: Maximum execution time of 0 seconds exceeded in Command line code on line 1
所以,实际发生了什么呢?
不幸的是,凭借一个信号并不能知道它是从哪个源头发出的。如果不是这样的话[1],或许能够很容易地知道是什么杀死了进程以及是什么改变了配置来使它停止。在这个案例中,解决方案是修改代码,让PHP”只能够”有20分钟的时间来运行,所以超时就不再是一个问题了。
如果我们假设在服务器上没有恶意的用户发送多余的信号来做拒绝服务的攻击,在max_execution_time的文档中提示了一种可能:
你的网络服务器可以有打断PHP执行的其他超时配置文件。Apache有一个Timeout 指令,IIS有一个CGI超时函数。它们的默认值都为300秒。查看你的网络服务器文档获取详细信息
随着一些探究的深入,我发现既不是Apache,也不是Nginx明确地发送SIGPROF。当Nginx使用settimer时,它使用SIGALRM做为触发器。
我最好的假设是服务器上多少有些在它消耗了太多资源(不是内存就是时间)之后杀死正在运行的PHP程序的看门狗进程。
这大概是一个在PHP中的BUG(或者至少,不期望的疑惑),SIGPROF并没有严格地从计时器超时中抛出,就好像计时器的确超时了的一样,它显示一样的信息,但是这看上去是可校正的。
标签:max_execution_time , php , php-internals , SIGPROF
- [1]注:原文是were that the case 翻译过来是是这样的情况,这样会与下文不通,我的理解是,如果不是这样的话,就能够简单确定是什么导致进程停止。
- 原文链接:http://bafford.com/2016/12/02/php-misleading-error-maximum-execution-time-of-0-seconds-exceeded/
- 如果翻译有误欢迎在评论区指正,我会修正,Thanks·~ ^_^