什么是PHP7中的孤儿进程与僵尸进程,加上守护进程

基本概念


 

僵尸进程:一个子进程在其父进程还没有调用wait()或waitpid()的情况下退出。这个子进程就是僵尸进程。任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个 子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。


 

孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和*出面处理它的一切善后工作。因此孤儿进程并不会有什么危害 。


 

守护进程(daemon): 是一类在后台运行的特殊进程,用于执行特定的系统任务。很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭。另一些只在需要的时候才启动,完成任务后就自动结束。

用户使守护进程独立于所有终端是因为,在守护进程从一个终端启动的情况下,这同一个终端可能被其他的用户使用。例如,用户从一个终端启动守护进程后退出,然后另外一个人也登录到这个终端。用户不希望后者在使用该终端的过程中,接收到守护进程的任何错误信息。同样,由终端键人的任何信号(例如中断信号)也不应该影响先前在该终端启动的任何守护进程的运行。虽然让服务器后台运行很容易(只要shell命令行以&结尾即可),但用户还应该做些工作,让程序本身能够自动进入后台,且不依赖于任何终端。


 

我们知道在unix/linux中,正常情况下,子进程是通过父进程创建的,子进程在创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束。 当一个 进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。

孤儿进程

一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

僵尸进程

一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

问题及危害

unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过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、孤儿进程被init进程收养

$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 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);
}
执行结果,另外一个终端窗口
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 命令可以看到,当程序在前十秒内运行的时候,php child process 的状态列为 [S+],然而在十秒钟过后,这个状态变成了 [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()如何正确使用啊?这样用,看起来毕竟不太科学。

那么,是时候引入信号学了!

 

实现守护进程

简介

守护进程是一个在后台运行并且不受任何终端控制的进程。Unix操作系统有很多典型的守护进程(其数目根据需要或20—50不等),它们在后台运行,执行不同的管理任务。
用户使守护进程独立于所有终端是因为,在守护进程从一个终端启动的情况下,这同一个终端可能被其他的用户使用。例如,用户从一个终端启动守护进程后退出,然后另外一个人也登录到这个终端。用户不希望后者在使用该终端的过程中,接收到守护进程的任何错误信息。同样,由终端键入的任何信号(例如中断信号)也不应该影响先前在该终端启动的任何守护进程的运行。虽然让服务器后台运行很容易(只要shell命令行以&结尾即可),但用户还应该做些工作,让程序本身能够自动进入后台,且不依赖于任何终端。
守护进程没有控制终端,因此当某些情况发生时,不管是一般的报告性信息,还是需由管理员处理的紧急信息,都需要以某种方式输出。

理解三个概念
 
1.进程组。一坨相关的进程可以组成一个进程组,每个进程组都会有一个组ID(正整数),每个进程组都会有一个组长进程,组长进程的ID等于进程组ID。组长进程可以创建新的进程组以及该进程组中的其他进程。一个进程组的是有生命周期的,即便是组长进程挂了,只有组里还有其他的活口,那就就算该进程组依然存活,只有到组里最后一个活口也挂了,那真的就是彻底没了。
2.会话。一坨相关的进程组组成了一个会话。在*NIX下,是通过setsid()创建一个新的会话。但是值得注意的是,组长进程不能创建会话,简单理解就是在组长进程中,执行setsid函数会报错,这点很重要。所以一般都是组长进程执行fork,然后主进程退出,因为子进程的进程ID是新分配的,而子进程的进程组ID是继承父进程的,所以子进程就注定不可能是组长进程,从而可以确保子进程中一定可以执行setsid函数。在执行setsid函数时候,一般会发生下面三个比较重要的事情:
该进程会创建一个新的进程组,该进程为进程组组长(或者你可以认为这是一种提升)
该进程会创建一个会话组并成为该会话的会话首进程(会话首进程就是创建该会话的进程)
该进程会失去控制终端。如果该进程本来就没有控制终端,则罢了(liao)。如果有,那么该进程也将脱离该控制终端,与之失去联系。
3.控制终端。每个会话可能会拥有一个控制终端(看着比较玄学,你可以暂时理解为就一个那种黑乎乎的命令行窗口),建立与控制终端连接的会话首进程叫做控制进程。

创建步骤

1.创建子进程,终止父进程

由于守护进程是脱离控制终端的,因此首先创建子进程,终止父进程,使得程序在shell终端里造成一个已经运行完毕的假象。之后所有的工作都在子进程中完成,而用户在shell终端里则可以执行其他的命令,从而使得程序以僵尸进程形式运行,在形式I上做到了与控制终端的脱离。

2.在子进程中创建新会话
 
这个步骤是创建守护进程中最重要的一步,在这里使用的是系统函数setsid。
setsid函数用于创建一个新的会话,并担任该会话组的组长。调用setsid仃三个作用:让进程摆脱原会话的控制、让进程摆脱原进程组的控制和让进程摆脱原控制终端的控制。
在调用fork函数时,子进程全盘拷贝父进程的会话期(session,是一个或多个进程组的集合)、进程组、控制终端等,虽然父进程退出了,但原先的会话期、进程组、控制终端等并没有改变,因此,那还不是真正意义上使两者独立开来。setsid函数能够使进程完全独立出来,从而脱离所有其他进程的控制。

3.改变工作目录
 
使用fork创建的子进程也继承了父进程的当前工作目录。由于在进程运行过程中,当前目录所在的文件系统不能卸载,因此,把当前工作目录换成其他的路径,如“/”或“/tmp”等。改变工作目录的常见函数是chdir。
 
4.重设文件创建掩码
 
文件创建掩码是指屏蔽掉文件创建时的对应位。由于使用fork函数新建的子进程继承了父进程的文件创建掩码,这就给该子进程使用文件带来了诸多的麻烦。因此,把文件创建掩码设置为0,可以大大增强该守护进程的灵活性。设置文件创建掩码的函数是umask,通常的使用方法为umask(0)。

5.关闭文件描述符
 
用fork新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读或写,但它们一样消耗系统资源,可能导致所在的文件系统无法卸载。 [2]
 

代码实现

照着上面的理论理解下面的代码

<?php
$fatherFile = fopen(test.txt);
// 重设文件创建掩码
umask( 0 );
// 创建子进程,终止父进程
$pid = pcntl_fork();
if( $pid < 0 ){
    exit(fork error.);
} else if( $pid > 0 ) {
    exit();
}
// 在子进程中创建新会话
if( !posix_setsid() ){
    exit(setsid error.);
}
$pid = pcntl_fork();
if( $pid  < 0 ){
    exit(fork error);
} else if( $pid > 0 ) {
    exit;
}
// 改变工作目录
chdir( /tmp );

// 关闭文件描述符
fclose($fatherFile);

参考地址

什么是PHP7中的孤儿进程与僵尸进程,加上守护进程

上一篇:基于windows10以太坊私有链eth搭建DApp开发环境并演示HelloWorld智能合约


下一篇:《移动平台开发》第3周学习总结