Maison  >  Article  >  interface Web  >  PHP implémente un démon - backend

PHP implémente un démon - backend

coldplay.xixi
coldplay.xixiavant
2020-06-16 17:22:511571parcourir

PHP implémente un démon - backend

TL;DR

Le démon d'implémentation PHP peut être implémenté via les extensions pcntl et posix.

Les choses à noter dans la programmation sont :

  • Passez le deuxième pcntl_fork() et posix_setsid pour sortir le processus principal du terminal
  • Passez pcntl_signal() Ignorer ou gérer les signaux SIGHUP
  • Les programmes multi-processus doivent passer pcntl_fork() deux fois ou pcntl_signal() pour ignorer le signal SIGCHLD afin d'empêcher le processus enfant de devenir un processus zombie
  • via le masque d'autorisation du fichier de paramètres umask() pour empêcher les autorisations des autorisations de fichier héritées d'affecter la fonction
  • Rediriger le STDIN/STDOUT/STDERR du processus en cours d'exécution vers /dev/null ou d'autres flux

si vous voulez encore mieux, vous devez également faire attention à :

  • Si vous démarrez via root, passez à une identité d'utilisateur peu privilégié lors de l'exécution de
  • Timely chdir() Prévenir les mauvais chemins d'opération
  • Multiple Le programme de processus doit envisager de redémarrer régulièrement pour éviter les fuites de mémoire

Qu'est-ce qu'un démon

Le protagoniste de l'article est un démon. La définition sur Wikipédia est :

Dans un système d'exploitation informatique multitâche, un démon (anglais : démon, /ˈdiːmən/ ou /ˈdeɪmən/) est un programme informatique qui s'exécute en arrière-plan. De tels programmes seront initialisés en tant que processus. Les noms des programmes démons se terminent généralement par la lettre « d » : par exemple, syslogd fait référence au démon qui gère les journaux système.
Habituellement, le processus démon n'a aucun processus parent existant (c'est-à-dire PPID=1) et se trouve directement sous init dans la hiérarchie des processus du système UNIX. Un programme démon se transforme généralement en démon en exécutant fork sur un processus enfant, puis en mettant immédiatement fin à son processus parent afin que le processus enfant puisse s'exécuter sous init. Cette méthode est souvent appelée « bombardement ».

Programmation avancée dans un environnement UNIX (deuxième édition) (ci-après dénommé APUE) Il y a un cloud dans le chapitre 13 :

Les processus démons sont également appelés processus démons. Un processus avec un long cycle de vie. Ils démarrent souvent au démarrage du système et ne se terminent que lorsque le système est arrêté. Parce qu'ils n'ont pas de terminal de contrôle, on dit qu'ils fonctionnent en arrière-plan.

On note ici que le démon présente les caractéristiques suivantes :

  • Aucun terminal
  • Fonctionne en arrière-plan
  • Le parent process pid est 1

Si vous souhaitez afficher le processus démon en cours d'exécution, vous pouvez le visualiser via ps -ax ou ps -ef, où -x signifie que les processus sans terminal de contrôle seront répertoriés .

Problèmes d'implémentation

Deuxième fork et setsid

Appel système fork

L'appel système fork est utilisé pour copier un processus qui est presque identique au parent process , la différence entre le processus enfant nouvellement généré et le processus parent est qu'il a un pid différent et un espace mémoire différent. Selon l'implémentation de la logique du code, les processus parent et enfant peuvent effectuer le même travail, ou ils peuvent être différents. . Le processus enfant hérite des ressources telles que les descripteurs de fichiers du processus parent.

L'extension pcntl en PHP implémente la fonction pcntl_fork(), qui est utilisée pour créer un nouveau processus en PHP.

Appel système setsid

L'appel système setsid est utilisé pour créer une nouvelle session et définir l'identifiant du groupe de processus.

Il y a plusieurs concepts ici : 会话, 进程组.

Sous Linux, la connexion utilisateur génère une session. Une session contient un ou plusieurs groupes de processus, et un groupe de processus contient plusieurs processus. Chaque groupe de processus a un chef de session et son pid est l'identifiant du groupe de processus. Une fois que le responsable du processus ouvre un terminal, ce terminal est appelé terminal de contrôle. Dès qu'une exception se produit dans le terminal de contrôle (déconnexion, erreur matérielle, etc.), un signal sera envoyé au chef du groupe de processus.

Les programmes exécutés en arrière-plan (tels que l'exécution d'instructions se terminant par & dans le shell) seront également supprimés après la fermeture du terminal, car le signal SIGHUP envoyé lorsque le terminal de contrôle est déconnecté n'est pas bien géré. , et SIGHUPLe comportement par défaut d'un signal pour un processus est de quitter le processus.

Après avoir appelé l'appel système setsid, le processus actuel créera un nouveau groupe de processus. Si le terminal n'est pas ouvert dans le processus actuel, alors il n'y aura pas de terminal de contrôle dans ce groupe de processus, et il n'y aura pas de terminal de contrôle dans ce groupe de processus. sera non. Il y a un problème avec le processus qui est arrêté lors de la fermeture du terminal.

L'extension posix en PHP implémente la fonction posix_setsid(), qui est utilisée pour définir un nouveau groupe de processus en PHP.

Processus orphelin

Le processus parent se termine avant le processus enfant, et le processus enfant deviendra un processus orphelin.

Le processus d'initialisation adoptera le processus orphelin, c'est-à-dire que le ppid du processus orphelin devient 1.

Le rôle du fork secondaire

Tout d'abord, l'appel système setsid ne peut pas être appelé par le chef du groupe de processus et renverra -1.

L'exemple de code pour la deuxième opération fork est le suivant :

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

Supposons que nous exécutons l'application dans le terminal, que le processus est a et que le premier fork sera générer un processus enfant b. Si fork réussit, le processus parent a se termine. b En tant que processus orphelin, il est hébergé par le processus init.

À ce stade, le processus b est dans le groupe de processus a et le processus b appelle posix_setsid pour demander la génération d'un nouveau groupe de processus. Une fois l'appel réussi, le groupe de processus actuel devient b.

A ce moment, le processus b a effectivement quitté n'importe quel terminal de contrôle, 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);
}

Après l'exécution du programme :

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

D'après les résultats de ps, le TTY de process_b est devenu , c'est-à-dire qu'il n'y a pas de terminal de contrôle correspondant.

Le code semble avoir terminé sa fonction à ce stade. process_b n'a pas été tué après la fermeture du terminal, mais pourquoi y a-t-il une deuxième opération fork ?

Une réponse sur StackOverflow est bien écrite :

Le deuxième fork(2) est là pour garantir que le nouveau processus n'est pas un leader de session, il ne pourra donc pas pour allouer (accidentellement) un terminal de contrôle, puisque les démons ne sont pas censés avoir un terminal de contrôle.

Cela permet d'empêcher les processus de travail réels de s'associer activement ou accidentellement avec le terminal de contrôle, encore une fois. le nouveau processus généré après fork ne peut pas postuler pour un terminal de contrôle associé car il n'est pas le leader du groupe de processus.

Pour résumer, la fonction du fork secondaire et du setsid est de générer un nouveau groupe de processus et d'empêcher le processus de travail d'être associé au terminal de contrôle.

Gestion du signal SIGHUP

L'action par défaut d'un processus recevant le signal SIGHUP est de terminer le processus.

Et SIGHUP sera émis dans les situations suivantes :

  • Le terminal de contrôle est déconnecté, SIGHUP est envoyé au chef du groupe processus
  • Le groupe processus leader quitte, SIGHUP sera envoyé au processus de premier plan dans le groupe de processus
  • SIGHUP est souvent utilisé pour notifier au processus de recharger le fichier de configuration (mentionné dans APUE, le démon est considéré comme peu susceptible de recevoir ce signal car il n'a pas de terminal de contrôle. Alors choisissez la réutilisation)

Étant donné que le processus de travail réel n'est pas dans le groupe de processus de premier plan et que le chef du groupe de processus a quitté et ne contrôle pas le terminal, il n'est bien sûr pas de problème s'il n'est pas traité normalement. Cependant, afin d'éviter que le processus ne se termine en raison de la réception accidentelle de SIGHUP, et pour suivre les conventions de programmation du démon, ce signal doit quand même être traité.

Traitement du processus zombie

Qu'est-ce que le processus zombie

En termes simples, le processus enfant se termine avant le processus parent, le processus parent n'appelle pas l'wait appel système traitement, et le processus devient pour le processus Zombie.

Lorsque le processus enfant se termine avant le processus parent, il enverra le signal SIGCHLD au processus parent. Si le processus parent ne le gère pas, le processus enfant deviendra également un processus zombie.

Les processus zombies occuperont le nombre de processus pouvant être dupliqués. Trop de processus zombies entraîneront l'incapacité de créer de nouveaux processus.

De plus, dans le système Linux, un processus dont le ppid est le processus d'initialisation sera recyclé et géré par le processus d'initialisation après qu'il soit devenu un Zombie.

Traitement du processus Zombie

D'après les caractéristiques du processus Zombie, pour les démons multi-processus, ce problème peut être résolu de deux manières :

  • Traitement du processus parent SIGCHLD Signal
  • Laisser le processus enfant être pris en charge par init

Pas besoin d'en dire plus sur le processus parent traitant le signal, enregistrez simplement la fonction de rappel de traitement du signal et appelez la méthode de recyclage.

Pour que le processus enfant soit repris par init, vous pouvez utiliser la méthode de fork deux fois, afin que le processus enfant a du premier fork puisse ensuite débourser le processus de travail réel b et laisser une sortie tout d'abord, faire en sorte que b devienne un processus orphelin afin qu'il puisse être hébergé par le processus d'initialisation.

umask

umask sera hérité du processus parent, affectant les autorisations de création de fichiers.

Le manuel PHP mentionne :

umask() définit l'umask de PHP sur le masque & 0777 et renvoie l'umask d'origine. Lorsque PHP est utilisé comme module serveur, l'umask est restauré après chaque requête.

Si l'umask du processus parent n'est pas défini correctement, des effets inattendus se produiront lors de l'exécution de certaines opérations sur les fichiers :

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

Donc, afin de garantir pour que les fichiers puissent être exploités selon les autorisations attendues à chaque fois, la valeur umask doit être définie sur 0.

Redirection 0/1/2

Le 0/1/2 fait référence ici à STDIN/STDOUT/STDERR, à savoir les trois flux d'entrée/sortie/erreur standard.

Exemple

Regardons d'abord un exemple :

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

Le code ci-dessus a presque complété tous les aspects mentionnés au début de l'article, le seulement La différence est que le flux standard n'est pas traité. Le programme peut également être exécuté en arrière-plan via la commande 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 错误,导致进程退出。

原因很简单,即我们编写的 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教程

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer