亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 開發 > PHP > 正文

php腳本運行時的超時機制詳解

2024-05-04 23:43:06
字體:
來源:轉載
供稿:網友
在我們平常的開發中,也許曾經都遇到過PHP腳本運行超時的情況,當遇到這種情況我們經常會通過使用 set_time_limit(非安全模式),或修改配置文件并重啟服務器,或者修改程序減少程序的執行時間,使其在允許的范圍之內,以解決此問題。
 

在做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對應的函數:

static PHP_INI_MH(OnUpdateTimeout){  // php啟動階段走這里  if (stage == PHP_INI_STAGE_STARTUP) {    // 將超時設置保存到EG(timeout_seconds)中    EG(timeout_seconds) = atoi(new_value);    return SUCCESS;  }   // php執行過程中的ini set則走這里  zend_unset_timeout(TSRMLS_C);  EG(timeout_seconds) = atoi(new_value);  zend_set_timeout(EG(timeout_seconds), 0);  return SUCCESS;}

暫時只看上半截,因為我們目前只需關注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函數中有如下代碼:

if (PG(max_input_time) == -1) {  zend_set_timeout(EG(timeout_seconds), 1);} else {  zend_set_timeout(PG(max_input_time), 1);}

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中可以看到:

// 設定執行超時if (PG(max_input_time) != -1) {#ifdef PHP_WIN32  zend_unset_timeout(TSRMLS_C); // 關閉之前的定時器#endif  zend_set_timeout(INI_INT("max_execution_time"), 0);} // 進入執行retval = (zend_execute_scripts(ZEND_REQUIRE TSRMLS_CC, NULL, 3, 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:

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指令,就會進行一次超時判斷。

因為主線程執行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

void zend_unset_timeout(TSRMLS_D) /* {{{ */{#ifdef ZEND_WIN32     // 通過發送WM_UNREGISTER_ZEND_TIMEOUT消息來關閉定時器  if(timeout_thread_initialized) {    PostThreadMessage(timeout_thread_id, WM_UNREGISTER_ZEND_TIMEOUT, (WPARAM) GetCurrentThreadId(), (LPARAM) 0);  }#else  if (EG(timeout_seconds)) {    struct itimerval no_timeout;    no_timeout.it_value.tv_sec = no_timeout.it_value.tv_usec = no_timeout.it_interval.tv_sec = no_timeout.it_interval.tv_usec = 0;         // 全置0,相當于關閉定時器    setitimer(ITIMER_PROF, &no_timeout, NULL);  }#endif}

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 void zend_timeout(int dummy) /* {{{ */{  TSRMLS_FETCH();   if (zend_on_timeout) {    zend_on_timeout(EG(timeout_seconds) TSRMLS_CC);  }   zend_error(E_ERROR, "Maximum execution time of %d second%s exceeded", EG(timeout_seconds), EG(timeout_seconds) == 1 ? "" : "s");}

如前文所述,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。

php腳本運行時的超時機制詳解

如圖所示,

如果execute中的判斷發生在紅線標注的時間點,則EG(timed_out)為1,execute會調用zend_timeout做超時處理。

如果execute中的判斷發生在藍線標注的時間點,則EG(timed_out)已被重置為0,max_input_time超時被徹底掩蓋。



注:相關教程知識閱讀請移步到PHP教程頻道。
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
欧美精品激情blacked18| 国产福利视频一区| 欧美老少配视频| 国产精品日本精品| 国产成人综合精品在线| 国产欧美精品在线| 国产精品91在线观看| 久久精品国产亚洲精品2020| 欧美中文字幕在线观看| 日韩精品日韩在线观看| 日本久久亚洲电影| 91精品国产色综合久久不卡98| 亚洲精品久久久久中文字幕欢迎你| 国产精品v日韩精品| 成人欧美在线视频| 国产69精品久久久久9| 欧美精品电影在线| 日韩av黄色在线观看| 欧美精品生活片| 中文在线资源观看视频网站免费不卡| 国产视频久久网| 欧美最猛性xxxxx亚洲精品| 日韩av成人在线| 国产亚洲精品久久久| 久久精品国产一区二区三区| 91精品视频观看| 狠狠躁天天躁日日躁欧美| 亚洲精品日韩欧美| 久久精品99久久久香蕉| 性欧美办公室18xxxxhd| 国产亚洲精品综合一区91| 亚洲激情久久久| 亚洲一区中文字幕| 欧美老女人xx| 久久久91精品国产一区不卡| 91禁外国网站| 国产日韩精品视频| 久久久亚洲国产天美传媒修理工| 美女999久久久精品视频| 欧美在线一区二区三区四| 日本高清+成人网在线观看| 欧美日本黄视频| 精品欧美国产一区二区三区| 中文字幕日韩在线观看| 国产激情视频一区| 欧美色道久久88综合亚洲精品| 九九精品在线播放| 久久婷婷国产麻豆91天堂| 国产性猛交xxxx免费看久久| 欧美日韩成人在线播放| 欧美午夜无遮挡| 色777狠狠综合秋免鲁丝| 97在线视频免费| 98精品在线视频| 成人亚洲欧美一区二区三区| 欧美在线观看网址综合| 欧美日韩国产成人| 青草青草久热精品视频在线网站| 久久99热精品| 欧美性猛交xxxx免费看| 欧美激情按摩在线| 亚洲精品一区中文字幕乱码| 国产精品 欧美在线| 热久久这里只有精品| 亚洲电影免费观看高清完整版在线| 亚洲精品在线观看www| 91极品女神在线| 中文字幕欧美国内| 欧美与欧洲交xxxx免费观看| 中文字幕亚洲综合| 国产精品美女在线观看| 成人网在线免费看| 一道本无吗dⅴd在线播放一区| 亚洲自拍偷拍网址| 欧美日韩一区二区三区在线免费观看| 国产啪精品视频网站| 国产91精品不卡视频| 亚洲福利在线看| 国产欧美亚洲视频| 久久精品电影网站| 日本一区二区三区四区视频| 日韩av在线直播| 超在线视频97| 日韩欧美国产一区二区| 91影院在线免费观看视频| 91免费看国产| 韩国一区二区电影| 中文字幕精品在线| 亚洲第一av网| 欧洲美女免费图片一区| 亚洲欧洲偷拍精品| 久久视频在线直播| 欧美黑人xxxⅹ高潮交| 亚洲日韩第一页| 在线看日韩欧美| 亚洲视频欧洲视频| 欧美性开放视频| 日韩视频亚洲视频| 久久久爽爽爽美女图片| 亚洲欧美中文日韩v在线观看| 精品国产91乱高清在线观看| 欧美国产精品人人做人人爱| 久久影视电视剧免费网站清宫辞电视| 原创国产精品91| 欧美夫妻性生活视频| 日韩av电影免费观看高清| 91精品中国老女人| 精品视频在线导航| 日韩av在线电影网| 亚洲第一视频网站| 好吊成人免视频| 久久久噜噜噜久久| 欧美日韩另类在线| 在线观看91久久久久久| 亚洲精品国精品久久99热一| 日韩女优在线播放| 亚洲美女动态图120秒| 久久久久久高潮国产精品视| 成人久久久久爱| 国产久一一精品| 亚洲综合第一页| 91精品视频专区| 另类天堂视频在线观看| 成人福利在线观看| 一区二区三区无码高清视频| 成人精品在线视频| 色诱女教师一区二区三区| 欧美成人自拍视频| 日韩电视剧在线观看免费网站| 亚洲精品乱码久久久久久按摩观| 91探花福利精品国产自产在线| 国产在线播放91| 欧美精品在线免费观看| 懂色av影视一区二区三区| 日韩欧美综合在线视频| 欧美午夜片在线免费观看| 18一19gay欧美视频网站| 欧美亚洲日本黄色| 久久天天躁日日躁| 黄色一区二区在线| 亚洲欧洲美洲在线综合| 亚洲国产99精品国自产| 亚洲最大成人免费视频| 欧美大片大片在线播放| 国产亚洲在线播放| 在线精品高清中文字幕| 精品国内产的精品视频在线观看| 精品久久久久久久久国产字幕| 1769国产精品| 欧美午夜激情小视频| 日本亚洲精品在线观看| 亚洲色在线视频| 欧美激情亚洲自拍| 国产有码一区二区| 国产精品久久久久久久久久免费| 色偷偷噜噜噜亚洲男人| 黑丝美女久久久| wwwwwwww亚洲| 亚洲成色777777女色窝| 亚洲欧美日韩在线高清直播| 国产精品久久久久久久久久ktv| 欧美午夜无遮挡| 欧美亚州一区二区三区| 最近中文字幕mv在线一区二区三区四区|