Concepts de base
Nous savons que sous Unix/Linux, dans des circonstances normales, le processus enfant est créé via le processus parent, et le processus enfant crée un nouveau processus. La fin du processus enfant et l'exécution du processus parent sont un processus asynchrone, c'est-à-dire que le processus parent ne peut jamais prédire quand le processus enfant se terminera. Lorsqu'un processus termine son travail et se termine, son processus parent doit appeler l'appel système wait() ou waitpid() pour obtenir l'état de fin du processus enfant.
Processus orphelin
Un processus parent se termine alors qu'un ou plusieurs de ses processus enfants sont toujours en cours d'exécution, ces processus enfants deviendront alors des processus orphelins. Le processus orphelin sera adopté par le processus d'initialisation (le numéro de processus est 1) et le processus d'initialisation terminera le travail de collecte de statut pour eux.
Processus zombie
Un processus utilise fork pour créer un processus enfant si le processus enfant se termine et que le processus parent n'appelle pas wait ou waitpid pour obtenir les informations d'état. du processus enfant, alors le descripteur de processus du processus enfant est toujours enregistré dans le système. Ce processus est appelé processus zombie.
Problèmes et dangers
Unix fournit un mécanisme pour garantir que tant que le processus parent souhaite connaître les informations d'état à la fin du processus enfant, il peut les obtenir . Ce mécanisme est le suivant : lorsque chaque processus se termine, le noyau libère toutes les ressources du processus, y compris les fichiers ouverts, la mémoire occupée, etc. Cependant, certaines informations sont toujours conservées pour celui-ci (notamment l'ID du processus, l'état de fin du processus, la quantité de temps CPU pris par le processus, etc.). Il n'est pas libéré tant que le processus parent ne l'a pas récupéré via wait/waitpid. Mais cela entraîne des problèmes. Si le processus n'appelle pas wait/waitpid, les informations conservées ne seront pas libérées et leur numéro de processus sera toujours occupé. Cependant, le nombre de numéros de processus que le système peut utiliser est limité. un grand nombre de Si un processus zombie est généré, le système ne pourra pas générer de nouveaux processus car il n'y a pas de numéro de processus disponible. C'est le mal des processus zombies et doit être évité.
Un processus orphelin est un processus sans processus parent. La responsabilité importante du processus orphelin incombe au processus init. Le processus init est comme un bureau des affaires civiles, responsable de gérer les conséquences du processus orphelin. Chaque fois qu'un processus orphelin apparaît, le noyau définit le processus parent du processus orphelin sur init, et le processus init attendra cycliquement() ses processus enfants quittés. De cette manière, lorsqu'un processus orphelin termine misérablement son cycle de vie, le processus d'initialisation gérera toutes ses conséquences au nom du parti et du gouvernement. Il n’y a donc aucun mal à rendre les processus orphelins.
Tout processus enfant (sauf init) ne disparaît pas immédiatement après exit(), mais laisse une structure de données appelée processus zombie (Zombie), en attente du processus parent. C’est l’étape par laquelle passe chaque processus enfant à la fin. Si le processus enfant n'a pas le temps de traiter après exit(), vous pouvez alors utiliser la commande ps pour voir que l'état du processus enfant est "Z". Si le processus parent peut le gérer à temps, il est peut-être trop tard pour voir l'état zombie du processus enfant à l'aide de la commande ps, mais cela ne signifie pas que le processus enfant ne passera pas par l'état zombie. Si le processus parent se termine avant la fin du processus enfant, le processus enfant sera repris par init. init traitera le processus enfant à l'état zombie en tant que processus parent.
Scénarios de danger de processus zombie
Par exemple, il existe un processus qui génère régulièrement un processus enfant. Ce processus enfant doit faire très peu de choses après le processus enfant. quitte, il se termine, donc le cycle de vie de ce processus enfant est très court. Cependant, le processus parent ne génère que de nouveaux processus enfants et ne se soucie pas de ce qui se passe après la sortie du processus enfant. De cette façon, le système s'exécute pendant un certain temps. Après cela, il y aura de nombreux processus zombies dans le système. Si vous utilisez la commande ps pour vérifier, vous verrez de nombreux processus avec le statut Z. À proprement parler, le processus zombie n'est pas la source du problème. Le coupable est le processus parent qui a engendré un grand nombre de processus zombie. Par conséquent, lorsque nous cherchons comment éliminer un grand nombre de processus zombies dans le système, la réponse est de tirer sur le coupable qui a généré un grand nombre de processus zombies (c'est-à-dire en envoyant un signal SIGTERM ou SIGKILL via kill). Une fois le processus coupable abattu, les processus zombies qu'il génère deviennent des processus orphelins. Ces processus orphelins seront repris par le processus init qui attendra () ces processus orphelins et libérera les ressources qu'ils occupent dans la table des processus système. De cette façon, ces processus orphelins morts peuvent disparaître en paix.
Test de processus orphelin et de processus zombie
1 Le processus orphelin a été adopté par le processus d'initialisation
$pid = pcntl_fork(); if ($pid > 0) { // 显示父进程的进程ID,这个函数可以是getmypid(),也可以用posix_getpid() echo "Father PID:" . getmypid() . PHP_EOL; // 让父进程停止两秒钟,在这两秒内,子进程的父进程ID还是这个父进程 sleep(2); } else if (0 == $pid) { // 让子进程循环10次,每次睡眠1s,然后每秒钟获取一次子进程的父进程进程ID for ($i = 1; $i <= 10; $i++) { sleep(1); // posix_getppid()函数的作用就是获取当前进程的父进程进程ID echo posix_getppid() . PHP_EOL; } } else { echo "fork error." . PHP_EOL; }
. Résultats des tests :
php daemo001.php Father PID:18046 18046 18046 www@iZ2zec3dge6rwz2uw4tveuZ:~/test$ 1 1 1 1 1 1 1 1
2. Processus et dangers zombies
Exécutez le code suivant php zombie1.php
$pid = pcntl_fork(); if( $pid > 0 ){ // 下面这个函数可以更改php进程的名称 cli_set_process_title('php father process'); // 让主进程休息60秒钟 sleep(60); } else if( 0 == $pid ) { cli_set_process_title('php child process'); // 让子进程休息10秒钟,但是进程结束后,父进程不对子进程做任何处理工作,这样这个子进程就会变成僵尸进程 sleep(10); } else { exit('fork error.'.PHP_EOL); }
Résultats d'exécution, une autre fenêtre de terminal
www@iZ2zec3dge6rwz2uw4tveuZ:~$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php www 18458 0.5 1.2 204068 25920 pts/1 S+ 16:34 0:00 php father process www 18459 0.0 0.3 204068 6656 pts/1 S+ 16:34 0:00 php child process www@iZ2zec3dge6rwz2uw4tveuZ:~$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php www 18458 0.0 1.2 204068 25920 pts/1 S+ 16:34 0:00 php father process www 18459 0.0 0.0 0 0 pts/1 Z+ 16:34 0:00 [php] <defunct>
Vous pouvez voir en exécutant la commande ps -aux que lorsque le programme est exécuté dans les dix premières secondes, l'état du processus enfant php est répertorié comme [S+]. après dix secondes, cet état devient [Z+], ce qui signifie qu'il devient un processus zombie qui endommage le système.
Alors, voici la question ? Comment éviter les processus zombies ?
PHP通过 pcntl_wait()
和 pcntl_waitpid()
两个函数来帮我们解决这个问题。了解Linux系统编程的应该知道,看名字就知道这其实就是PHP把C语言中的 wait()
和 waitpid()
包装了一下。
通过代码演示 pcntl_wait()
来避免僵尸进程。
pcntl_wait()
函数:
这个函数的作用就是 “ 等待或者返回子进程的状态 ”,当父进程执行了该函数后,就会阻塞挂起等待子进程的状态一直等到子进程已经由于某种原因退出或者终止。
换句话说就是如果子进程还没结束,那么父进程就会一直等等等,如果子进程已经结束,那么父进程就会立刻得到子进程状态。这个函数返回退出的子进程的进程 ID 或者失败返回 -1。
执行以下代码 zombie2.php
$pid = pcntl_fork(); if ($pid > 0) { // 下面这个函数可以更改php进程的名称 cli_set_process_title('php father process'); // 返回$wait_result,就是子进程的进程号,如果子进程已经是僵尸进程则为0 // 子进程状态则保存在了$status参数中,可以通过pcntl_wexitstatus()等一系列函数来查看$status的状态信息是什么 $wait_result = pcntl_wait($status); print_r($wait_result); print_r($status); // 让主进程休息60秒钟 sleep(60); } else if (0 == $pid) { cli_set_process_title('php child process'); // 让子进程休息10秒钟,但是进程结束后,父进程不对子进程做任何处理工作,这样这个子进程就会变成僵尸进程 sleep(10); } else { exit('fork error.' . PHP_EOL); }
在另外一个终端中通过ps -aux查看,可以看到在前十秒内,php child process 是 [S+] 状态,然后十秒钟过后进程消失了,也就是被父进程回收了,没有变成僵尸进程。
www@iZ2zec3dge6rwz2uw4tveuZ:~/test$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php www@iZ2zec3dge6rwz2uw4tveuZ:~/test$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php www 18519 0.5 1.2 204068 25576 pts/1 S+ 16:42 0:00 php father process www 18520 0.0 0.3 204068 6652 pts/1 S+ 16:42 0:00 php child process www@iZ2zec3dge6rwz2uw4tveuZ:~/test$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php www 18519 0.0 1.2 204068 25576 pts/1 S+ 16:42 0:00 php father process
但是,pcntl_wait() 有个很大的问题,就是阻塞。父进程只能挂起等待子进程结束或终止,在此期间父进程什么都不能做,这并不符合多快好省原则,所以 pcntl_waitpid() 闪亮登场。pcntl_waitpid( pid, &status, $option = 0 )的第三个参数如果设置为WNOHANG,那么父进程不会阻塞一直等待到有子进程退出或终止,否则将会和pcntl_wait()的表现类似。
修改第三个案例的代码,但是,我们并不添加WNOHANG,演示说明pcntl_waitpid()
功能:
$pid = pcntl_fork(); if ($pid > 0) { // 下面这个函数可以更改php进程的名称 cli_set_process_title('php father process'); // 返回值保存在$wait_result中 // $pid参数表示 子进程的进程ID // 子进程状态则保存在了参数$status中 // 将第三个option参数设置为常量WNOHANG,则可以避免主进程阻塞挂起,此处父进程将立即返回继续往下执行剩下的代码 $wait_result = pcntl_waitpid($pid, $status); var_dump($wait_result); var_dump($status); // 让主进程休息60秒钟 sleep(60); } else if (0 == $pid) { cli_set_process_title('php child process'); // 让子进程休息10秒钟,但是进程结束后,父进程不对子进程做任何处理工作,这样这个子进程就会变成僵尸进程 sleep(10); } else { exit('fork error.' . PHP_EOL); }
下面是运行结果,一个执行php zombie3.php 程序的终端窗口
www@iZ2zec3dge6rwz2uw4tveuZ:~/test$ php zombie3.php int(18586) int(0) ^C
ctrl-c 发送 SIGINT 信号给前台进程组中的所有进程。常用于终止正在运行的程序。
下面是ps -aux终端窗口
www@iZ2zec3dge6rwz2uw4tveuZ:~$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php www 18605 0.3 1.2 204068 25756 pts/1 S+ 16:52 0:00 php father process www 18606 0.0 0.3 204068 6636 pts/1 S+ 16:52 0:00 php child process www@iZ2zec3dge6rwz2uw4tveuZ:~$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php www 18605 0.1 1.2 204068 25756 pts/1 S+ 16:52 0:00 php father process www@iZ2zec3dge6rwz2uw4tveuZ:~$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php www 18605 0.0 1.2 204068 25756 pts/1 S+ 16:52 0:00 php father process www@iZ2zec3dge6rwz2uw4tveuZ:~$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php // ctrl-c 后不再被阻塞 www@iZ2zec3dge6rwz2uw4tveuZ:~$
实际上可以看到主进程是被阻塞的,一直到第十秒子进程退出了,父进程不再阻塞
修改第四段代码,添加第三个参数WNOHANG,代码如下:
$pid = pcntl_fork(); if ($pid > 0) { // 下面这个函数可以更改php进程的名称 cli_set_process_title('php father process'); // 返回值保存在$wait_result中 // $pid参数表示 子进程的进程ID // 子进程状态则保存在了参数$status中 // 将第三个option参数设置为常量WNOHANG,则可以避免主进程阻塞挂起,此处父进程将立即返回继续往下执行剩下的代码 $wait_result = pcntl_waitpid($pid, $status, WNOHANG); var_dump($wait_result); var_dump($status); echo "不阻塞,运行到这里" . PHP_EOL; // 让主进程休息60秒钟 sleep(60); } else if (0 == $pid) { cli_set_process_title('php child process'); // 让子进程休息10秒钟,但是进程结束后,父进程不对子进程做任何处理工作,这样这个子进程就会变成僵尸进程 sleep(10); } else { exit('fork error.' . PHP_EOL); }
执行 php zombie4.php
www@iZ2zec3dge6rwz2uw4tveuZ:~/test$ php zombie4.php int(0) int(0) 不阻塞,运行到这里
另一个ps -aux终端窗口
www@iZ2zec3dge6rwz2uw4tveuZ:~$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php www 18672 0.3 1.2 204068 26284 pts/1 S+ 17:00 0:00 php father process www 18673 0.0 0.3 204068 6656 pts/1 S+ 17:00 0:00 php child process www@iZ2zec3dge6rwz2uw4tveuZ:~$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php www 18672 0.0 1.2 204068 26284 pts/1 S+ 17:00 0:00 php father process www 18673 0.0 0.0 0 0 pts/1 Z+ 17:00 0:00 [php] <defunct>
实际上可以看到主进程是被阻塞的,一直到第十秒子进程退出了,父进程不再阻塞。
问题出现了,竟然php child process进程状态竟然变成了[Z+],这是怎么搞得?回头分析一下代码:
我们看到子进程是睡眠了十秒钟,而父进程在执行pcntl_waitpid()之前没有任何睡眠且本身不再阻塞,所以,主进程自己先执行下去了,而子进程在足足十秒钟后才结束,进程状态自然无法得到回收。
如果我们将代码修改一下,就是在主进程的pcntl_waitpid()前睡眠15秒钟,这样就可以回收子进程了。但是即便这样修改,细心想的话还是会有个问题,那就是在子进程结束后,在父进程执行pcntl_waitpid()回收前,有五秒钟的时间差,在这个时间差内,php child process也将会是僵尸进程。那么,pcntl_waitpid()如何正确使用啊?这样用,看起来毕竟不太科学。
那么,是时候引入信号学了!
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!