剖析PHP脚本的超时机制(2)

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

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

剖析PHP脚本的超时机制

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指令,就会进行一次超时判断。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/d96cc7904de123940760e51fb4c3a79a.html