TL;DR
PHP-реализация демона может быть выполнена черезpcntl
иposix
Расширенная реализация.
На что следует обратить внимание при программировании:
- через второй
pcntl_fork()
а такжеposix_setsid
вывести основной процесс из терминала - пройти через
pcntl_signal()
игнорировать или обрабатыватьSIGHUP
Сигнал - Многопроцессорные программы должны проходить дважды
pcntl_fork()
илиpcntl_signal()
пренебрегатьSIGCHLD
Сигнал предотвращает превращение дочернего процесса в зомби-процесс - пройти через
umask()
Установите маску разрешения файла, чтобы предотвратить наследование разрешения файла и влияние на функцию. - будет запускать процесс
STDIN/STDOUT/STDERR
перенаправить на/dev/null
или другие потоки
Если вы хотите стать лучше, вам также необходимо обратить внимание на:
- Если запущен как root, запустите как пользователь с низкими привилегиями
- своевременный
chdir()
Предотвратить операцию неправильного пути - Рассмотрите возможность перезапуска по времени для многопроцессорных программ, чтобы предотвратить утечку памяти.
что такое демон
главный герой статьидемон, определение в Википедии:
В многозадачной компьютерной операционной системе демон (англ. daemon, /ˈdiːmən/ или /ˈdeɪmən/) — это компьютерная программа, работающая в фоновом режиме. Такие программы инициализируются как процессы. Имена программ-демонов обычно заканчиваются буквой «d»: например, syslogd относится к демону, который управляет системным журналом.
Как правило, демон не имеет родительского процесса (т. е. PPID=1) и находится непосредственно под init в иерархии системных процессов UNIX. Программа-демон обычно делает себя демоном, запуская ответвление дочернего процесса, а затем вызывая немедленное завершение своего родителя, позволяя дочернему процессу работать под управлением init. Этот метод часто называют «распаковкой».
Расширенное программирование в среде UNIX (второе издание)(далее для краткости именуемый APUE) Глава 13 содержит следующее:
Процесс демона также является процессом мастера (демон) — это процесс с длительным жизненным циклом. Они часто запускаются при начальной загрузке системы и завершаются только при выключении системы. Поскольку они не контролируют терминал, говорят, что они работают в фоновом режиме.
Обратите внимание, что демон имеет следующие характеристики:
- нет терминала
- Фоновый процесс
- Pid родительского процесса равен 1
Чтобы просмотреть запущенный процесс демона, вы можете передатьps -ax
илиps -ef
взгляд, который-x
Указывает, что будут перечислены процессы без управляющего терминала.
Фокус реализации
Квадратичная вилка с setid
форк системного вызова
forkСистемный вызов используется для копирования процесса, который практически идентичен родительскому процессу.Отличие вновь созданного дочернего процесса состоит в том, что у него другой pid и другой объем памяти, чем у родительского процесса.Согласно логике кода, родительский и дочерний процессы могут выполнять одну и ту же работу. , также могут различаться. Дочерний процесс наследует ресурсы, такие как файловые дескрипторы, от родительского процесса.
в PHPpcntl
реализовано в расширенииpcntl_fork()
Функция для создания нового процесса в PHP.
системный вызов setsid
setsidСистемный вызов используется для создания нового сеанса и установки идентификатора группы процессов.
Вот несколько концепций:会话
,进程组
.
В Linux вход пользователя создает сеанс (сеанс), сеанс содержит одну или несколько групп процессов, а группа процессов содержит несколько процессов. В каждой группе процессов есть руководитель сеанса, pid которого является идентификатором группы процессов. Как только лидер процесса открывает терминал, этот терминал называется контролирующим терминалом. Как только управляющий терминал выходит из строя (отключение, аппаратная ошибка и т. д.), лидеру группы процессов отправляется сигнал.
Запуск программ в фоновом режиме (например, в оболочке, начинающейся с&
Команда завершения выполнения) также будет уничтожена после закрытия терминала, то есть выдается при отключении терминала управления.SIGHUP
сигнал, иSIGHUP
Поведение сигнала по умолчанию для процесса — выход из процесса.
перечислитьsetsid
После системного вызова текущий процесс создаст новую группу процессов.Если терминал не открыт в текущем процессе, то в этой группе процессов не будет управляющего терминала, и не будет проблемы убить процесс из-за закрытие терминала.
в PHPposix
реализовано в расширенииposix_setsid()
Функция для установки новой группы процессов в PHP.
потерянный процесс
Если родительский процесс завершается раньше дочернего процесса, дочерний процесс становится осиротевшим процессом.
Процесс init примет процесс-сироту, то есть ppid процесса-сироты станет равным 1.
Роль вторичной вилки
Во-первых,setsid
Системный вызов не может быть вызван лидером группы процессов и вернет -1.
Пример кода для вторичной операции fork выглядит следующим образом:
$pid1 = pcntl_fork();
if ($pid1 > 0) {
exit(0);
} else if ($pid1 < 0) {
exit("Failed to fork 1\n");
}
if (-1 == posix_setsid()) {
exit("Failed to setsid\n");
}
$pid2 = pcntl_fork();
if ($pid2 > 0) {
exit(0);
} else if ($pid2 < 0) {
exit("Failed to fork 2\n");
}
Предположим, мы выполняем приложение в терминале, процесс — это а, первая вилка сгенерирует дочерний процесс b, если вилка пройдет успешно, родительский процесс а выйдет. b — это потерянный процесс, размещенный в процессе init.
В этот момент процесс b находится в группе процессов a, и процесс b вызываетposix_setsid
Требуется сгенерировать новую группу процессов.После успешного вызова текущая группа процессов становится b.
В этот момент процесс b фактически покинул любой управляющий терминал, процедура:
<?php
cli_set_process_title('process_a');
$pidA = pcntl_fork();
if ($pidA > 0) {
exit(0);
} else if ($pidA < 0) {
exit(1);
}
cli_set_process_title('process_b');
if (-1 === posix_setsid()) {
exit(2);
}
while(true) {
sleep(1);
}
После выполнения программы:
➜ ~ php56 2fork1.php
➜ ~ ps ax | grep -v grep | grep -E 'process_|PID'
PID TTY STAT TIME COMMAND
28203 ? Ss 0:00 process_b
В результате ps TTY процесса_b стал?
, то есть нет соответствующего терминала управления.
Тут приходит код, вроде функцию выполнил, а process_b не убивается после закрытия терминала, но зачем второй раз форкнуть?
Один на StackOverflowотвечатьХорошо написан:
Вторая вилка (2) предназначена для того, чтобы гарантировать, что новый процесс не является лидером сеанса, поэтому он не сможет (случайно) выделить управляющий терминал, поскольку у демонов никогда не должно быть управляющего терминала.
Это делается для предотвращения активного или случайного связывания фактического рабочего процесса с терминалом управления.Новый процесс, сгенерированный после разветвления, снова не может применяться для связывания терминала управления, поскольку он не является лидером группы процессов.
Таким образом, роль вторичного форка и setsid заключается в создании новой группы процессов, чтобы рабочий процесс не был связан с терминалом управления.
Обработка сигнала SIGHUP
процесс получаетSIGHUP
Действием по умолчанию для сигнала является завершение процесса.
иSIGHUP
будет выдано, когда:
- Управляющий терминал отключен, SIGHUP отправляется лидеру группы процессов
- Лидер группы процессов выходит, и SIGHUP будет отправлен процессу переднего плана в группе процессов.
- SIGHUP часто используется для уведомления процесса о перезагрузке файла конфигурации (как упоминалось в APUE, считается, что демон не может получить этот сигнал, поскольку у него нет управляющего терминала, поэтому он выбирает повторное использование)
Поскольку фактический рабочий процесс не находится в группе процессов переднего плана, а лидер группы процессов вышел и не имеет управляющего терминала, то, конечно, нет проблем не для того, чтобы иметь дело с нормальной ситуацией, а для того, чтобы предотвратить случайный приемSIGHUP
Вызывает завершение процесса, и, чтобы следовать соглашениям программирования демона, этот сигнал все равно должен обрабатываться.
Обработка зомби-процессов
Что такое зомби-процесс
Проще говоря, дочерний процесс завершается раньше родительского процесса, и родительский процесс не вызываетwait
Системный вызов обрабатывается, и процесс становится зомби-процессом.
Когда дочерний процесс завершается раньше родительского процесса, он отправляет сообщение родительскому процессу.SIGCHLD
Сигнал, если родительский процесс не справится с этим, дочерний процесс также станет процессом-зомби.
Процесс-зомби будет занимать количество процессов, которые можно разветвить, а слишком много процессов-зомби сделает невозможным разветвление новых процессов.
Кроме того, процесс, ppid которого является процессом инициализации в системе Linux, будет перезапущен и будет управляться процессом инициализации после того, как станет зомби.
Обработка зомби-процессов
Судя по характеристикам процесса Zombie, для мультипроцессных демонов эту проблему можно решить двумя способами:
- родительский процесс
SIGCHLD
Сигнал - Пусть дочерний процесс будет передан init
Родительскому процессу не нужно много говорить об обработке сигналов, просто зарегистрируйте функцию обратного вызова обработки сигналов и вызовите метод повторного использования.
Чтобы дочерний процесс был передан init, вы можете дважды разветвить, позволить дочернему процессу a из первого разветвления, а затем разветвить фактический рабочий процесс b, разрешить выход сначала и сделать b осиротевшим процессом, чтобы вы могли Управляется процессом инициализации.
umask
Маска umask наследуется от родительского процесса и влияет на разрешения на создание файлов.
PHP руководствоУпомянутый выше:
umask() устанавливает umask PHP равным mask & 0777 и возвращает исходную umask. При использовании PHP в качестве серверного модуля umask восстанавливается после каждого запроса.
Если umask родительского процесса установлен неправильно, при выполнении некоторых файловых операций будут возникать неожиданные эффекты:
➜ ~ cat test_umask.php
<?php
chdir('/tmp');
umask(0066);
mkdir('test_umask', 0777);
➜ ~ php test_umask.php
➜ ~ ll /tmp | grep umask
drwx--x--x 2 root root 4.0K 8月 22 17:35 test_umask
Следовательно, чтобы каждый раз гарантировать, что с файлом можно будет работать в соответствии с ожидаемыми разрешениями, необходимо установить значение umask, равное 0.
Перенаправление 0/1/2
0/1/2 здесь относятся кSTDIN/STDOUT/STDERR
, то есть стандартный ввод/вывод/ошибка трех потоков.
Образец
Сначала посмотрите на пример:
<?php
// not_redirect_std_stream_daemon.php
$pid1 = pcntl_fork();
if ($pid1 > 0) {
exit(0);
} else if ($pid1 < 0) {
exit("Failed to fork 1\n");
}
if (-1 == posix_setsid()) {
exit("Failed to setsid\n");
}
$pid2 = pcntl_fork();
if ($pid2 > 0) {
exit(0);
} else if ($pid2 < 0) {
exit("Failed to fork 2\n");
}
umask(0);
declare(ticks = 1);
pcntl_signal(SIGHUP, SIG_IGN);
echo getmypid() . "\n";
while(true) {
echo time() . "\n";
sleep(10);
}
Вышеприведенный код делает почти все, о чем говорилось в начале статьи, с той лишь разницей, что стандартный поток не обрабатывается. пройти черезphp not_redirect_std_stream_daemon.php
Директивы также позволяют программам работать в фоновом режиме.
существуетsleep
В промежутке закройте терминал, и вы обнаружите, что процесс завершается.
пройти черезstrace
Обратите внимание на ситуацию с системным вызовом:
➜ ~ strace -p 6723
Process 6723 attached - interrupt to quit
restart_syscall(<... resuming interrupted call ...>) = 0
write(1, "1503417004\n", 11) = 11
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
nanosleep({10, 0}, 0x7fff71a30ec0) = 0
write(1, "1503417014\n", 11) = -1 EIO (Input/output error)
close(2) = 0
close(1) = 0
munmap(0x7f35abf59000, 4096) = 0
close(0) = 0
Обнаружена ошибка EIO, из-за которой процесс завершается.
Причина очень проста, то есть программа-демон, которую мы написали, использует стандартный поток, предоставляемый терминалом во время запуска.Когда терминал закрыт, стандартный поток становится нечитаемым и недоступным для записи.Как только мы пытаемся читать и писать, процесс завершится.
существуетСиньхайлунСообщение блога"Сбой процесса, вызванный эхом"Аналогичная проблема упоминалась и в .
решение
Образец АПУЭ
В APUE 13.3 (пункт 6) упоминается правило программирования:
некоторые демоны открыты
/dev/null
Эпоха имеет файловые дескрипторы 0, 1 и 2, так что любая библиотечная процедура, которая смотрит на чтение стандартного ввода, запись стандартного вывода или стандартную ошибку, не будет иметь никакого эффекта. Поскольку демон не связан с терминальным устройством, он не может отображать выходные данные на терминальном устройстве и не может принимать ввод от интерактивного пользователя. Демон JIT запускается из интерактивного сеанса, но, поскольку демон работает в фоновом режиме, завершение сеанса входа в систему не влияет на демон. Если другие пользователи вошли в систему на том же терминальном устройстве, мы не увидим вывод демона на этом терминале, и пользователи не могут ожидать, что их ввод на терминале будет прочитан демоном.
Проще говоря:
- демоны не должны использовать стандартные потоки
- 0/1/2 следует установить в /dev/null
Обычное использование:
for (i = 0; i < rl.rlim_max; i++)
close(i);
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);
реализовал эту функцию.dup()
(Ссылаться наруководство) системный вызов копирует дескриптор файла во входном параметре и копирует его в наименьший нераспределенный дескриптор файла. Таким образом, приведенную выше процедуру можно понимать как:
关闭所有可以打开的文件描述符,包括标准输入输出错误;
打开/dev/null并赋值给变量fd0,因为标准输入已经关闭了,所以/dev/null会绑定到0,即标准输入;
因为最小未分配文件描述符为1,复制文件描述符0到文件描述符1,即标准输出也绑定到/dev/null;
因为最小未分配文件描述符为2,复制文件描述符0到文件描述符2,即标准错误也绑定到/dev/null;
Реализация проекта с открытым исходным кодом: Workerman
WorkermanсерединаWorker.phpсерединаresetStd()
метод реализует аналогичную операцию.
/**
* Redirect standard input and output.
*
* @throws Exception
*/
public static function resetStd()
{
if (!self::$daemonize) {
return;
}
global $STDOUT, $STDERR;
$handle = fopen(self::$stdoutFile, "a");
if ($handle) {
unset($handle);
@fclose(STDOUT);
@fclose(STDERR);
$STDOUT = fopen(self::$stdoutFile, "a");
$STDERR = fopen(self::$stdoutFile, "a");
} else {
throw new Exception('can not open stdoutFile ' . self::$stdoutFile);
}
}
Это реализовано в Workerman в сочетании сСообщение блога, что может быть связано с механизмом GC PHP. Для fd 0 1 2 PHP будет поддерживать счетчик ссылок для этих трех ресурсов. После прямого fclose счетчик ссылок переменной типа ресурса, соответствующего этим fd, будет равен 0. , что приводит к запуску рециркуляции. Все, что нужно сделать, это сделать эти переменные глобальными, гарантируя существование ссылки.