同样也是采取调用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函数用来设置定时器。具体来看下实现:
void zend_set_timeout(long seconds, int reset_signals) /* {{{ */
{
TSRMLS_FETCH();
// 赋值
EG(timeout_seconds) = seconds;
#ifdef ZEND_WIN32
if(!seconds) {
return;
}
// 启动定时器线程
if (timeout_thread_initialized == 0 && InterlockedIncrement(&timeout_thread_initialized) == 1) {
/* 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.
*/
zend_init_timeout_thread();
}
// 向线程发送WM_REGISTER_ZEND_TIMEOUT消息
PostThreadMessage(timeout_thread_id, WM_REGISTER_ZEND_TIMEOUT, (WPARAM) GetCurrentThreadId(),
(LPARAM) seconds);
#else
// linux平台下
struct itimerval t_r; /* timeout requested */
int signo;
if (seconds) {
t_r.it_value.tv_sec = seconds;
t_r.it_value.tv_usec = t_r.it_interval.tv_sec = t_r.it_interval.tv_usec = 0;
// 设置定时器,seconds秒后会发送SIGPROF信号
setitimer(ITIMER_PROF, &t_r, NULL);
}
signo = SIGPROF;
if (reset_signals) {
sigset_t sigset;
// 设置SIGPROF信号对应的处理函数为zend_timeout
signal(signo, zend_timeout);
// 防屏蔽
sigemptyset(&sigset);
sigaddset(&sigset, signo);
sigprocmask(SIG_UNBLOCK, &sigset, NULL);
}
#endif
}
上述实现基本上可以完全分成两种平台:
•先看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)。
相关代码还是很清晰的:
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指令,就会进行一次超时判断。