The timeout mechanism of PHP script runtime detailed _php instance

Source: Internet
Author: User
Tags php script signal handler

When doing PHP development, max_input_time and max_execution_time are often set up to control the timeout of the script. But never thought about the principles behind it.

Take advantage of the two days of free time to study the problem.

Timeout configuration

How PHP's INI configuration works, this is a commonplace topic.

First, we are configuring in the php.ini. When PHP starts (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 it to the Configuration_hash table.

OK, then PHP will call Zend_startup_extensions to start each module (including the PHP core module, and all extensions that need to be loaded). In the start function of each module, the Register_ini_entries action is completed. Register_ini_entries is responsible for taking some of the configuration of the module out of the Configuration_hash table and then calling the handler function, which will eventually save the processed value into the module's globals variable.

Max_input_time, max_execution_time These two configurations belong to the PHP core module. For the PHP core, register_ini_entries still occurs in Php_module_startup. Also belong to the PHP core module configuration and expose_php, Display_errors, Memory_limit and so on ...

The schematic diagram is 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 invoke different functions to handle. Let's look directly at Max_execution_time's corresponding function:

Static PHP_INI_MH (onupdatetimeout)
{
  //PHP boot phase walk here
  if (stage = = Php_ini_stage_startup) {
    // Save the timeout setting to eg (timeout_seconds)
    = (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;
}

Only half of the time, because we only need to focus on the start-up phase of PHP, the function is very simple, the max_execution_time into the eg (timeout_seconds).

As for Max_input_time, there is no special processing function, the default is to deposit Max_input_time into PG (MAX_INPUT_TIME).

So, when the register_ini_entries is done, what happens is:

Max_execution_time----> deposit in EG (timeout_seconds)

Max_input_time----> Deposit PG (MAX_INPUT_TIME)

Request Timeout Control

Now we figure out what's going on in PHP's startup phase and continue to see how PHP manages timeouts when it actually handles requests.

The following code is available in the Php_request_startup function:

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

The timing of php_request_startup is very exquisite.

In the case of CGI, Php_request_startup is invoked only if PHP has obtained the original request from the CGI and some CGI environment variables. When the above code actually executes, the SG (REQUEST_INFO) is in a ready state because the request has been received, but the $_get,$_post,$_file in PHP is not yet generated.

To understand from the code:

1, if the user will max_input_time to do-1, or no configuration, then the life cycle of the script is only by the EG (timeout_seconds) constraints.

2, otherwise, request the start 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 passes, the timer notifies the PHP process. The zend_set_timeout below will be specifically analysed.

Php_request_startup completes, then enters the actual execution stage of PHP, namely Php_execute_script. You can see in the Php_execute_script:

Set execution Timeout
if (PG (max_input_time)!=-1) {
#ifdef php_win32
  zend_unset_timeout (Tsrmls_c);//close before timer
# endif
  zend_set_timeout (Ini_int ("Max_execution_time"), 0);
}
 
into execution
retval = (zend_execute_scripts (Zend_require tsrmls_cc, NULL, 3, Prepend_file_p, Primary_file, append_file_p ) = = SUCCESS);

OK, if the code executes here and the Max_input_time timeout has not occurred, the timeout for Max_execution_time will be specified again.

It is also taken to invoke Zend_set_timeout and pass in Max_execution_time. In particular, under Windows you need to explicitly call Zend_unset_timeout to close the original timer, which is not required under Linux. This is due to the different principle of the timer implementation of two platforms, and the following details will be discussed.

Finally, a graph is used to indicate the process of the timeout control, and the case on the left indicates that the user has configured both Max_input_time and Max_execution_time. The difference between the right side is that the user simply configures the Max_execution_time:

Zend_set_timeout

As mentioned earlier, the Zend_set_timeout function is used to set timers. A concrete look at the implementation:

void Zend_set_timeout (long seconds, int reset_signals)/* {{* */{tsrmls_fetch ();
 
The 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 and Zend_startup (), because if Zend * are initialized
     Inside a DllMain (), you ' re not supposed to 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) GetCu
Rrentthreadid (), (LPARAM) seconds);    #else//Linux platform under 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 timer, seconds seconds will send Sigprof signal Setitimer (itimEr_prof, &t_r, NULL);
 
  } Signo = Sigprof;
 
    if (reset_signals) {sigset_t sigset;
     
    The processing function for setting the SIGPROF signal is zend_timeout signal (Signo, zend_timeout);
    Anti-shielding Sigemptyset (&sigset);
    Sigaddset (&sigset, Signo);
  Sigprocmask (Sig_unblock, &sigset, NULL);

 } #endif}

The above implementation can basically be divided into two kinds of platform completely:

First Look at Linux:

Linux timer is much easier to call the Setitimer function on the line, in addition, Zend_set_timeout also set the SIGPROF signal handler for the zend_timeout.

Note that when calling Setitimer, the It_interval is set to 0, indicating that the timer is triggered only once, not once every other time. Setitimer can be timed in three ways, with Itimer_prof in PHP, which calculates the execution time of user code and kernel code. Once the time is up, a sigprof signal will be generated.

When the PHP process receives the SIGPROF signal, it jumps into zend_timeout regardless of what is currently executing. Zend_timeout is the actual processing timeout function.

Look at Windows again:

First, a child thread is started, which is used primarily to set the timer, while maintaining the EG (timed_out) variable.

Once a child thread is generated, the main thread sends a message to the child: Wm_register_zend_timeout. After the child thread receives the Wm_register_zend_timeout, a timer is generated and the timer is started. At the same time, the child thread will set eg (timed_out) = 0. It's important! Under Windows platform, it is by judging if eg (timed_out) is 1 to determine whether to timeout.

If the timer is up to time, the child thread receives the WM_TIMER message, cancels the timer, and sets the EG (timed_out) = 1.

If you need to turn off the timer, the child thread will receive a wm_unregister_zend_timeout message. Turning off the timer does not change eg (timed_out).

The relevant code is still very clear:

 static LRESULT CALLBACK Zend_timeout_wndproc (HWND hwnd, UINT message, WPARAM WPARAM, LPAR
      AM 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 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; Close Timer Case wm_unregister_zend_timeout:/* WParam is the thread ID pointer * * killtimer (Timeout_window,
      WParam);
     
    Break
        Timed out, also need to close the 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 the timeout. So how does Windows get into Zend_timeout?

Window only in the Execute function (where zend_vm_execute.h just started), you can see the call Zend_timeout:

while (1) {
  int ret;
#ifdef zend_win32
  if (EG (timed_out)) {  //timeout under Windows, before executing each opcode before deciding whether to call Zend_timeout
    zend_timeout (0 );
  }
#endif
 
  if ((ret = Opline->handler (execute_data tsrmls_cc)) > 0) {
  ...}}
}

The above code can see:

Under Windows, a timeout judgment is made every 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 allow the main thread to stop the work at hand and jump directly into the zend_timeout. So you have to use the child thread first to set the EG (timed_out) to 1, and then the main thread waits until the current opcode execution completes, before entering the next opcode, Judge eg (timed_out) and then call Zend_timeout.

So to be exact, Windows timeout is actually a bit of a delay. It cannot be interrupted at least in the course of a opcode execution. Of course, under normal circumstances, the execution time of a single opcode will be very short. But it can be very easy to artificially construct some time-consuming functions so that function call needs to wait a long time. At this point, if the child thread is judged to have timed out, it will take a long wait until the main thread completes the bar opcode before calling Zend_timeout.

Zend_unset_timeout

void Zend_unset_timeout (Tsrmls_d)/{{* *
*
/{#ifdef ZEND_WIN32
   
  //by sending Wm_unregister_zend_ Timeout message to close
  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 closing timer
    Setitimer (itimer_prof, &no_timeout, NULL);
#endif
}

Zend_unset_timeout is also divided into two kinds of platform implementations.

First Look at Linux:

The shutdown timer under Linux is also very simple. Just set the 4 values in the struct itimerval to 0.

Look at Windows again:

Because Windows uses a separate thread to time the clock. Therefore, Zend_unset_timeout sends a wm_unregister_zend_timeout message to the thread. Wm_unregister_zend_timeout the corresponding action is to call the KillTimer to close the timer. Note that the thread itself does not exit.

The previous article left a problem, in Php_execute_script, Windows to display call Zend_unset_timeout to close the timer, and Linux does not need. Because for a Linux process, there can only be one setitimer timer. That is, repeated calls to the Setitimer, followed by the timer will directly cover the front.

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 the actual processing timeout function. It is also very simple to implement.

If there is a configuration exit_on_timeout, Zend_on_timeout attempts to invoke the sapi_terminate_process shutdown SAPI process. If Exit_on_timeout is not required, go directly to Zend_error for error handling. Most of the time, we don't set exit_on_timeout, after all, we expect a request to be timed out, but the process remains, serving the next request.

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 invoked to finish the finishing work.

Until here, the timeout mechanism for PHP scripts is clear.

Finally, look at a suspected PHP kernel bug.

Bugs under Windows Max_input_time

Recall that the previous mention of Windows only one place called Zend_timeout, is the Execute function, exactly before each opcode execution.

Then, if the Max_input_time type timeout occurs, even if the child thread makes the eg (timed_out) set to 1, it must be deferred to execute for timeout processing. Looks like everything's fine.

The crux of the problem is that we do not guarantee that the main thread executes to execute when EG (timed_out) is still 1. Once you enter Execute, the EG (timed_out) quilt thread is modified to 0, then the Max_input_time type timeout will never be handle.

Why would eg (timed_out) be modified to 0 in quilt thread? The reason is: in Php_execute_script, the Zend_set_timeout (Ini_int ("Max_execution_time"), 0) is invoked to set the timer.

Zend_set_timeout sends wm_register_zend_timeout messages to the child thread. The child thread receives this message and sets the EG (timed_out) = 0 In addition to creating the timer (see the Zend_timeout_wndproc snippet that was intercepted above). Because of the uncertainty of thread execution, it is not possible to determine whether the child thread has received the message and set eg (timed_out) to 0 when the main thread executes to execute.

As shown in the figure,

If the judgment in execute occurs at the point in time of the red line callout, then eg (timed_out) will call Zend_timeout for 1,execute.

If the judgment in execute occurs at a point in time on the Blue Line callout, then eg (timed_out) has been reset to 0,max_input_time timeout is completely masked.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.