PHP, set_time_limit, exec
Всем привет! Давненько я не писал про интересные штуки при программировании на php. Сегодня я опишу один хак, который я нашел столкнувшись с проблемой запуска программ из php..
Итак, у нас есть задача: надо запускать некий внешний софт из php, после чего нужно обработать результат работы внешнего приложения. Делать это можно несколькими способами, например с помощью функции exec.
Например, так:
1 2 3 |
exec('ps'); |
Получить результат работы в php можно так:
1 2 3 4 5 6 7 8 9 10 11 |
$cmd = 'ps'; $result = exec("$cmd 2>&1", $out, $err); echo 'RESULT:'.PHP_EOL; print_r($result); echo PHP_EOL.'OUT:'.PHP_EOL; print_r($out); echo PHP_EOL.'ERR:'.PHP_EOL; print_r($err); |
Создаем файл test.php с таким содержимым и запускаем его так из консоли:
1 2 3 |
php test.php |
Результатом работы будет следующий вывод:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# php -f test.php RESULT: 29509 pts/1 00:00:00 ps OUT: Array ( [0] => PID TTY TIME CMD [1] => 10063 pts/1 00:00:00 bash [2] => 21507 pts/1 00:00:00 htop [3] => 23172 pts/1 00:00:00 htop [4] => 24745 pts/1 00:00:00 htop [5] => 25321 pts/1 00:00:00 watch [6] => 29506 pts/1 00:00:00 php [7] => 29508 pts/1 00:00:00 sh [8] => 29509 pts/1 00:00:00 ps ) ERR: 0 |
Теперь поговорим о set_time_limit, данная функция, позволяет прервать выполнение скрипта, в случае если его время работы превысило время установленное в этой функции, самый простой пример на php может выглядеть так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?php set_time_limit(3); echo 'Start'.PHP_EOL; $start_sec = 0; while (1) { if ($start_sec!=date('s')) { echo date('H:i:s').PHP_EOL; $start_sec = date('s'); } } echo 'Finish'.PHP_EOL; |
Вот какой будет результат:
1 2 3 4 5 6 7 8 9 |
# php test1.php Start 13:39:50 13:39:51 13:39:52 13:39:53 PHP Fatal error: Maximum execution time of 3 seconds exceeded in /root/test/test1.php on line 10 |
Это отличный вариант, однако при вызове внешних команд, время не учитывается, модернизируем скрипт так и проверим:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php set_time_limit(3); echo 'Start '.date('H:i:s') .PHP_EOL.'---'.PHP_EOL; $cmd = 'sleep 10 && echo \'External script call\''; $result = exec("$cmd 2>&1", $out, $err); echo 'RESULT:'.PHP_EOL; print_r($result); echo PHP_EOL.'OUT:'.PHP_EOL; print_r($out); echo PHP_EOL.'ERR:'.PHP_EOL; print_r($err); echo PHP_EOL.'---'.PHP_EOL .'Finish '.date('H:i:s'); |
Результат:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# php test3.php Start 13:46:21 --- RESULT: External script call OUT: Array ( [0] => External script call ) ERR: 0 --- Finish 13:46:31 |
Как видите скрипт отработал 10 секунд и не был прерван, т.к. 10 секунд работала внешняя программа.
При разработке, я столкнулся со следующей проблемой: при запуске внешней программы, она иногда зависала по каким-то неведомым мне причинам. Т.е. управление не возвращалось в php скрипт.
Т.к. в моем случае, я предусмотрел ограничение запуска только указанного кол-ва процессов моего скрипта, то я отделался лишь несколькими зависшими экземплярами, после чего нужная работа попросту прекратилась. Если бы такого ограничения не было, память сервера довольно быстро бы заполнилась и получились бы проблемы.
Подобного поведения можно добиться например таким скриптом:
1 2 3 4 5 6 7 8 9 10 11 12 |
<?php $cmd = 'watch ps'; $result = exec("$cmd 2>&1", $out, $err); echo 'RESULT:'.PHP_EOL; print_r($result); echo PHP_EOL.'OUT:'.PHP_EOL; print_r($out); echo PHP_EOL.'ERR:'.PHP_EOL; print_r($err); |
Скрипт будет работать, пока Вы не нажмете Ctrl+C (разумеется если вы его запустите из консоли)
Как же можно решить такую проблему?
Есть несколько вариантов:
- Делать вызовы через семейство pcntl функций, и отслеживать время работы.
- Как-то ограничить время выполнения на стороне запуска внешней команды
Т.к. в моем случае, использование pcntl функций, значило бы переписывание части функционала, то меня такая перспектива не очень радовала. Поэтому я решил использовать второй вариант, а именно модифицировать команду запуска внешнего приложения таким образом, чтобы она автоматически завершалась по таймауту. Этот способ было очень просто реализовать, т.к. команды запуска были вынесены в конфигурационный файл.
Сделать это можно с помощью команды timeout вот так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?php echo 'Start: '.date('H:i:s').PHP_EOL .'---'.PHP_EOL; $cmd = 'timeout -k 3s 10s watch ps'; $result = exec("$cmd 2>&1", $out, $err); echo 'RESULT:'.PHP_EOL; print_r($result); echo PHP_EOL.'OUT:'.PHP_EOL; print_r($out); echo PHP_EOL.'ERR:'.PHP_EOL; print_r($err); echo PHP_EOL.'---'.PHP_EOL .'Finish: '.date('H:i:s').PHP_EOL; |
Формат команды timeout такой:
1 2 3 |
#timeout -sСИГНАЛ -kТАЙМАУТ_2 ТАЙМАУТ_1 КОМАНДА |
- СИГНАЛ = сигнал который будет послан после времени указанного в ТАЙМАУТ_1. Обычно это SIGHUP или SIGTERM.
- ТАЙМАУТ_1 = время после которого приложению будет послан сигнал SIGTERM, если не указан другой в пераметре -s. Время можно указывать с префикасми (s=секунда, m=минута, h=часы, d=дни), например: 20m = 20 минут
- ТАЙМАУТ_2 = время после которого приложению будет послан сигнал SIGKILL, т.е. оно будет завершено. Начинает отсчитываться, после наступления ТАЙМАУТ_1. Т.е. время данное на завершение.
- Команда возвращает код ошибки 124 в случае таймаута, либо код возвращения запущенной программы в случае если таймаута не было.
Результат:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# php test.php Start: 14:06:55 --- RESULT: OUT: Array ( [0] => ) ERR: 124 --- Finish: 14:07:05 |
Как видим через 10 секунд наша программа завершилась по таймауту, что нам и требовалось.
Есть еще один вариант, который я написал, до того, как нашел информацию о существовании команды timeout, вот он:
1 2 3 |
# watch ps & PID=$! bash -c 'sleep 5; kill -TERM $PID; sleep 1; kill $PID' |
В принципе, он делает тоже самое, что и команда выше, только с более сложной записью.
Здесь мы делаем следующее:
- Запускаем команду в фоне
- Параллельно запоминаем PID запущенной команды в переменную
- Далее ждем 5 секунд и отправляем приложению SIGTERM
- После чего ждем еще 1 секунду и отправляем приложению SIGKILL
Данный способ пригодится тем, у кого не будет утилиты timeout. Собственно именно таким образом я и решил возникшую у меня проблему, теперь в случае зависание внешних утилит они автоматически убиваются.
Если Вы знаете другие варианты решения такой проблемы, напишите в комментариях.
Author: | Tags: /
| Rating:
4 comments.
Write a comment