Detailed description of timeout mechanism during php script running

Source: Internet
Author: User
In our normal development, we may have encountered PHP script running timeout. in this case, we often use set_time_limit (non-secure mode ), you can also modify the configuration file and restart the server, or modify the program to reduce the execution time of the program so that it can set max_input_time, max_execution_time, used to control the script timeout. But I have never thought about the principle behind it.

Let's take a look at this question when there is no space in the past two days.

Timeout configuration

How the ini configuration of php works is a common topic.

First, we configure it in php. ini. When php is started (php_module_startup stage), it will try to read the INI file and parse it. The parsing process is simply to analyze the INI file, extract valid key-value pairs, and save them to the configuration_hash table.

OK. Then php will call zend_startup_extensions to start each module (including the php Core module and all the extensions to be loaded ). The REGISTER_INI_ENTRIES action is completed in the Start function of each module. REGISTER_INI_ENTRIES is responsible for extracting some configurations of the module from the configuration_hash table, then calling the processing function, and finally saving the processed values to the globals variable of the module.

The max_input_time and max_execution_time configurations belong to the php Core module. For php Core, REGISTER_INI_ENTRIES still occurs in php_module_startup. The configuration of the php Core module also includes 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

As mentioned above, REGISTER_INI_ENTRIES calls different functions for different configurations. Let's look at the max_execution_time function:

Static PHP_INI_MH (OnUpdateTimeout) {// php start stage here if (stage = PHP_INI_STAGE_STARTUP) {// Save the timeout settings to EG (timeout_seconds) in EG (timeout_seconds) = atoi (new_value); return SUCCESS;} // The ini set in the php execution process goes here zend_unset_timeout (TSRMLS_C); EG (timeout_seconds) = atoi (new_value ); zend_set_timeout (EG (timeout_seconds), 0); return SUCCESS ;}

For the moment, we only need to look at the first half, because currently we only need to pay attention to the php startup phase. This function is very simple and stores max_execution_time into EG (timeout_seconds ).

Max_input_time does not have any special processing functions. by default, max_input_time is saved to PG (max_input_time ).

Therefore, when REGISTER_INI_ENTRIES is completed, the following occurs:

Max_execution_time ----> save to EG (timeout_seconds)

Max_input_time ----> Store to PG (max_input_time)

Request Timeout control

Now let's figure out what happened in the php startup phase. let's continue to look at how php manages timeout when actually processing requests.

The php_request_startup function has the following code:

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 exquisite.

Taking cgi as an example, php_request_startup is called only when php has obtained the original request from CGI and some CGI environment variables. When the above code is actually executed, SG (request_info) is in the ready state because the request has been obtained, but $ _ GET, $ _ POST in php, $ _ FILE and other super global variables are not yet generated.

Understanding from the code:

1. if the max_input_time configuration is-1 or is not configured, the lifecycle of the script is limited only by EG (timeout_seconds.

2. Otherwise, the timeout control in the request startup phase is restricted by PG (max_input_time.

3. the zend_set_timeout function is used to set the timer. Once the specified time passes, the timer will notify the php process. Zend_set_timeout is analyzed in detail below.

After php_request_startup is completed, the actual execution phase of php is entered, that is, php_execute_script. You can see in php_execute_script:

// Set the execution timeout if (PG (max_input_time )! =-1) {# ifdef PHP_WIN32 zend_unset_timeout (TSRMLS_C); // disable the previous timer # endif zend_set_timeout (INI_INT ("max_execution_time"), 0 );} // enter the execution retval = (zend_execute_scripts (ZEND_REQUIRE TSRMLS_CC, NULL, 3, prepend_file_p, primary_file, append_file_p) = SUCCESS );

OK. if max_input_time has not timed out before the code is executed, max_execution_time will be re-specified.

Similarly, zend_set_timeout is called and max_execution_time is passed in. Note that in windows, zend_unset_timeout needs to be explicitly called to disable the original timer, which is not required in linux. This is due to the different implementation principles of the timer on the two platforms, which will be described in detail below.

The last figure shows the timeout control process. The case on the left shows that you have configured both max_input_time and max_execution_time. The difference on the right is that the user only configures max_execution_time:

Zend_set_timeout

As mentioned above, the zend_set_timeout function is used to set the timer. The specific implementation is as follows:

Void zend_set_timeout (long seconds, int reset_signals)/* {*/{TSRMLS_FETCH (); // assign values to EG (timeout_seconds) = seconds; # ifdef ZEND_WIN32 if (! Seconds) {return;} // start the 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 inside a DllMain (), you're not supposed to start threads from it. */zend_init_timeout_thread ();} // send the WM_REGISTER_ZEND_TIMEOUT message PostThreadMessage (timeout_thread_id, timeout, (WPARAM) GetCurrentThreadId (), (LPARAM) seconds) to the thread ); # else // struct itimerval t_r on linux;/* timeout requested */int signo; if (seconds) {t_r.it_value. TV _sec = seconds; timeout = t_r.it_interval. TV _sec = timeout = 0; // Set the timer. after seconds, the SIGPROF signal setitimer (ITIMER_PROF, & t_r, NULL) will be sent;} signo = SIGPROF; if (reset_signals) {sigset_t sigset; // Set the processing function corresponding to the SIGPROF signal to zend_timeout signal (signo, zend_timeout); // blocked sigemptyset (& sigset); sigaddset (& sigset, signo); sigprocmask (SIG_UNBLOCK, & sigset, NULL) ;}# endif}

The above implementation can basically be divided into two platforms:

First look at linux:

In linux, the timer is much easier. you can call the setitimer function. In addition, zend_set_timeout sets the handler of the SIGPROF signal to zend_timeout.

Note: When you call setitimer, set it_interval to 0, indicating that the timer is triggered only once, instead of once at intervals. Setitimer can be timing in three ways. in php, ITIMER_PROF is used to calculate the execution time of user code and kernel code at the same time. Once the time is reached, a SIGPROF signal will be generated.

When the php process receives the SIGPROF signal, no matter what is being executed, it will jump to zend_timeout. Zend_timeout is the function for processing timeout.

Look at windows again:

First, a subthread is started. This thread is mainly used to set the timer and maintain the EG (timed_out) variable.

Once the sub-thread is generated, the main thread will send a message to the sub-thread: WM_REGISTER_ZEND_TIMEOUT. After the sub-thread receives WM_REGISTER_ZEND_TIMEOUT, a timer is generated and the timer starts. At the same time, the sub-thread will set EG (timed_out) = 0. This is important! In windows, the timeout is determined by determining whether EG (timed_out) is 1.

If the timer reaches the time, the subthread receives the WM_TIMER message, cancels the timer, and sets EG (timed_out) to 1.

If you need to disable the timer, the subthread will receive the message WM_UNREGISTER_ZEND_TIMEOUT. Turn off the timer and will not change EG (timed_out ).

The related code is clear:

Static lresult callback zend_timeout_WndProc (HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {switch (message) {case WM_DESTROY: PostQuitMessage (0); break; // generates 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; // disable the timer case WM_UNREGISTER_ZEND_TIMEOUT: /* wParam is the thread id pointer */KillTimer (timeout_window, wParam); break; // timed out. you also need to disable the timer case WM_TIMER: {KillTimer (timeout_window, wParam ); EG (timed_out) = 1;} break; default: return DefWindowProc (hWnd, message, wParam, lParam);} return 0 ;}

According to the above description, the zend_timeout must be redirected to process the timeout. In windows, how does one access zend_timeout?

Window is only in the execute function (the place where zend_vm_execute.h was just started). you can see that zend_timeout is called:

While (1) {int ret; # ifdef ZEND_WIN32 if (EG (timed_out) {// timeout in windows, determine whether to call zend_timeout (0) before executing each opcode;} # endif if (ret = OPLINE-> handler (execute_data TSRMLS_CC)> 0) {...}}

The code above shows:

In windows, every time an opcode command is executed, a timeout judgment is performed.

Because when the main thread executes opcode, the sub-thread may have timed out, and windows does not have any mechanism to stop the work of the main thread and directly jump into zend_timeout. Therefore, we have to use the sub-thread to set EG (timed_out) to 1, and then the main thread will judge EG (timed_out) before calling zend_timeout until the current opcode execution is complete and enters the next opcode.

Therefore, to be accurate, windows timeout is actually a little delayed. At least one opcode cannot be interrupted during execution. Of course, under normal circumstances, the execution time of a single opcode will be very short. However, it is easy to construct some time-consuming functions manually, so that the function call takes a long time. At this time, if the sub-thread determines that it has timed out, it will take a long wait until the main thread completes the opcode before calling zend_timeout.

Zend_unset_timeout

Void zend_unset_timeout (TSRMLS_D)/* {*/{# ifdef ZEND_WIN32 // disable the timer if (timeout_thread_initialized) {PostThreadMessage (timeout_thread_id, timeout, (WPARAM) by sending the timeout message) getCurrentThreadId (), (LPARAM) 0) ;}# else if (EG (timeout_seconds) {struct itimerval no_timeout; no_timeout.it_value. TV _sec = timeout = 0; // all 0, it is equivalent to disabling the timer setitimer (ITIMER_PROF, & no_timeout, NULL);} # endif}

Zend_unset_timeout is also divided into two platforms.

First look at linux:

In linux, it is also very easy to disable the timer. Set all four values in struct itimerval to 0.

Look at windows again:

Windows uses an independent thread for timing. Therefore, zend_unset_timeout will send the WM_UNREGISTER_ZEND_TIMEOUT message to the thread. The corresponding action of WM_UNREGISTER_ZEND_TIMEOUT is to call KillTimer to disable the timer. Note that the thread itself does not quit.

The previous article left a problem. in php_execute_script, in windows, zend_unset_timeout is called to disable the timer, which is not required in linux. For a linux process, only one setitimer timer exists. That is to say, if you call setitimer repeatedly, the subsequent timer will directly overwrite the previous one.

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 above, zend_timeout is a function that actually processes timeout. Its implementation is also very simple.

If exit_on_timeout is configured, zend_on_timeout tries to call sapi_terminate_process to disable the sapi process. If you do not need exit_on_timeout, you can directly enter zend_error for error handling. In most cases, we do not set exit_on_timeout. after all, we expect that although a request times out, the process will still be retained to serve the next request.

In addition to printing error logs, zend_error also uses longjump to jump to the stack frame specified by boilout, which is generally the place where zend_end_try or zend_catch macro is located. For longjump, you can start another topic, which is not described in this article. In php_execute_script, zend_error will redirect the program to the zend_end_try position and continue execution. Continuing execution means that php_request_shutdown and other functions are called to complete the final work.

Until now, the timeout mechanism of php scripts is clear.

Finally, let's look at a suspected php kernel bug.

Max_input_time bug in windows

Recall that zend_timeout is called in only one place in windows, that is, in the execute function, before each opcode is executed.

If a max_input_time-out occurs, even if the sub-thread sets EG (timed_out) to 1, it must be delayed to execute for timeout processing. Everything looks normal.

The key to the problem is that we cannot guarantee that when the main thread runs to execute, the EG (timed_out) is still 1. Once the EG (timed_out) quilt thread is changed to 0 before the execute, the max_input_time type timeout will never be handle.

Why does EG (timed_out) change the quilt thread to 0 again? The reason is: in php_execute_script, zend_set_timeout (INI_INT ("max_execution_time"), 0) is called to set the timer.

Zend_set_timeout will send the WM_REGISTER_ZEND_TIMEOUT message to the subthread. When the subthread receives this message, in addition to creating a timer, it also sets EG (timed_out) = 0 (see the zend_timeout_WndProc code snippet intercepted above ). Due to the uncertainty of thread execution, it is impossible to determine whether the subthread has received the message and set EG (timed_out) to 0 when the main thread executes to execute.

,

If the judgment in execute occurs at the time point marked by the Red Line, the EG (timed_out) is 1, and the execute will call zend_timeout for timeout processing.

If the judgment in execute occurs at the time point marked by the blue line, EG (timed_out) has been reset to 0, and 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.