Асинхронная оболочка exec в PHP

199

У меня есть сценарий PHP, который должен вызывать сценарий оболочки, но не заботится о выводе. Сценарий оболочки выполняет несколько вызовов SOAP и выполняется медленно, поэтому я не хочу замедлять запрос PHP, пока он ожидает ответа. Фактически, PHP-запрос должен иметь возможность завершиться без завершения процесса оболочки.

Я посмотрел в различные exec(), shell_exec(), pcntl_fork()функции и т.д., но ни один из них не кажется, предлагают именно то , что я хочу. (Или, если они это сделают, мне не понятно, как.) Есть предложения?

AdamTheHutt
источник
Независимо от того, какое решение вы выберете, вы также должны рассмотреть возможность использования скрипта оболочки niceи ioniceпредотвратить его перегрузку вашей системы (например /usr/bin/ionice -c3 /usr/bin/nice -n19)
rinogo
Возможный дубликат php выполняет фоновый процесс
Шон Бин

Ответы:

218

Если он "не заботится о выводе", не может ли быть вызван exec для сценария с помощью &background для процесса?

РЕДАКТИРОВАТЬ - включив то, что @ AdamTheHut прокомментировал к этому сообщению, вы можете добавить это к звонку exec:

" > /dev/null 2>/dev/null &"

Это перенаправит и stdio(first >), и stderr( 2>) /dev/nullв фоновый режим.

Есть другие способы сделать то же самое, но это самый простой для чтения.


Альтернатива вышеупомянутому двойному перенаправлению:

" &> /dev/null &"
кроличий садок
источник
11
Кажется, это работает, но ему нужно немного больше, чем амперсанд. Я получил его, добавив "> / dev / null 2> / dev / null &" к вызову exec (). Хотя я должен признать, я не совсем уверен, что это делает.
AdamTheHutt
2
Определенно путь, если вы хотите выстрелить и забыть с php и apache. Во многих производственных средах Apache и PHP функция pcntl_fork () отключена.
метод
9
Замечание &> /dev/null &: xdebug не будет генерировать логи, если вы используете это. Проверьте stackoverflow.com/questions/4883171/…
kapeels
5
@MichaelJMulligan это закрывает дескрипторы файлов. Тем не менее, несмотря на повышение эффективности, задним числом, использование /dev/nullявляется лучшей практикой, поскольку запись в закрытые FD вызывает ошибки, тогда как попытки чтения или записи /dev/nullпросто молча ничего не делают.
Чарльз Даффи
3
Фоновые скрипты будут убиты при перезапуске apache ... просто знайте об этом для очень долгих заданий или если вам не повезло в сроках и вы удивляетесь, почему ваша работа исчезла
Julien
53

Я использовал в для этого, поскольку это действительно начинает независимый процесс.

<?php
    `echo "the command"|at now`;
?>
Czimi
источник
4
в некоторых ситуациях это абсолютно лучшее решение. он был единственным, который работал для меня , чтобы выпустить «SUDO перезагрузки» ( «эхо„сон 3; SUDO перезагрузку“| в настоящее время») из WebGUI и закончить обработку страницы .. на OpenBSD
Кай
1
если пользователь, которого вы запускаете apache (обычно www-data), не имеет разрешений на использование, atи вы не можете его настроить, вы можете попробовать использовать <?php exec('sudo sh -c "echo \"command\" | at now" ');If, commandсодержащий кавычки, см. escapeshellarg, чтобы избавить вас от головной боли
Julien
1
Если подумать, заставить sudo запустить sh - не самая лучшая идея, так как она в основном дает sudo root-доступ ко всему. Я вернулся к использованию echo "sudo command" | at nowи комментированию www-dataв/etc/at.deny
Julien
@Julien, может быть, вы могли бы создать скрипт оболочки с командой sudo и запустить его вместо этого. до тех пор, пока вы не передаете значения, переданные пользователем, в указанный скрипт
Garet Claborn
26

Для всех пользователей Windows: я нашел хороший способ запустить асинхронный PHP-скрипт (на самом деле он работает практически со всем).

Он основан на командах popen () и pclose (). И хорошо работает как на Windows, так и на Unix.

function execInBackground($cmd) {
    if (substr(php_uname(), 0, 7) == "Windows"){
        pclose(popen("start /B ". $cmd, "r")); 
    }
    else {
        exec($cmd . " > /dev/null &");  
    }
} 

Оригинальный код от: http://php.net/manual/en/function.exec.php#86329

LucaM
источник
это только исполняет файл, но не работает при использовании php / python / node и т. д.
Дэвид Валентино
@davidvalentino Правильно, и это нормально! Если вы хотите выполнить скрипт PHP / Pyhton / NodeJS, вам необходимо вызвать исполняемый файл и передать ему ваш скрипт. Например: вы не кладете в свой терминал, myscript.jsа вместо этого, вы будете писать node myscript.js. То есть: узел - это исполняемый файл, myscript.js - это, ну, в общем , скрипт для выполнения. Существует огромная разница между исполняемым файлом и скриптом.
LucaM
Правильно, в этом случае нет проблем, в других случаях, например, необходимо запустить laravel artisan, php artisanпросто оставьте здесь комментарий, чтобы не было необходимости отслеживать, почему он не будет работать с командами
Дэвид Валентино
22

На Linux вы можете сделать следующее:

$cmd = 'nohup nice -n 10 php -f php/file.php > log/file.log & printf "%u" $!';
$pid = shell_exec($cmd);

Это выполнит команду в командной строке, а затем просто вернет PID, который можно проверить на> 0, чтобы убедиться, что он работает.

Этот вопрос похож: есть ли у PHP многопоточность?

Дэррил Хейн
источник
Этот ответ будет легче читать, если вы включите только основные элементы (исключая action=generate var1_id=23 var2_id=35 gen_id=535сегмент). Кроме того, поскольку OP спросил о запуске сценария оболочки, вам не нужны специфичные для PHP части. Окончательный код будет:$cmd = 'nohup nice -n 10 /path/to/script.sh > /path/to/log/file.log & printf "%u" $!';
17
Кроме того, как заметка того, кто «был там раньше», любой, кто читает это, может подумать об использовании не только, niceно и ionice.
Риного
1
Что значит "% u" $! делать точно?
Веточки
@Twigs & запускает предыдущий код в фоновом режиме, затем printfиспользуется для форматированного вывода $!переменной, содержащей PID
NoChecksum
1
Огромное спасибо, я попробовал все виды решений, которые я нашел для вывода в журнал с помощью асинхронного вызова PHP-оболочки, и ваше единственное решение, которое удовлетворяет всем критериям.
Джош Паулисон
6

В Linux вы можете запустить процесс в новом независимом потоке, добавив амперсанд в конце команды

mycommand -someparam somevalue &

В Windows вы можете использовать команду «start» DOS

start mycommand -someparam somevalue
Лео
источник
2
В Linux родительский процесс все еще может блокироваться до тех пор, пока дочерний процесс не завершит работу, если он попытается прочитать из дескриптора открытого файла, хранящегося в подпроцессе (т. Е. Stdout), так что это не полное решение.
Чарльз Даффи
1
Протестированная startкоманда на windows, она не запускается асинхронно ... Не могли бы вы указать источник, откуда вы получили эту информацию?
Alph.Dev
@ Alph.Dev, пожалуйста, посмотрите мой ответ, если вы используете Windows: stackoverflow.com/a/40243588/1412157
LucaM
@mynameis Ваш ответ показывает, почему команда запуска НЕ ​​работает. Это из-за /Bпараметра. Я объяснил это здесь: stackoverflow.com/a/34612967/1709903
Alph.Dev
6

правильный способ (!) сделать это

  1. вилка ()
  2. setsid ()
  3. execve ()

fork-вилки, setsid сообщают текущему процессу, чтобы он стал главным (без родителя), execve говорят, что вызывающий процесс должен быть заменен вызываемым. так что родитель может выйти, не влияя на ребенка.

 $pid=pcntl_fork();
 if($pid==0)
 {
   posix_setsid();
   pcntl_exec($cmd,$args,$_ENV);
   // child becomes the standalone detached process
 }

 // parent's stuff
 exit();

источник
6
Проблема с pcntl_fork () заключается в том, что вы не должны использовать его при работе под веб-сервером, как это делает OP (кроме того, OP уже пробовал это).
Гусс
6

Я использовал это ...

/** 
 * Asynchronously execute/include a PHP file. Does not record the output of the file anywhere.  
 * Relies on the PHP_PATH config constant.
 *
 * @param string $filename  file to execute
 * @param string $options   (optional) arguments to pass to file via the command line
 */ 
function asyncInclude($filename, $options = '') {
    exec(PHP_PATH . " -f {$filename} {$options} >> /dev/null &");
}

(где PHP_PATHconst определяется как define('PHP_PATH', '/opt/bin/php5')или подобное)

Он передает аргументы через командную строку. Чтобы прочитать их в PHP, смотрите argv .

philfreo
источник
4

Единственный способ, который я нашел, который действительно работал для меня, был:

shell_exec('./myscript.php | at now & disown')
Гордон Форсайт
источник
3
'disown' является встроенным в Bash и не работает с shell_exec () таким образом. Я попробовал, shell_exec("/usr/local/sbin/command.sh 2>&1 >/dev/null | at now & disown")и все, что я получаю, это:sh: 1: disown: not found
Томас Daugaard
4

Я также нашел Symfony Process Component полезным для этого.

use Symfony\Component\Process\Process;

$process = new Process('ls -lsa');
// ... run process in background
$process->start();

// ... do other things

// ... if you need to wait
$process->wait();

// ... do things after the process has finished

Посмотрите, как это работает в репозитории GitHub .

Антон Пелых
источник
2
Предупреждение: если вы не дождетесь, процесс будет
остановлен,
Идеальный инструмент, основанный на proc_*внутренних функциях.
MAChitgarha
2

Вы также можете запустить скрипт PHP как демон или cronjob :#!/usr/bin/php -q

Рональд Конко
источник
1

Используйте названный fifo.

#!/bin/sh
mkfifo trigger
while true; do
    read < trigger
    long_running_task
done

Затем, когда вы захотите запустить долгосрочную задачу, просто напишите новую строку (неблокирующую в файле триггера).

Пока ваш ввод меньше, чем PIPE_BUFодна write()операция, вы можете записывать аргументы в fifo и отображать их как $REPLYв скрипте.

geocar
источник
1

без использования очереди вы можете использовать proc_open()как это:

    $descriptorspec = array(
        0 => array("pipe", "r"),
        1 => array("pipe", "w"),
        2 => array("pipe", "w")    //here curaengine log all the info into stderror
    );
    $command = 'ping stackoverflow.com';
    $process = proc_open($command, $descriptorspec, $pipes);
Крис Руф
источник