基本概念
unix/linux では、通常の状況では、子プロセスが親プロセスを通じて作成され、子プロセスが新しいプロセスを作成することがわかっています。子プロセスの終了と親プロセスの実行は非同期プロセスです。つまり、親プロセスは子プロセスがいつ終了するかを予測できません。プロセスが作業を完了して終了すると、その親プロセスは、wait() または waitpid() システム コールを呼び出して、子プロセスの終了ステータスを取得する必要があります。
孤立プロセス
1 つ以上の子プロセスがまだ実行中に親プロセスが終了すると、それらの子プロセスは孤立プロセスになります。孤立プロセスは init プロセス (プロセス番号 1) に採用され、init プロセスがステータス収集作業を完了します。
ゾンビ プロセス
プロセスは fork を使用して子プロセスを作成します。子プロセスが終了し、親プロセスが wait または waitpid を呼び出してステータス情報を取得しない場合子プロセスのプロセス記述子は引き続きシステムに保存されます。このプロセスをゾンビプロセスと呼びます。
問題と危険
Unix は、子プロセスの終了時に親プロセスがステータス情報を知りたい限り、それを確実に取得できるメカニズムを提供します。 。このメカニズムは次のとおりです。各プロセスが終了すると、カーネルは開いているファイル、占有メモリなどを含むプロセスのすべてのリソースを解放します。ただし、特定の情報 (プロセス ID、プロセスの終了ステータス、プロセスにかかった CPU 時間など) は引き続き保持されます。親プロセスが wait/waitpid を通じて取得するまで解放されません。しかし、これでは問題が発生し、プロセスが wait/waitpid を呼ばないと、保持している情報が解放されず、プロセス番号が常に占有されてしまいますが、システムが使用できるプロセス番号には制限があります。ゾンビ プロセスが生成されると、使用可能なプロセス番号がないため、システムは新しいプロセスを生成できなくなります。これはゾンビ プロセスの害であり、回避する必要があります。
孤立プロセスは親プロセスのないプロセスです。孤立プロセスの重要なタスクは init プロセスにあります。init プロセスは民事局のようなもので、孤立プロセスのその後の処理を担当します。孤立したプロセスが現れるたびに、カーネルは孤立したプロセスの親プロセスを init に設定し、init プロセスは終了した子プロセスを周期的に wait() します。このようにして、孤立プロセスがライフサイクルを悲惨な形で終了した場合、init プロセスが党と政府に代わってその後のすべての処理を行います。したがって、孤立したプロセスは害を及ぼしません。
子プロセス (init を除く) は、exit() の直後には消えませんが、ゾンビ プロセス (Zombie) と呼ばれるデータ構造を残して、親プロセスの処理を待ちます。これは、すべての子プロセスが最後に通過する段階です。子プロセスが exit() の後に処理する時間がない場合は、ps コマンドを使用して、子プロセスのステータスが「Z」であることを確認できます。親プロセスが時間内にそれを処理できる場合、ps コマンドを使用して子プロセスのゾンビ状態を確認するには手遅れになる可能性がありますが、これは子プロセスがゾンビ状態を経ないという意味ではありません。子プロセスが終了する前に親プロセスが終了した場合、子プロセスは init によって引き継がれます。 initはゾンビ状態の子プロセスを親プロセスとして処理します。
ゾンビ プロセス ハザード シナリオ
たとえば、子プロセスを定期的に生成するプロセスがあります。この子プロセスが行う必要のあることはほとんどありません。完了後、子プロセスが終了すると、子プロセスも終了するため、この子プロセスのライフサイクルは非常に短いです。ただし、親プロセスは新しい子プロセスを生成するだけであり、子の後に何が起こるかは考慮しません。プロセスが終了します。このようにして、システムは一定期間実行されます。その後、システム内に多数のゾンビ プロセスが存在します。ps コマンドを使用して確認すると、ステータス Z のプロセスが多数表示されます。厳密に言うと、ゾンビ プロセスが問題の原因ではなく、多数のゾンビ プロセスを生成した親プロセスが問題の原因となります。したがって、システム内の大量のゾンビ プロセスを排除する方法を探す場合、その答えは、大量のゾンビ プロセスを生成した犯人を撃つことです (つまり、kill を通じて SIGTERM または SIGKILL シグナルを送信します)。原因プロセスが撃たれた後、それが生成するゾンビ プロセスは孤立プロセスになります。これらの孤立プロセスは init プロセスによって引き継がれます。init プロセスはこれらの孤立プロセスを wait() し、システム プロセス テーブル内で占有しているリソースを解放します。このようにして、停止したこれらの孤立したプロセスは平和に消滅することができます。
孤立プロセスとゾンビプロセスのテスト
1.孤立プロセスは初期化プロセスに採用されます
$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; }
テスト結果:
php daemo001.php Father PID:18046 18046 18046 www@iZ2zec3dge6rwz2uw4tveuZ:~/test$ 1 1 1 1 1 1 1 1
2. ゾンビプロセスとハザード
次のコードを実行 phpゾンビ1.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); }
実行結果、別のターミナルウィンドウ
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>
ps -auxコマンドを実行すると、最初の10秒以内はプログラム実行中はphpの子プロセスのステータスが「S」と表示されますが、10秒を超えるとこのステータスになります。 [Z]、またシステムに害を及ぼすゾンビプロセスになります。
それでは、次のような質問がありますか?ゾンビプロセスを回避するにはどうすればよいですか?
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()如何正确使用啊?这样用,看起来毕竟不太科学。
那么,是时候引入信号学了!
以上がPHP7の孤立プロセスとゾンビプロセスの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。