John Bafford
PHP Misleading Error: Maximum execution time of 0 seconds exceeded
Yesterday, on freenode #phpc, someone posted this curious error message:
PHP Fatal error: Maximum execution time of 0 seconds exceeded
Jokes aside of how, only eight hours into December, they had exceeded their monthly allotment of PHP time, this is a rather curious error message to receive: PHP’s set_time_limit() function and associated max_execution_time ini setting define a limit of 0 to mean no limit. A script with no limit should not be hitting a maximum execution time of zero seconds!
While several of us tried brainstorming possible causes, additional details were provided, including that the script, running on PHP 7 on unix, under php-fpm, was twice terminated after running for roughly two hours.
I set out to look at PHP’s source code, certain that there would be something that would shed light on this mystery.
Let’s investigate PHP internals
The first thing I did was search for “Maximum execution time” in the PHP sources, hoping to find exactly one hit. Ignoring test cases, that string appears exactly once in the source, in the zend_timeout() function in zend_execute_API.c .
When zend_timeout() is called, PHP calls a function specified by the SAPI (the interface between PHP and the web server) if one is defined, and then emits an error and exits.
Knowing now where the error message is generated (and that it only comes from one place), I needed to determine what calls zend_timeout() , so I searched the code for that function’s name. Excluding the function’s own definition, and its declaration in a header file, there were four results. Two of those results were specific to Windows support, and could be ignored.
The other two results were conveniently located next to each other in zend_set_timeout() , both used as the callback function to a signal handler.
This means that, in normal use, the execution time limit message can only be generated in response to receiving a signal.
Signals
In UNIX-like systems, signals provide for a limited form of inter-process communication, in the form of sending and receiving an interrupt with a numeric identifier. The Wikipedia page on signals provides additional detail.
There C standard defines six signals, but many operating systems define and use many additional signals.
Some signals sent to a process cause its unconditional termination (e.g. SIGKILL ) or have other operating-system level effects (such as SIGSTOP and SIGCONT ). Most other signals can be caught by the target process if it has installed a signal handler to receive that notification. (If a signal handler is not installed, a default handler is used, which often, but not always, results in the self-termination of the process.)
How PHP’s execution time limit works
So, then, what signal triggers zend_timeout() ? A few lines up indicates the answer: SIGPROF . (Alternatively, SIGALRM is used when running under Cygwin on Windows.)
SIGPROF was a new one to me. Wikipedia says:
The SIGALRM , SIGVTALRM and SIGPROF signal is sent to a process when the time limit specified in a call to a preceding alarm setting function (such as setitimer ) elapses. SIGALRM is sent when real or clock time elapses. SIGVTALRM is sent when CPU time used by the process elapses. SIGPROF is sent when CPU time used by the process and by the system on behalf of the process elapses.
And indeed, there is a call to setitimer() before PHP installs the signal handler on SIGPROF . The man page for setitimer() describes how to use it to set a timer that counts time the process spends executing, triggering a SIGPROF after the timer elapses.
Some more searching through PHP’s code makes clear how the entire process works: when PHP is loaded, set_time_limit() is called, or max_execution_time is changed, PHP clears the timer (if set), and re-sets the timer (if the timeout is non-zero). Additionally, during request start-up, the signal handler on SIGPROF is installed (regardless of whether a timeout is actually set).
When the timer set by setitimer fires, the SIGPROF handler, zend_timeout() runs, displays the error message with the number of seconds filled in from the current configuration, and exits.
Reading between the lines
That answers the question of how the execution time limit error was displayed. But it doesn’t answer why: one would naturally assume that since the timer isn’t set, the operating system won’t send SIGPROF , and the script wouldn’t be terminated.
The answer lies in one further detail of the UNIX signal mechanism: any process can send any other process any signal (except where disallowed by security policy, such as for processes owned by a different user).
This means that a script doesn’t actually need to hit the time limit to be killed. PHP just needs to be told that it has.
You can test this yourself by doing something similar to this:
$ php -r 'posix_kill(getmypid(), SIGPROF);' Fatal error: Maximum execution time of 0 seconds exceeded in Command line code on line 1
So, what really happened?
Unfortunately, a signal does not come with the identity of its source. Were that the case, it would have been easy to determine what was killing the process and what configuration to change to stop it. In this case, the resolution was to modify the php code to “only” take 20 minutes to run, so the timeout was no longer an issue.
If we make the assumption that there wasn’t a malicious user on the server sending unwanted signals as a denial of service attack, the documentation for max_execution_time hints at one possibility:
Your web server can have other timeout configurations that may also interrupt PHP execution. Apache has a Timeout directive and IIS has a CGI timeout function. Both default to 300 seconds. See your web server documentation for specific details.
With some further research, I found that neither Apache nor nginx explicitly send SIGPROF , though. And while nginx does use setitimer , its use triggers a SIGALRM .
My best guess is that there likely was some sort of watchdog process on the server that killed off the process running php after it consumed too many resources (either memory or time).
It’s probably a bug (or at least, undesirable confusion) in PHP that a SIGPROF that doesn’t arise strictly from a timer expiration displays the same message as if the timer did expire, but this looks to be correctable.
This entry was posted on Friday, December 2nd, 2016 at 4:19 pm and is filed under Development, PHP. You can follow any responses to this entry through the RSS 2.0 feed. Both comments and pings are currently closed.
Как отловить фатальную ошибку: в PHP превышено максимальное время выполнения 30 секунд
Это случилось, когда я делал что-то нереалистичное, но тем не менее это могло случиться с пользователем. Кто-нибудь знает, есть ли способ поймать это исключение? Я читал, но все, кажется, предлагают увеличить время.
Я считаю, что как только время выполнения будет превышено, сценарий будет прерван. В этом случае сценарий, который мог перехватить исключение, уже был уничтожен.
Неустранимые ошибки не могут быть перехвачены (они не являются исключениями) или обработаны. Вы можете обрабатывать завершение сценария только изящно, зарегистрировав функцию завершения работы, но сценарий завершится впоследствии.
Нет, возможный дубликат не является «точным» дубликатом. Он не запрашивает фатальную ошибку, вызванную тайм-аутом скрипта, а только как поймать фатальную ошибку в целом. Но поскольку генерал также отвечает на конкретное в этом случае, он квалифицируется как возможный дубликат. Если вопрос заключается в том, как решить ошибку тайм-аута сценария, то возможно, что он является дубликатом большинства из них stackoverflow.com/search?q=maximum+execution+time+php 😉
Вообще говоря, я не могу ответить на это, но в некоторых ситуациях вы можете ограничить время на чем-то меньшем, чем max_execution_time, а затем это отловить. Например, curl_setopt ($ client, CURLOPT_TIMEOUT, 27); заставит curl сдаваться через 27 секунд, чтобы не допустить критической ошибки.
8 ответов
Ваши единственные варианты — увеличить допустимое время выполнения (установка его на 0 делает его бесконечным, но это не рекомендуется) из script или порождает новый поток и надеется на лучшее.
Причина, по которой это не увлекательно, заключается в том, что ее не бросают. Ни одна строка кода не вызвала ошибку, скорее, PHP сказал: «Нет, простите, это слишком долго. Пора прекратить сейчас». И это имеет смысл. Представьте себе, что script с максимальным временем выполнения 30 секунд поймать эту ошибку и занять еще 30 секунд. в плохо разработанной программе, которая открывает некоторые довольно неприятные возможности для использования. Как минимум, это создаст возможности для DOS.
Это может работать в некоторых случаях, но не во всех. Вероятно, было бы лучше, если бы это было в какой-то форме цикла, чтобы вы могли регулярно тестировать.
Хм. Причина, по которой я это заметил, была при получении пакетов. Я установил размер пакета на что-то очень маленькое, и его отправка заняла целую вечность. Я думаю, что если было отправлено много данных с размером пакета около 1024 байт, я мог бы установить собственное время ожидания в этом цикле и реагировать в течение стольких секунд, чтобы предупредить пользователя.
Я возражаю против того, чтобы PHP нанимал насчет того, допускает ли недостаток кодирования бесконечные циклы, или DOS-эксплойты и т. Д. Я думаю, что программист должен решать эту ошибку. Может быть, тайм-аут происходит во время cURL, и я хочу, чтобы страница обновлялась бесконечно, пока она не переподключится к URL (потому что линия T1 не работает, или, возможно, удаленный сервер, кто знает?). Я не хочу, чтобы мой препроцессор, будь то php, asp, cgi и т. Д., Принимал это решение за меня. Я не думаю, что препроцессор или даже клиентский язык, такой как javascript, должны принимать этические или моральные решения от моего имени.
Существуют и другие варианты, связанные с таймаутами, но они не рассматриваются в этом вопросе. Наиболее распространенная практика — просто убедиться, что запрос не занимает более 60 секунд. В этот момент вы не будете публично сталкиваться в любом случае.
Как насчет того, чтобы в качестве документации PHP (ну. хотя бы один из ее читателей) сказать:
register_shutdown_function('shutdown'); ini_set('max_execution_time',1 ); sleep(3); ?>
Посмотрите на следующие ссылки:
Я проверял это. Но все равно фатальное сообщение об ошибке отображается. Можно ли этого избежать. В любом случае я использовал ini_set (‘display_errors’, ‘0’);
sleep () на самом деле не влияет на счетчик ограничения времени, что странно, но верно. Поэтому вы должны сделать что-то еще, чтобы протестировать свою функцию shutdown () — пара вложенных циклов for, каждый из которых повторяет миллион раз, должна это делать.
@BobbyJack Хотя это странно, это имеет смысл, так как спящий поток останавливает выполнение на некоторый промежуток времени. Поскольку это не выполняется, это время не считается истекшим временем выполнения.
Это не исключение, это ошибка. Существуют важные различия между исключениями и ошибками, прежде всего ошибки не могут быть захвачены семантикой try/catch.
PHP-скрипты построены вокруг парадигмы короткого времени выполнения, поэтому по умолчанию PHP настроен на предположение, что если script работает более 30 секунд, он должен быть пойман в бесконечном цикле и поэтому должен быть завершен, Это должно предотвратить ошибочный PHP скрипт, вызывающий отказ в обслуживании, либо случайно, либо злонамеренным намерением.
Однако для сценариев иногда требуется больше времени работы, чем по умолчанию.
Вы можете попробовать изменить максимальное время выполнения, используя set_time_limit() или изменив значение max_execution_time в файле php.ini , чтобы повысить лимит. вы также можете полностью удалить предел, установив время выполнения равным 0, хотя это не рекомендуется.
set_time_limit() может быть отключен такими механизмами, как disable_functions , поэтому он может быть недоступен для вас, также у вас может не быть доступа к php.ini . Если это так, тогда вам следует обратиться за помощью к своему хозяину.
Одно из исключений — это скрипты PHP, выполняемые из командной строки . В этих условиях работы скрипты PHP могут быть интерактивными и нуждаются в длительной обработке данных или ожидании ввода. По этой причине по умолчанию для сценариев, запущенных из командной строки, не существует предела max_execution_time .