PHP誤導性錯誤:Maximum execution time of 0 seconds exceeded

前言:前幾天做項目的時候遇到了PHP上傳文件超時的錯誤,並且我都把PHP配置中的超時時間設爲0(無限制)還是錯,後來Google到了這篇文章,說還有一種原因可能是我們服務器的設置,我用的是Apache,還是在Windows下的,這篇文章着重講的是Linux/Unix下,雖然問題沒解決,不過感覺文章還不錯,講到了網上解決超時沒有講到的地方,所以翻譯過來給大家分享一下~
這篇文章告訴我們:出現PHP超時,有可能問題並不出在PHP本身

昨天,在phpcfreenode上,有人貼出了一個奇怪的錯誤:

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章