In the development of PHP, often set max_input_time, Max_execution_time, to control the script time-out. But I never thought about the principle behind it.
Take advantage of the two days to study the problem.
Timeout configuration
How the INI configuration works in PHP is a commonplace topic.
First, we configure it in the php.ini. When PHP starts (the php_module_startup phase), it attempts to read the INI file and parse it. The parsing process is simply to parse the INI file, extract the valid key-value pairs, and save them to the Configuration_hash table.
OK, then PHP will call Zend_startup_extensions further to launch the modules (including the PHP core module, and all the extensions that need to be loaded). The Register_ini_entries action is completed in the start function of each module. Register_ini_entries is responsible for removing some of the module's configuration from the Configuration_hash table and then invoking the handler function, which eventually stores the processed value in the module's Globals variable.
Max_input_time, max_execution_time These two configurations belong to the PHP core module. For PHP core, register_ini_entries still occurs in Php_module_startup. Also belonging to the PHP core module configuration is expose_php, Display_errors, Memory_limit and so on ...
As follows:
---->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
It says that for different configurations, register_ini_entries will call different functions to handle. Let's take a look at the corresponding function of Max_execution_time:
Static PHP_INI_MH (onupdatetimeout) { //PHP start phase go here if (stage = = Php_ini_stage_startup) { //Save timeout setting to eg ( Timeout_seconds) Medium EG (timeout_seconds) = Atoi (new_value); return SUCCESS; } The INI set in PHP execution goes here zend_unset_timeout (tsrmls_c); EG (timeout_seconds) = Atoi (new_value); Zend_set_timeout (EG (Timeout_seconds), 0); return SUCCESS;}
For the time being, we only need to focus on the start-up phase of PHP, the function is simple, and max_execution_time is deposited in eg (timeout_seconds).
As for Max_input_time, there is no special processing function, and the default is to deposit Max_input_time into PG (MAX_INPUT_TIME).
So, when Register_ini_entries is done, what happens is:
Max_execution_time----> Deposit eg (timeout_seconds)
Max_input_time----> Deposit PG (MAX_INPUT_TIME)
Request Timeout Control
Now that we figure out what's going on in PHP's start-up phase, we'll continue to see how PHP manages timeouts when actually processing requests.
The following code is in the Php_request_startup function:
if (PG (max_input_time) = =-1) { zend_set_timeout (EG (timeout_seconds), 1);} else { Zend_set_timeout (PG (MAX_ Input_time), 1);}
The timing of php_request_startup is very particular.
In the case of CGI, Php_request_startup is only called when PHP has taken the original request from CGI and some CGI environment variables. The above code actually executes, because the request has been received, so the SG (REQUEST_INFO) is in a ready state, but the $_get,$_post,$_file in PHP and other super-global variables have not been generated.
Understand from the code:
1, if the user will be max_input_time with 1, or not configured, then the life cycle of the script is only subject to eg (timeout_seconds) constraints.
2, otherwise, request the start-up phase of the timeout control, by the PG (MAX_INPUT_TIME) constraints.
3, the Zend_set_timeout function is responsible for setting the timer. Once the specified time has elapsed, the timer notifies the PHP process. Zend_set_timeout will be analyzed in detail below.
Php_request_startup completes, then enters the actual execution stage of PHP, namely Php_execute_script. In the Php_execute_script, you can see:
Set execution Timeout if (PG (max_input_time)! =-1) {#ifdef php_win32 zend_unset_timeout (Tsrmls_c);//Turn off the timer before #endif Zend_ Set_timeout (Ini_int ("Max_execution_time"), 0);} Enter execution retval = (zend_execute_scripts (Zend_require tsrmls_cc, NULL, 3, Prepend_file_p, Primary_file, append_file_p) = = SU ccess);
OK, if the code executes here and the Max_input_time timeout has not occurred, the timeout for Max_execution_time will be re-specified.
The same is done by calling Zend_set_timeout and passing in Max_execution_time. Pay special attention to the need to explicitly call zend_unset_timeout under Windows to turn off the original timer, while Linux does not. This is due to the different principles of the timer implementation of the two platforms, which will be described in detail below.
Finally, a graph is used to indicate the process of time-out control, and the case on the left indicates that the user is configured with Max_input_time and Max_execution_time. The difference to the right is that the user only configures the Max_execution_time:
Zend_set_timeout
As mentioned earlier, the Zend_set_timeout function is used to set the timer. Specifically, the implementation:
void Zend_set_timeout (long seconds, int reset_signals)/* {{*/{tsrmls_fetch (); Assigned value EG (timeout_seconds) = seconds; #ifdef ZEND_WIN32 if (!seconds) {return; }//Start timer thread 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 insid e a DllMain (), you ' re not supposed to the start threads from it. */Zend_init_timeout_thread (); }//Send Wm_register_zend_timeout message to Thread PostThreadMessage (timeout_thread_id, Wm_register_zend_timeout, (WPARAM) GetCurr Entthreadid (), (LPARAM) seconds), #else//Linux platform under the 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; Set the timer, seconds seconds will send Sigprof signal Setitimer (itimer_prof, &t_r, NULL); } Signo = Sigprof; if (reset_signals) {sigset_t sigset; Set the sigprof signal corresponding to the processing function for zend_timeout signal (Signo, zend_timeout); Anti-shielding Sigemptyset (&sigset); Sigaddset (&sigset, Signo); Sigprocmask (Sig_unblock, &sigset, NULL); } #endif}
The above implementations can basically be completely divided into two types of platforms:
First Look at Linux:
Linux timer is much easier to call Setitimer function on the line, in addition, Zend_set_timeout also set the SIGPROF signal handler for Zend_timeout.
Note that when you call Setitimer, the It_interval is set to 0, indicating that the timer is triggered only once, not once every time. Setitimer can be timed in three ways, using Itimer_prof in PHP, which calculates the execution time of user code and kernel code. Once the time is up, a sigprof signal is generated.
When the PHP process receives the SIGPROF signal, it jumps into the zend_timeout regardless of what is currently executing. Zend_timeout is the function that actually handles timeouts.
Look at Windows again:
First, a child thread is started that is used primarily to set timers while maintaining the EG (timed_out) variable.
Once a child thread is generated, the main thread sends a message to the child thread: Wm_register_zend_timeout. After the child thread receives the Wm_register_zend_timeout, a timer is generated and the timer starts. At the same time, the child thread sets eg (timed_out) = 0. It's important! It is up to the Windows platform to determine if eg (timed_out) is 1, to decide whether to time out.
If the timer is up to the time, the child thread receives the WM_TIMER message, cancels the timer, and sets eg (timed_out) = 1.
If you need to turn off the timer, the child thread receives the WM_UNREGISTER_ZEND_TIMEOUT message. Turning off the timer does not change the eg (timed_out).
The relevant code is still very clear:
Static LRESULT CALLBACK Zend_timeout_wndproc (HWND hwnd, UINT message, WPARAM WPARAM, LPARAM LPARAM) { switch (message) {Case Wm_destroy: postquitmessage (0); break; Generate a timer, start timing case wm_register_zend_timeout:/ * WParam is the thread ID pointer, LParam are 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; Turn off timer case wm_unregister_zend_timeout:/ * WParam is the thread ID pointer * /KillTimer (Timeout_window , WParam); break; Timeout, also need to close timer case Wm_timer: { killtimer (Timeout_window, wParam); EG (timed_out) = 1; } break; Default: return DefWindowProc (hWnd, message, WParam, LParam); } return 0;}
As described above, it is ultimately necessary to jump to zend_timeout to handle timeouts. How does Windows get into the zend_timeout?
Under Window only in the Execute function (where zend_vm_execute.h is just beginning), you can see the call to Zend_timeout:
while (1) { int ret; #ifdef zend_win32 if (EG (timed_out)) { ////Windows Time-out, before executing each opcode to determine if zend_ Timeout zend_timeout (0); } #endif if (ret = Opline->handler (execute_data tsrmls_cc)) > 0) { ... }}
The code above can be seen:
Under Windows, a timeout is determined each time a opcode instruction is executed.
Because the main thread executes opcode, the child thread may have timed out, and Windows has no mechanism to let the main thread stop working at hand and jump directly into zend_timeout. So we have to use the child thread first set eg (timed_out) to 1, and then the main thread until the current opcode execution completes, before entering the next opcode, to determine the eg (timed_out) and then call Zend_timeout.
So, to be precise, the Windows timeout is actually a little bit delayed. At least in the course of a certain opcode execution, it cannot be interrupted. Of course, the execution time of a single opcode can be very short under normal circumstances. But it can be easy to artificially construct some time-consuming functions that make function call wait longer. At this point, if the child thread is determined to have timed out, it will have to wait a long time until the main thread finishes the bar opcode to call zend_timeout.
Zend_unset_timeout
void Zend_unset_timeout (tsrmls_d)/* {{{*/{#ifdef zend_win32 //By sending a wm_unregister_zend_timeout message to turn off the timer 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; Full 0, equivalent to shutdown timer Setitimer (itimer_prof, &no_timeout, NULL); } #endif}
The zend_unset_timeout is also divided into two platforms for implementation.
First Look at Linux:
The shutdown timer under Linux is also simple. As long as the 4 values in the struct itimerval are set to 0, it's OK.
Look at Windows again:
Because Windows is taking advantage of a separate thread to timing. Therefore, Zend_unset_timeout sends the WM_UNREGISTER_ZEND_TIMEOUT message to the thread. Wm_unregister_zend_timeout the corresponding action is to call KillTimer to close the timer. Note that the thread itself does not exit.
The previous article left a problem in Php_execute_script, under Windows to show the call Zend_unset_timeout to turn off the timer, while Linux is not required. Because for a Linux process, there can only be one setitimer timer. That is, repeated calls to Setitimer, the following timer will be directly covered by the previous.
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");}
As mentioned earlier, Zend_timeout is a function that actually handles timeouts. It is also very simple to implement.
If there is a configuration of exit_on_timeout, Zend_on_timeout attempts to invoke Sapi_terminate_process to close the SAPI process. If Exit_on_timeout is not required, go directly to Zend_error for error handling. In most cases, we do not set exit_on_timeout, after all, we expect that although a request has timed out, the process remains and the next request is served.
In addition to printing error logs, Zend_error also uses Longjump to jump to boilout specified stack frames, typically zend_end_try or Zend_catch macros. About Longjump, can be another topic, this article is not specifically described. Inside the Php_execute_script, Zend_error will cause the program to jump to the zend_end_try position and continue execution. Continued execution means that functions such as php_request_shutdown are called to complete the finishing touches.
Until this point, the PHP script's timeout mechanism is clear.
Finally, consider a bug that has a suspected PHP kernel.
Bugs in Max_input_time under Windows
Recall that before the mention of Windows only one place called the Zend_timeout, is the Execute function, exactly the opcode before each execution.
Then, if a timeout of type max_input_time occurs, even if the child thread has set eg (timed_out) to 1, it will have to be deferred to execute for timeout processing. Looks like everything is fine.
The crux of the problem is that we cannot guarantee that the main thread executes to execute, EG (timed_out) is still 1. Once the Timed_out quilt thread is modified to 0 before entering execute, the timeout for the Max_input_time type will never be handle.
Why does eg (timed_out) change the quilt thread to 0? The reason: in Php_execute_script, Zend_set_timeout (Ini_int ("Max_execution_time"), 0) are called to set the timer.
Zend_set_timeout sends a WM_REGISTER_ZEND_TIMEOUT message to the child thread. The child thread receives this message and, in addition to creating the timer, sets eg (timed_out) = 0 (see the Zend_timeout_wndproc code snippet captured above). Because of the uncertainty of thread execution, it is not possible to determine whether a child thread has received a message and set eg (timed_out) to 0 when the main thread executes to execute.
,
If the judgment in execute occurs at the point in time of the red Line callout, eg (timed_out) is 1,execute calls Zend_timeout to do the timeout processing.
If the judgment in execute occurs at the point in time of the Blue Line callout, eg (timed_out) has been reset to the 0,max_input_time timeout being completely masked.