php脚本运行时的超时机制详解(2)

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

因为主线程执行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

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

转载注明出处:http://www.heiqu.com/c6311b1864ef54917e1656247f7a23db.html