X

PHP, set_time_limit, exec

Всем привет! Давненько я не писал про интересные штуки при программировании на php. Сегодня я опишу один хак, который я нашел столкнувшись с проблемой запуска программ из php..

Итак, у нас есть задача: надо запускать некий внешний софт из php, после чего нужно обработать результат работы внешнего приложения. Делать это можно несколькими способами, например с помощью функции exec.

Например, так:

exec('ps');

Получить результат работы в php можно так:

$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 с таким содержимым и запускаем его так из консоли:

php test.php

Результатом работы будет следующий вывод:

# 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 может выглядеть так:

<?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;

Вот какой будет результат:

# 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

Это отличный вариант, однако при вызове внешних команд, время не учитывается, модернизируем скрипт так и проверим:

<?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');

Результат:

# 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 скрипт.

Т.к. в моем случае, я предусмотрел ограничение запуска только указанного кол-ва процессов моего скрипта, то я отделался лишь несколькими зависшими экземплярами, после чего нужная работа попросту прекратилась. Если бы такого ограничения не было, память сервера довольно быстро бы заполнилась и получились бы проблемы.

Подобного поведения можно добиться например таким скриптом:

<?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 вот так:

<?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 такой:

#timeout -sСИГНАЛ -kТАЙМАУТ_2 ТАЙМАУТ_1 КОМАНДА
  • СИГНАЛ = сигнал который будет послан после времени указанного в ТАЙМАУТ_1. Обычно это SIGHUP или SIGTERM.
  • ТАЙМАУТ_1 = время после которого приложению будет послан сигнал SIGTERM, если не указан другой в пераметре -s. Время можно указывать с префикасми (s=секунда, m=минута, h=часы, d=дни), например: 20m = 20 минут
  • ТАЙМАУТ_2 = время после которого приложению будет послан сигнал SIGKILL, т.е. оно будет завершено. Начинает отсчитываться, после наступления ТАЙМАУТ_1. Т.е. время данное на завершение.
  • Команда возвращает код ошибки 124 в случае таймаута, либо код возвращения запущенной программы в случае если таймаута не было.

Результат:

# php test.php
Start: 14:06:55
---
RESULT:

OUT:
Array
(
    [0] =>
)

ERR:
124
---
Finish: 14:07:05

Как видим через 10 секунд наша программа завершилась по таймауту, что нам и требовалось.

Есть еще один вариант, который я написал, до того, как нашел информацию о существовании команды timeout, вот он:

# watch ps & PID=$! bash -c 'sleep 5; kill -TERM $PID; sleep 1; kill $PID'

В принципе, он делает тоже самое, что и команда выше, только с более сложной записью.

Здесь мы делаем следующее:

  • Запускаем команду в фоне
  • Параллельно запоминаем PID запущенной команды в переменную
  • Далее ждем 5 секунд и отправляем приложению SIGTERM
  • После чего ждем еще 1 секунду и отправляем приложению SIGKILL

Данный способ пригодится тем, у кого не будет утилиты timeout. Собственно именно таким образом я и решил возникшую у меня проблему, теперь в случае зависание внешних утилит они автоматически убиваются.

Если Вы знаете другие варианты решения такой проблемы, напишите в комментариях.

Категории: Linux PHP
Тэги: cliexectimeout

Комментарии (4)

  • Спасибо большое за #timeout -sСИГНАЛ -kТАЙМАУТ_2 ТАЙМАУТ_1 КОМАНДА!
    Очень долго искал, как и чем прерывать выполнение скрипта, зависающего по причине другого скрипта.

  • Я понимаю, что от php никуда не деться, но после увиденного, мне еще больше захотелось выучить flash и action script.

    • А смысл? Flash-же на клиенте работает, а то, что описано в статье, на стороне сервера. Т.е. клиент это никак не увидит. Кроме того Adobe Flash умирает постепенно, лучше смотри в сторону HTML5.