Heim  >  Artikel  >  Web-Frontend  >  PHP implementiert Daemon – Backend

PHP implementiert Daemon – Backend

coldplay.xixi
coldplay.xixinach vorne
2020-06-16 17:22:511571Durchsuche

PHP implementiert Daemon – Backend

TL;DR

Der PHP-Implementierungsdaemon kann über die Erweiterungen pcntl und posix implementiert werden.

Bei der Programmierung sind Folgendes zu beachten:

  • Übergeben Sie den zweiten pcntl_fork() und posix_setsidum den Hauptprozess aus dem Terminal zu holen
  • Pass pcntl_signal() Ignorieren oder Behandeln von SIGHUP-Signalen
  • Mehrprozessprogramme müssen pcntl_fork() zweimal passieren oder pcntl_signal(), um das SIGCHLD-Signal zu ignorieren und zu verhindern, dass der untergeordnete Prozess zu einem Zombie-Prozess wird
  • über die umask()-Einstellungsmaske für Dateiberechtigungen, um zu verhindern, dass Berechtigungen von geerbten Dateiberechtigungen die Funktion beeinträchtigen
  • Umleitung des STDIN/STDOUT/STDERR des laufenden Prozesses zu /dev/null oder anderen Streams

Wenn Sie noch besser sein möchten, müssen Sie auch auf Folgendes achten:

  • Wenn Sie über Root starten, wechseln Sie beim Ausführen zu einer Benutzeridentität mit niedrigen Privilegien
  • Rechtzeitig chdir()Verhindern Sie falsche Betriebspfade
  • Mehrere Das Prozessprogramm sollte einen regelmäßigen Neustart in Betracht ziehen, um Speicherverluste zu vermeiden

Was ist ein Daemon?

Der Protagonist des Artikels ist ein Daemon. Die Definition auf Wikipedia lautet:

In einem Multitasking-Computerbetriebssystem ist ein Daemon (englisch: daemon, /ˈdiːmən/ oder /ˈdeɪmən/) ein Computerprogramm, das im Hintergrund ausgeführt wird. Solche Programme werden als Prozesse initialisiert. Die Namen von Daemon-Programmen enden normalerweise mit dem Buchstaben „d“: syslogd bezieht sich beispielsweise auf den Daemon, der Systemprotokolle verwaltet.
Normalerweise verfügt der Daemon-Prozess über keinen vorhandenen übergeordneten Prozess (d. h. PPID=1) und befindet sich direkt unter init in der Prozesshierarchie des UNIX-Systems. Ein Daemon-Programm macht sich normalerweise selbst zu einem Daemon, indem es fork auf einem untergeordneten Prozess ausführt und dann seinen übergeordneten Prozess sofort beendet, damit der untergeordnete Prozess unter init ausgeführt werden kann. Diese Methode wird oft als „Beschuss“ bezeichnet.

Erweiterte Programmierung in der UNIX-Umgebung (zweite Ausgabe) (im Folgenden als APUE bezeichnet) In Kapitel 13 gibt es eine Wolke:

Der Daemon-Prozess ist ebenfalls zu einem Elf geworden Prozess (Daemon). Ein Prozess mit einem langen Lebenszyklus. Sie werden häufig beim Systemstart gestartet und erst beim Herunterfahren des Systems beendet. Da sie über kein Steuerungsterminal verfügen, sollen sie im Hintergrund laufen.

Hier wird darauf hingewiesen, dass der Daemon die folgenden Eigenschaften aufweist:

  • Kein Terminal
  • Läuft im Hintergrund
  • Das übergeordnete Element Prozess-PID ist 1

Wenn Sie den laufenden Daemon-Prozess anzeigen möchten, können Sie ihn über ps -ax oder ps -ef anzeigen, wobei -x bedeutet, dass Prozesse ohne steuerndes Terminal aufgelistet werden .

Implementierungsbedenken

Zweiter Fork und Setsid

Fork-Systemaufruf

Der Fork-Systemaufruf wird verwendet, um einen Prozess zu kopieren, der fast identisch mit dem übergeordneten Prozess ist Der Unterschied zwischen dem neu generierten untergeordneten Prozess und dem übergeordneten Prozess besteht darin, dass er eine andere PID und einen anderen Speicherplatz hat. Je nach Codelogikimplementierung können der übergeordnete und der untergeordnete Prozess dieselbe Arbeit ausführen oder unterschiedlich sein . Der untergeordnete Prozess erbt Ressourcen wie Dateideskriptoren vom übergeordneten Prozess.

Die pcntl-Erweiterung in PHP implementiert die pcntl_fork()-Funktion, die zum Forken eines neuen Prozesses in PHP verwendet wird.

setsid-Systemaufruf

setsid-Systemaufruf wird verwendet, um eine neue Sitzung zu erstellen und die Prozessgruppen-ID festzulegen.

Hier gibt es mehrere Konzepte: 会话, 进程组.

Unter Linux generiert die Benutzeranmeldung eine Sitzung. Eine Sitzung enthält eine oder mehrere Prozessgruppen, und eine Prozessgruppe enthält mehrere Prozesse. Jede Prozessgruppe hat einen Sitzungsleiter und seine PID ist die Gruppen-ID der Prozessgruppe. Sobald der Prozessleiter ein Terminal öffnet, wird dieses Terminal als steuerndes Terminal bezeichnet. Sobald im Steuerterminal eine Ausnahme auftritt (Verbindungsunterbrechung, Hardwarefehler usw.), wird ein Signal an den Prozessgruppenleiter gesendet.

Im Hintergrund laufende Programme (z. B. das Ausführen von Anweisungen, die mit & in der Shell enden) werden auch nach dem Schließen des Terminals beendet, da das SIGHUP-Signal, das gesendet wird, wenn die Verbindung zum Steuerterminal getrennt wird, nicht gut verarbeitet wird , und SIGHUPDas Standardverhalten eines Signals für einen Prozess besteht darin, den Prozess zu verlassen.

Nach dem Aufruf des Systemaufrufs setsid erstellt der aktuelle Prozess eine neue Prozessgruppe. Wenn das Terminal im aktuellen Prozess nicht geöffnet ist, gibt es in dieser Prozessgruppe kein Steuerterminal wird nein sein. Es liegt ein Problem damit vor, dass der Prozess durch Schließen des Terminals abgebrochen wird.

Die posix-Erweiterung in PHP implementiert die posix_setsid()-Funktion, die zum Festlegen einer neuen Prozessgruppe in PHP verwendet wird.

Verwaister Prozess

Der übergeordnete Prozess wird vor dem untergeordneten Prozess beendet, und der untergeordnete Prozess wird zu einem verwaisten Prozess.

Der Init-Prozess übernimmt den Waisenprozess, dh die PPID des Waisenprozesses wird 1.

Die Rolle der sekundären Verzweigung

Erstens kann der setsid Systemaufruf nicht vom Prozessgruppenleiter aufgerufen werden und gibt -1 zurück.

Der Beispielcode für den zweiten Fork-Vorgang lautet wie folgt:

$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");
}

Angenommen, wir führen die Anwendung im Terminal aus, der Prozess ist a, und der erste Fork wird ausgeführt Generieren Sie einen untergeordneten Prozess. b. Wenn fork erfolgreich ist, wird der übergeordnete Prozess a beendet. b Da es sich um einen verwaisten Prozess handelt, wird er vom Init-Prozess gehostet.

Zu diesem Zeitpunkt befindet sich Prozess b in Prozessgruppe a und Prozess b ruft posix_setsid auf, um die Generierung einer neuen Prozessgruppe anzufordern. Nach erfolgreichem Aufruf wird die aktuelle Prozessgruppe zu b.

Zu diesem Zeitpunkt hat Prozess b tatsächlich jedes Steuerterminal verlassen, Routine:

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

Nach der Ausführung des Programms:

➜  ~ php56 2fork1.php
➜  ~ ps ax | grep -v grep | grep -E 'process_|PID'
  PID TTY      STAT   TIME COMMAND
28203 ?        Ss     0:00 process_b

Aus den Ergebnissen von ps geht hervor, dass sich der TTY von Prozess_b in geändert hat, was bedeutet, dass kein entsprechendes Steuerterminal vorhanden ist.

Der Code scheint zu diesem Zeitpunkt seine Funktion abgeschlossen zu haben. Prozess_b wurde nach dem Schließen des Terminals nicht beendet, aber warum gibt es einen zweiten Fork-Vorgang?

Eine Antwort auf StackOverflow ist gut geschrieben:

Der zweite Fork(2) dient dazu, sicherzustellen, dass der neue Prozess kein Sitzungsleiter ist, also nicht dazu in der Lage ist um (versehentlich) ein steuerndes Terminal zuzuweisen, da Dämonen niemals ein steuerndes Terminal haben sollten.

Dadurch soll verhindert werden, dass der eigentliche Arbeitsprozess aktiv oder versehentlich erneut eine Verbindung zum steuernden Terminal herstellt Der nach der Verzweigung generierte neue Prozess kann sich nicht für ein zugehöriges Steuerterminal bewerben, da er nicht der Anführer der Prozessgruppe ist.

Zusammenfassend besteht die Funktion von Secondary Fork und Setsid darin, eine neue Prozessgruppe zu generieren und zu verhindern, dass der Arbeitsprozess mit dem Steuerterminal verknüpft wird.

SIGHUP-Signalverarbeitung

Die Standardaktion eines Prozesses, der das SIGHUP-Signal empfängt, besteht darin, den Prozess zu beenden.

Und SIGHUP wird in den folgenden Situationen ausgegeben:

  • Das Steuerterminal ist nicht verbunden, SIGHUP wird an den Prozessgruppenleiter gesendet
  • Die Prozessgruppe Wenn der Anführer beendet wird, wird SIGHUP an den Vordergrundprozess in der Prozessgruppe gesendet
  • SIGHUP wird häufig verwendet, um den Prozess zu benachrichtigen, die Konfigurationsdatei neu zu laden (in APUE erwähnt, wird davon ausgegangen, dass der Daemon dieses Signal wahrscheinlich nicht empfängt, da es verfügt über kein Steuerungsterminal. Wählen Sie daher die Wiederverwendung.

Da sich der eigentliche Arbeitsprozess nicht in der Vordergrundprozessgruppe befindet und der Leiter der Prozessgruppe das Terminal verlassen hat und das Terminal nicht steuert ist natürlich kein Problem, wenn es nicht normal verarbeitet wird. Um jedoch zu verhindern, dass der Prozess aufgrund des versehentlichen Empfangs von SIGHUP abbricht, und um den Konventionen der Daemon-Programmierung zu folgen, sollte dieses Signal trotzdem verarbeitet werden.

Zombie-Prozessverarbeitung

Was ist der Zombie-Prozess?

Einfach ausgedrückt wird der untergeordnete Prozess vor dem übergeordneten Prozess beendet. Der übergeordnete Prozess ruft den Systemaufruf wait nicht auf Verarbeitung, und der Prozess wird zum Zombie-Prozess.

Wenn der untergeordnete Prozess vor dem übergeordneten Prozess beendet wird, sendet er das Signal SIGCHLD an den übergeordneten Prozess. Wenn der übergeordnete Prozess dies nicht verarbeitet, wird auch der untergeordnete Prozess zu einem Zombie-Prozess.

Zombie-Prozesse belegen die Anzahl der Prozesse, die gegabelt werden können. Zu viele Zombie-Prozesse führen dazu, dass keine neuen Prozesse gegabelt werden können.

Darüber hinaus wird im Linux-System ein Prozess, dessen ppid der Init-Prozess ist, vom Init-Prozess recycelt und verwaltet, nachdem er zu einem Zombie geworden ist.

Zombie-Prozessverarbeitung

Ausgehend von den Eigenschaften des Zombie-Prozesses kann dieses Problem für Multiprozess-Daemons auf zwei Arten gelöst werden:

  • Übergeordnete Prozessverarbeitung SIGCHLD Signal
  • Lassen Sie den untergeordneten Prozess von init übernehmen

Sie müssen nicht mehr über den übergeordneten Prozess sagen, der das Signal verarbeitet. Registrieren Sie einfach die Rückruffunktion für die Signalverarbeitung und rufen Sie sie auf die Recyclingmethode.

Damit der untergeordnete Prozess von init übernommen wird, können Sie die Methode des zweimaligen Forkens verwenden, sodass der untergeordnete Prozess a aus dem ersten Fork dann den eigentlichen Arbeitsprozess b abzweigen und a beenden kann Zuerst wird b zum Orphan-Prozess gemacht, damit er vom Init-Prozess gehostet werden kann.

umask

umask wird vom übergeordneten Prozess geerbt und wirkt sich auf die Berechtigungen zum Erstellen von Dateien aus.

Das PHP-Handbuch erwähnt:

umask() setzt PHPs umask auf mask & 0777 und gibt die ursprüngliche umask zurück. Bei Verwendung von PHP als Servermodul wird die umask nach jeder Anfrage wiederhergestellt.

Wenn die umask des übergeordneten Prozesses nicht richtig eingestellt ist, treten beim Ausführen einiger Dateivorgänge unerwartete Effekte auf:

➜  ~ 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

Also, um sicherzustellen Damit Dateien jedes Mal entsprechend den erwarteten Berechtigungen bearbeitet werden können, muss der umask-Wert auf 0 gesetzt werden.

0/1/2 umleiten

Die 0/1/2 beziehen sich hier auf STDIN/STDOUT/STDERR, nämlich die drei Standard-Eingabe-/Ausgabe-/Fehlerströme.

Beispiel

Schauen wir uns zunächst ein Beispiel an:

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

Der obige Code hat fast alle am Anfang des Artikels genannten Aspekte abgeschlossen Der einzige Unterschied besteht darin, dass der Standardstream nicht verarbeitet wird. Das Programm kann auch im Hintergrund über den Befehl php not_redirect_std_stream_daemon.php ausgeführt werden.

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 错误,导致进程退出。

原因很简单,即我们编写的 daemon 程序使用了当时启动时终端提供的标准流,当终端关闭时,标准流变得不可读不可写,一旦尝试读写,会导致进程退出。

在信海龙的博文《一个echo引起的进程崩溃》中也提到过类似的问题。

解决方案

APUE 样例

APUE 13.3中提到过一条编程规则(第6条):

某些守护进程打开 /dev/null 时期具有文件描述符0、1和2,这样,任何一个视图读标准输入、写标准输出或者标准错误的库例程都不会产生任何效果。因为守护进程并不与终端设备相关联,所以不能在终端设备上显示器输出,也无从从交互式用户那里接受输入。及时守护进程是从交互式会话启动的,但因为守护进程是在后台运行的,所以登录会话的终止并不影响守护进程。如果其他用户在同一终端设备上登录,我们也不会在该终端上见到守护进程的输出,用户也不可期望他们在终端上的输入会由守护进程读取。

简单来说:

  • daemon 不应使用标准流
  • 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 中如此实现,结合博文,可能与 PHP 的 GC 机制有关,对于 fd 0 1 2来说,PHP 会维持对这三个资源的引用计数,在直接 fclose 之后,会使得这几个 fd 对应的资源类型的变量引用计数为0,导致触发回收。所需要做的就是将这些变量变为全局变量,保证引用的存在。

推荐教程:《php教程

Das obige ist der detaillierte Inhalt vonPHP implementiert Daemon – Backend. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:juejin.im. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen