Heim  >  Artikel  >  Backend-Entwicklung  >  Detaillierte Erläuterung des Timeout-Mechanismus von PHP-Skripten

Detaillierte Erläuterung des Timeout-Mechanismus von PHP-Skripten

*文
*文Original
2017-12-29 18:04:101857Durchsuche

In unserer täglichen Entwicklung sind wir möglicherweise auf die Situation gestoßen, in der PHP-Skripte eine Zeitüberschreitung aufweisen. Wenn wir auf diese Situation stoßen, verwenden wir häufig set_time_limit (nicht sicheren Modus) oder ändern die Konfigurationsdatei und starten den Server neu oder ändern das Programm um die Ausführungszeit des Programms auf den zulässigen Bereich zu reduzieren, um dieses Problem zu lösen. Ich hoffe, es hilft allen.

Bei der PHP-Entwicklung werden max_input_time und max_execution_time häufig festgelegt, um das Timeout des Skripts zu steuern. Aber ich habe nie über das Prinzip dahinter nachgedacht.

Nutzen Sie die freie Zeit dieser beiden Tage, um sich mit diesem Thema zu befassen.

Timeout-Konfiguration

Wie die INI-Konfiguration von PHP funktioniert, ist ein häufiges Thema.

Zuerst konfigurieren wir es in php.ini. Wenn PHP startet (phase php_module_startup), versucht es, die INI-Datei zu lesen und zu analysieren. Vereinfacht ausgedrückt besteht der Parsing-Prozess darin, die INI-Datei zu analysieren, die zulässigen Schlüssel-Wert-Paare zu extrahieren und sie in der Tabelle „configuration_hash“ zu speichern.

OK, dann ruft PHP weiterhin zend_startup_extensions auf, um jedes Modul zu starten (einschließlich des PHP-Core-Moduls und aller Erweiterungen, die geladen werden müssen). In der Startfunktion jedes Moduls wird die Aktion REGISTER_INI_ENTRIES abgeschlossen. REGISTER_INI_ENTRIES ist dafür verantwortlich, einige dem Modul entsprechende Konfigurationen aus der Tabelle „configuration_hash“ zu entnehmen, dann die Verarbeitungsfunktion aufzurufen und schließlich die verarbeiteten Werte in der globalen Variablen des Moduls zu speichern.

Die beiden Konfigurationen max_input_time und max_execution_time gehören zum PHP-Core-Modul. Für PHP Core kommt REGISTER_INI_ENTRIES immer noch in php_module_startup vor. Zu den Konfigurationen, die auch zum PHP-Core-Modul gehören, gehören „exposure_php“, „display_errors“, „memory_limit“ usw >Wie oben erwähnt, ruft REGISTER_INI_ENTRIES für verschiedene Konfigurationen unterschiedliche Funktionen zur Verarbeitung auf. Schauen wir uns direkt die Funktion an, die max_execution_time entspricht:


Wir werden uns vorerst nur die obere Hälfte ansehen, da wir uns nur auf die Startphase von konzentrieren müssen PHP. Das Verhalten dieser Funktion ist sehr einfach. Speichern Sie max_execution_time in EG(timeout_seconds).
---->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

Für max_input_time gibt es keine spezielle Verarbeitungsfunktion. Standardmäßig wird max_input_time in PG gespeichert (max_input_time).

Wenn REGISTER_INI_ENTRIES abgeschlossen ist, passiert Folgendes:

max_execution_time ----> EG einzahlen (timeout_seconds)
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;
}

max_input_time ----> PG hinterlegen )

Anforderungs-Timeout-Kontrolle

Da wir nun verstehen, was in der Startphase von PHP passiert, schauen wir uns weiter an, wie PHP die Anforderung tatsächlich verarbeitet. wie man Timeouts verwaltet.

Die Funktion php_request_startup enthält den folgenden Code:


Das Timing von php_request_startup ist sehr speziell.

Am Beispiel von CGI wird php_request_startup erst aufgerufen, nachdem PHP die ursprüngliche Anfrage und einige CGI-Umgebungsvariablen von CGI erhalten hat. Wenn der obige Code tatsächlich ausgeführt wird, befindet sich SG (request_info) seit Erhalt der Anforderung in einem Bereitschaftszustand, aber superglobale Variablen wie $_GET, $_POST, $_FILE usw. in PHP wurden noch nicht generiert.

Verständnis aus dem Code:

1. Wenn der Benutzer max_input_time als -1 konfiguriert oder nicht, ist der Lebenszyklus des Skripts nur durch EG (timeout_seconds) begrenzt.
if (PG(max_input_time) == -1) {
  zend_set_timeout(EG(timeout_seconds), 1);
} else {
  zend_set_timeout(PG(max_input_time), 1);
}

2. Ansonsten unterliegt die Timeout-Kontrolle in der Anforderungsstartphase PG (max_input_time).

3. Die Funktion zend_set_timeout ist für die Einstellung des Timers verantwortlich. Sobald die angegebene Zeit abgelaufen ist, benachrichtigt der Timer den PHP-Prozess. zend_set_timeout wird im Folgenden im Detail analysiert.

Nachdem php_request_startup abgeschlossen ist, tritt es in die eigentliche Ausführungsphase von PHP ein, nämlich php_execute_script. Sie können in php_execute_script sehen:

OK Wenn der Code hier ausgeführt wird und das max_input_time-Timeout nicht aufgetreten ist, wird das Timeout von max_execution_time erneut angegeben.

Ruft außerdem zend_set_timeout auf und übergibt max_execution_time. Achten Sie besonders darauf, dass Sie zend_unset_timeout explizit aufrufen müssen, um den ursprünglichen Timer unter Windows auszuschalten, nicht jedoch unter Linux. Dies liegt an den unterschiedlichen Implementierungsprinzipien von Timern auf den beiden Plattformen, die im Folgenden ausführlich beschrieben werden.

Abschließend wird ein Bild verwendet, um den Prozess der Timeout-Kontrolle zu zeigen. Der Fall links zeigt, dass der Benutzer sowohl max_input_time als auch max_execution_time konfiguriert hat. Der Unterschied auf der rechten Seite besteht darin, dass der Benutzer nur max_execution_time konfiguriert hat:

// 设定执行超时
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);

zend_set_timeout

Wie bereits erwähnt, die Die Funktion zend_set_timeout dient zum Einstellen des Timers. Schauen wir uns die Implementierung konkret an:


Die obige Implementierung kann grundsätzlich vollständig in zwei Plattformen unterteilt werden:

Schauen Sie sich zunächst Linux an :


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
}
Der Timer unter Linux ist viel einfacher, rufen Sie einfach die Funktion setitimer auf. Außerdem setzt zend_set_timeout auch den Handler des SIGPROF-Signals auf 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中才能进行超时处理。貌似一切正常。

Der Schlüssel zum Problem liegt darin, dass wir nicht garantieren können, dass EG (timed_out) immer noch 1 ist, wenn der Hauptthread die Ausführung ausführt. Sobald EG(timed_out) vom untergeordneten Thread vor dem Aufrufen der Ausführung auf 0 geändert wird, wird das Timeout des Typs max_input_time nie behandelt.

Warum wird EG(timed_out) vom untergeordneten Thread auf 0 geändert? Der Grund ist: In php_execute_script wird zend_set_timeout(INI_INT("max_execution_time"), 0) aufgerufen, um den Timer einzustellen.

zend_set_timeout sendet die WM_REGISTER_ZEND_TIMEOUT-Nachricht an den untergeordneten Thread. Wenn der untergeordnete Thread diese Nachricht empfängt, erstellt er nicht nur einen Timer, sondern setzt auch EG(timed_out) = 0 (Einzelheiten finden Sie im oben abgefangenen Codeausschnitt zend_timeout_WndProc). Aufgrund der Unsicherheit der Thread-Ausführung ist es unmöglich zu bestimmen, ob der untergeordnete Thread die Nachricht empfangen hat, und EG (timed_out) auf 0 zu setzen, wenn der Haupt-Thread die Ausführung ausführt.

Wie in der Abbildung gezeigt,

Wenn die Ausführungsentscheidung zu dem durch die rote Linie markierten Zeitpunkt erfolgt, ist EG (timed_out) 1 und Ausführen ruft zend_timeout auf und führt die Timeout-Verarbeitung durch.

Wenn die Ausführungsentscheidung zu dem durch die blaue Linie markierten Zeitpunkt erfolgt, wurde EG (timed_out) auf 0 zurückgesetzt und das max_input_time-Timeout wird vollständig abgedeckt.

Verwandte Empfehlungen:

Eine kurze Analyse des zugrunde liegenden Betriebsmechanismus und der Arbeitsprinzipien von PHP

Detaillierte Erläuterung der PHP-Shared-Memory-Nutzung

PHP generiert textbasierten Morsecode

Das obige ist der detaillierte Inhalt vonDetaillierte Erläuterung des Timeout-Mechanismus von PHP-Skripten. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn