Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

Explanation of the timeout mechanism when php scripts are running

2025-04-01 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/03 Report--

This article mainly explains "the explanation of timeout mechanism when php script runs". Interested friends may wish to take a look at it. The method introduced in this paper is simple, fast and practical. Now let the editor to take you to learn the "php script runtime timeout mechanism explanation" bar!

Timeout configuration

How php's ini configuration works is a clich é topic.

First, we configure it in php.ini. When php starts (the php_module_startup phase), it attempts to read the ini file and parse it. To put it simply, the parsing process is to analyze the ini file, extract the legal key-value pairs, and save them to the configuration_ hash table.

OK, and then php further calls zend_startup_extensions to start each module (including the php Core module, as well as any extensions that need to be loaded). In the startup function of each module, the REGISTER_INI_ENTRIES action is completed. REGISTER_INI_ENTRIES is responsible for extracting some configurations corresponding to the module from the configuration_ hash table, then calling the processing function, and finally storing the processed value into the module's globals variable.

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

As mentioned above, REGISTER_INI_ENTRIES will call different functions to handle different configurations. Let's look directly at the corresponding function of max_execution_time:

Static PHP_INI_MH (OnUpdateTimeout) {/ / php startup phase go here if (stage = = PHP_INI_STAGE_STARTUP) {/ / Save the timeout setting to EG (timeout_seconds) EG (timeout_seconds) = atoi (new_value); return SUCCESS;} / / ini set during 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 look at the top half, because we only need to focus on the startup phase of php, which simply stores the max_execution_time in EG (timeout_seconds).

As for max_input_time, there is no special handler, and the default is to save max_input_time into PG (max_input_time).

So, when the REGISTER_INI_ENTRIES is complete, what happens is:

Max_execution_time-> save to EG (timeout_seconds)

Max_input_time-> save to PG (max_input_time)

Request timeout control

Now that we know what happened in the startup phase of php, let's move on to see how php manages timeouts when actually processing requests.

There is the following code 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 exquisite.

In the case of cgi, php_request_startup is called only after php has obtained the original request and some CGI environment variables from CGI. When the above code is actually executed, the SG (request_info) is ready because the request has been obtained, but the superglobal variables such as $_ GET,$_POST,$_FILE in php have not yet been generated.

Understand from the code:

1. If the user configures max_input_time as-1, or if it is not configured, then the life cycle of the script is only constrained by EG (timeout_seconds).

2. Otherwise, the timeout control of the request startup phase is constrained by PG (max_input_time).

3. The zend_set_timeout function is responsible for setting the timer. Once the specified time has passed, the timer notifies the php process. Zend_set_timeout will be analyzed in detail below.

When the php_request_startup is completed, it enters the actual execution phase of php, 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); / / close 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 the code executes here and the max_input_time timeout has not occurred, the max_execution_time timeout will be reassigned.

It also takes a call to zend_set_timeout and passes in max_execution_time. In particular, note that the one under windows needs to explicitly call zend_unset_timeout to turn off the original timer, but not under linux. This is due to the different implementation principles of timers on the two platforms, which will be described in detail below.

Finally, a diagram is used to show the flow of timeout control, and the case on the left shows that the user has configured both max_input_time and max_execution_time. The difference on the right is that the user has only configured max_execution_time:

Zend_set_timeout

As mentioned earlier, the zend_set_timeout function is used to set the timer. Specifically, let's look at the implementation:

Void zend_set_timeout (long seconds, int reset_signals) / * {{* / {TSRMLS_FETCH (); / / assign EG (timeout_seconds) = seconds; # ifdef ZEND_WIN32 if (! seconds) {return } / / start timer thread if (timeout_thread_initialized = = 0 & & InterlockedIncrement (& timeout_thread_initialized) = = 1) {/ * We startup 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 WM_REGISTER_ZEND_TIMEOUT messages PostThreadMessage (timeout_thread_id, WM_REGISTER_ZEND_TIMEOUT, (WPARAM) GetCurrentThreadId (), (LPARAM) seconds) to threads; # else / / struct itimerval tweeter on linux platform; / * 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 to send SIGPROF signal setitimer (ITIMER_PROF, & tweer, NULL) after seconds seconds;} signo = SIGPROF; if (reset_signals) {sigset_t sigset / / set the processing function corresponding to SIGPROF signal to zend_timeout signal (signo, zend_timeout); / / Anti-masking sigemptyset (& sigset); sigaddset (& sigset, signo); sigprocmask (SIG_UNBLOCK, & sigset, NULL);} # endif}

The above implementation can basically be completely divided into two platforms:

Let's take a look at linux:

The timer under linux is much easier, just call the setitimer function. In addition, zend_set_timeout also sets the handler of the SIGPROF signal to zend_timeout.

Note that when you call setitimer, set it_interval to 0, indicating that the timer is triggered only once, not at regular intervals. Setitimer can be timed in three ways. ITIMER_PROF is used in php, which calculates the execution time of both user code and kernel code. Once the time is up, a SIGPROF signal will be generated.

When the php process receives a SIGPROF signal, no matter what it is currently executing, it jumps into the zend_timeout. Zend_timeout is the function that actually handles timeouts.

Take a look at windows:

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

Once the 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, it generates a timer and starts timing. At the same time, the subthread sets EG (timed_out) = 0. This is important! Under the windows platform, it is by judging whether the EG (timed_out) is 1 or not to determine whether or not to time out.

If the timer is up and the child thread receives the WM_TIMER message, cancel the timer and set EG (timed_out) = 1.

If the timer needs to be turned off, the child thread receives a WM_UNREGISTER_ZEND_TIMEOUT message. Turning off the timer does not change the EG (timed_out).

The relevant code is still 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 and 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; / / turn off timer case WM_UNREGISTER_ZEND_TIMEOUT: / * wParam is the thread id pointer * / KillTimer (timeout_window, wParam); break / / timed out, also need to turn off 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, eventually you need to jump to zend_timeout to handle timeouts. So how to enter zend_timeout under windows?

Under window, you can see the call to zend_timeout only in the execute function (where zend_vm_execute.h just started):

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

The above code can be seen:

Under windows, each time an opcode instruction is executed, a timeout judgment is made.

Because the child thread may have timed out while the main thread executes opcode, windows has no mechanism to make the main thread stop the work at hand and jump directly into the zend_timeout. So we have to use the child thread to set EG (timed_out) to 1, and then the main thread determines the EG (timed_out) before calling zend_timeout until the current opcode execution completes and moves on to the next opcode.

So exactly speaking, the timeout of windows is actually a little bit delayed. At least during the execution of one opcode, it cannot be interrupted. Of course, under normal circumstances, the execution time of a single opcode is very short. But it is easy to artificially construct some time-consuming functions that make function call have to wait a long time. At this point, if the child 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 / / turn off 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; / / set all 0, which is equivalent to turning off timer setitimer (ITIMER_PROF, & no_timeout, NULL);} # endif}

Zend_unset_timeout is also divided into two platform implementations.

Let's take a look at linux:

The shutdown timer under linux is also very simple. Just set all four values in struct itimerval to 0, and that's fine.

Take a look at windows:

Because windows uses a separate thread for timing. Therefore, zend_unset_timeout sends an WM_UNREGISTER_ZEND_TIMEOUT message to the thread. The corresponding action of WM_UNREGISTER_ZEND_TIMEOUT is to call KillTimer to turn off the timer. Note that the thread itself does not exit.

The previous article left a problem, in php_execute_script, the windows under the display to call zend_unset_timeout to turn off the timer, but not in linux. Because there can only be one setitimer timer for a linux process. That is, if you call setitimer repeatedly, the following 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 earlier, zend_timeout is a function that actually handles timeouts. Its implementation is also very simple.

If exit_on_timeout is configured, zend_on_timeout attempts to call sapi_terminate_process to shut down the sapi process. If you don't need exit_on_timeout, go directly to zend_error for error handling. In most cases, we do not set exit_on_timeout, after all, we expect that although one request times out, the process remains 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, usually where the zend_end_try or zend_ catch macro is located. With regard to longjump, we can start another topic, which will not be described in detail in this article. In php_execute_script, zend_error causes the program to jump to the location of zend_end_try and continue execution. To continue execution means that functions such as php_request_shutdown will be called to complete the finishing touches.

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

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

Bug of max_input_time under windows

Recall that it was mentioned earlier that there is only one place under windows where zend_timeout is called, that is, in the execute function, exactly before each opcode is executed.

Then, if a max_input_time type timeout occurs, even if the child thread sets EG (timed_out) to 1, it will have to be delayed to execute to handle the timeout. Everything seems to be fine.

The crux of the problem is that there is no guarantee that the EG (timed_out) will still be 1 when the main thread executes to execute. Once the EG (timed_out) quilt thread is modified to 0 before entering execute, the max_input_time type timeout will never be handle.

Why did EG (timed_out) change the quilt child thread to 0? 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 sends a WM_REGISTER_ZEND_TIMEOUT message to the child thread. The child thread receives this message and, in addition to creating a timer, sets EG (timed_out) = 0 (see the zend_timeout_WndProc snippet intercepted above for details). Due to 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 time marked by the red line, then if EG (timed_out) is 1, zend_timeout will be called to handle the timeout.

If the judgment in execute occurs at the time marked by the blue line, then the EG (timed_out) has been reset to 0magentic maxillary inputtimeout time is completely masked.

At this point, I believe that everyone on the "php script runtime timeout mechanism description" have a deeper understanding, might as well to the actual operation of it! Here is the website, more related content can enter the relevant channels to inquire, follow us, continue to learn!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report