但我认为在正式开始吹牛之前还是要说两个基本概念:孤儿进程、僵尸进程。
上文我整篇尬聊的都是pcntl_fork()
,只管fork生产,不管产后护理,实际上这样并不符合主流价值观,而且,操作系统本身资源有限,这样无限生产不顾护理,操作系统也会吃不消的。
孤儿进程是指父进程在fork出子进程后,自己先完了。这个问题很尴尬,因为子进程从此变得无依无靠、无家可归,变成了孤儿。用术语来表达就是,父进程在子进程结束之前提前退出,这些子进程将由init(进程ID为1)进程收养并完成对其各种数据状态的收集。init进程是Linux系统下的奇怪进程,这个进程是以普通用户权限运行但却具备超级权限的进程,简单地说,这个进程在Linux系统启动的时候做初始化工作,比如运行getty、比如会根据/etc/inittab中设置的运行等级初始化系统等等,当然了,还有一个作用就是如上所说的:收养孤儿进程。
僵尸进程是指父进程在fork出子进程,而后子进程在结束后,父进程并没有调用wait或者waitpid等完成对其清理善后工作,导致改子进程进程ID、文件描述符等依然保留在系统中,极大浪费了系统资源。所以,僵尸进程是对系统有危害的,而孤儿进程则相对来说没那么严重。在Linux系统中,我们可以通过ps -aux来查看进程,如果有[Z+]标记就是僵尸进程。
在PHP中,父进程对子进程的状态收集等是通过pcntl_wait()
和pcntl_waitpid()
等完成的。依然还是要通过代码还演示说明:
演示并说明孤儿进程的出现,并演示孤儿进程被init进程收养:
$id = pcntl_fork();
if( $pid > 0 ){
echo "Father PID:".getmypid().PHP_EOL;
sleep( 2 );
} else if( 0 == $pid ) {
for( $i = 1; $i <= 10; $i++ ){
sleep( 1 );
echo posix_getppid().PHP_EOL;
} else {
echo "fork error.".PHP_EOL;
}
运行结果如下图:
可以看到,前两秒内,子进程的父进程进程ID为4129,但是从第三秒开始,由于父进程已经提前退出了,子进程变成孤儿进程,所以init进程收养了子进程,所以子进程的父进程进程ID变成了1。
演示并说明僵尸进程的出现,并演示僵尸进程的危害:
$pid = pcntl_fork();
if( $pid > 0 ){
cli_set_process_title('php father process');
sleep(60);
} else if( 0 == $pid ) {
cli_set_process_title('php child process');
sleep(10);
} else {
exit('fork error.'.PHP_EOL);
}
运行结果如下图:
通过执行ps -aux命令可以看到,当程序在前十秒内运行的时候,php child process的状态列为[S+],然而在十秒钟过后,这个状态变成了[Z+],也就是变成了危害系统的僵尸进程。
那么,问题来了?如何避免僵尸进程呢?PHP通过pcntl_wait()和pcntl_waitpid()两个函数来帮我们解决这个问题。了解Linux系统编程的应该知道,看名字就知道这其实就是PHP把C语言中的wait()和waitpid()包装了一下。
通过代码演示pcntl_wait()来避免僵尸进程,在开始之前先简单普及一下pcntl_wait()的相关内容:这个函数的作用就是 “ 等待或者返回子进程的状态 ”,当父进程执行了该函数后,就会阻塞挂起等待子进程的状态一直等到子进程已经由于某种原因退出或者终止。换句话说就是如果子进程还没结束,那么父进程就会一直等等等,如果子进程已经结束,那么父进程就会立刻得到子进程状态。这个函数返回退出的子进程的进程ID或者失败返回-1。
我们将第二个案例中代码修改一下:
$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);
}
将文件保存为wait.php,然后php wait.php,在另外一个终端中通过ps -aux查看,可以看到在前十秒内,php child process是[S+]状态,然后十秒钟过后进程消失了,也就是被父进程回收了,没有变成僵尸进程。
但是,pcntl_wait()
有个很大的问题,就是阻塞。父进程只能挂起等待子进程结束或终止,在此期间父进程什么都不能做,这并不符合多快好省原则,所以pcntl_waitpid()
闪亮登场。pcntl_waitpid( $pid, &$status, $option = 0 )
的第三个参数如果设置为WNOHANG
,那么父进程不会阻塞一直等待到有子进程退出或终止,否则将会和pcntl_wait()
的表现类似。
修改第三个案例的代码,但是,我们并不添加WNOHANG
,演示说明pcntl_waitpid()
功能:
$pid = pcntl_fork();
if( $pid > 0 ){
cli_set_process_title('php father process');
$wait_result = pcntl_waitpid( $pid, $status );
var_dump( $wait_result );
var_dump( $status );
sleep(60);
} else if( 0 == $pid ) {
cli_set_process_title('php child process');
sleep(10);
} else {
exit('fork error.'.PHP_EOL);
}
下面是运行结果,一个执行php程序的终端窗口,另一个是ps -aux终端窗口。实际上可以看到主进程是被阻塞的,一直到第十秒子进程退出了,父进程不再阻塞:
那么我们修改第四段代码,添加第三个参数WNOHANG,代码如下:
$pid = pcntl_fork();
if( $pid > 0 ){
cli_set_process_title('php father process');
$wait_result = pcntl_waitpid( $pid, $status, WNOHANG );
var_dump( $wait_result );
var_dump( $status );
echo "不阻塞,运行到这里".PHP_EOL;
sleep(60);
} else if( 0 == $pid ) {
cli_set_process_title('php child process');
sleep(10);
} else {
exit('fork error.'.PHP_EOL);
}
下面是运行结果,一个执行php程序的终端窗口,另一个是ps -aux终端窗口。实际上可以看到主进程是被阻塞的,一直到第十秒子进程退出了,父进程不再阻塞:
问题出现了,竟然php child process进程状态竟然变成了[Z+],这是怎么搞得?回头分析一下代码:
我们看到子进程是睡眠了十秒钟,而父进程在执行pcntl_waitpid()之前没有任何睡眠且本身不再阻塞,所以,主进程自己先执行下去了,而子进程在足足十秒钟后才结束,进程状态自然无法得到回收。如果我们将代码修改一下,就是在主进程的pcntl_waitpid()前睡眠15秒钟,这样就可以回收子进程了。但是即便这样修改,细心想的话还是会有个问题,那就是在子进程结束后,在父进程执行pcntl_waitpid()回收前,有五秒钟的时间差,在这个时间差内,php child process也将会是僵尸进程。那么,pcntl_waitpid()如何正确使用啊?这样用,看起来毕竟不太科学。
那么,是时候引入信号量了!
信号的产生是有多种方式的,下面是常见的几种:
键盘上按某些组合键,比如Ctrl+C或者Ctrl+D等,会产生SIGINT信号。使用posix kill调用,可以向某个进程发送指定的信号。远程ssh终端情况下,如果你在服务器上执行了一个阻塞的脚本,正在阻塞过程中你关闭了终端,可能就会产生SIGHUP信号。硬件也会产生信号,比如OOM了或者遇到除0这种情况,硬件也会向进程发送特定信号。
而进程在收到信号后,可以有如下三种响应:
直接忽略,不做任何反映。就是俗称的完全不鸟。但是有两种信号,永远不会被忽略,一个是SIGSTOP,另一个是SIGKILL,因为这两个进程提供了向内核最后的可靠的结束进程的办法。捕捉信号并作出相应的一些反应,具体响应什么可以由用户自己通过程序自定义。系统默认响应。大多数进程在遇到信号后,如果用户也没有自定义响应,那么就会采取系统默认响应,大多数的系统默认响应就是终止进程。
用人话来表达,就是说假如你是一个进程,你正在干活,突然施工队的喇叭里冲你嚷了一句:“吃饭了!”,于是你就放下手里的活儿去吃饭。你正在干活,突然施工队的喇叭里冲你嚷了一句:“发工资了!”,于是你就放下手里的活儿去领工资。你正在干活,突然施工队的喇叭里冲你嚷了一句:“有人找你!”,于是你就放下手里的活儿去看看是谁找你什么事情。当然了,你很任性,那是完全可以不鸟喇叭里喊什么内容,也就是忽略信号。也可以更任性,当喇叭里冲你嚷“吃饭”的时候,你去就不去吃饭,你去睡觉,这些都可以由你来。而你在干活过程中,从来不会因为要等某个信号就不干活了一直等信号,而是信号随时随地都可能会来,而你只需要在这个时候作出相应的回应即可,所以说,信号是一种软件中断,也是一种异步的处理事件的方式。
回到上文所说的问题,就是子进程在结束前,父进程就已经先调用了pcntl_waitpid()
,导致子进程在结束后依然变成了僵尸进程。实际上在父进程不断while循环调用pcntl_waitpid()
是个解决办法,大概代码如下:
$pid = pcntl_fork();
if (0 > $pid) {
exit('fork error.' . PHP_EOL);
} else {
if (0 < $pid) {
cli_set_process_title('php father process');
while (true) {
sleep(1);
pcntl_waitpid($pid, &$status, WNOHANG);
} else {
if (0 == $pid) {
cli_set_process_title('php child process');
sleep(20);
exit;
}
下图是运行结果:
解析一下这个结果,我先后三次执行了ps -aux | grep php去查看这两个php进程。
第一次:子进程正在休眠中,父进程依旧在循环中。第二次:子进程已经退出了,父进程依旧在循环中,但是代码还没有执行到pcntl_waitpid(),所以在子进程退出后到父进程执行回收前这段空隙内子进程变成了僵尸进程。第三次:此时父进程已经执行了pcntl_waitpid(),将已经退出的子进程回收,释放了pid等资源。
但是这样的代码有一个缺陷,实际上就是子进程已经退出的情况下,主进程还在不断while pcntl_waitpid()
去回收子进程,这是一件很奇怪的事情,并不符合社会主义主流价值观,不低碳不节能,代码也不优雅,不好看。所以,应该考虑用更好的方式来实现。那么,我们篇头提了许久的信号终于概要出场了。
现在让我们考虑一下,为何信号可以解决“不低碳不节能,代码也不优雅,不好看”的问题。子进程在退出的时候,会向父进程发送一个信号,叫做SIGCHLD
,那么父进程一旦收到了这个信号,就可以作出相应的回收动作,也就是执行pcntl_waitpid()
,从而解决掉僵尸进程,而且还显得我们代码优雅好看节能环保。
梳理一下流程,子进程向父进程发送SIGCHLD信号是对人们来说是透明的,也就是说我们无须关心。但是,我们需要给父进程安装一个响应SIGCHLD信号的处理器,除此之外,还需要让这些信号处理器运行起来,安装上了不运行是一件尴尬的事情。那么,在php里给进程安装信号处理器使用的函数是pcntl_signal(),让信号处理器跑起来的函数是pcntl_signal_dispatch()
。
pcntl_signal()
,安装一个信号处理器,具体说明是pcntl_signal ( int $signo , callback $handler [, bool $restart_syscalls = true ] )
,参数signo就是信号,callback则是响应该信号的代码段,返回bool值。pcntl_signal_dispatch()
,调用每个等待信号通过pcntl_signal()
安装的处理器,参数为void,返回bool值。
下面结合新引入的两个函数来解决一下楼上的丑陋代码:
$pid = pcntl_fork();
if( 0 > $pid ){
exit('fork error.'.PHP_EOL);
} else if( 0 < $pid ) {
pcntl_signal( SIGCHLD, function() use( $pid ) {
echo "收到子进程退出".PHP_EOL;
pcntl_waitpid( $pid, $status, WNOHANG );
cli_set_process_title('php father process');
while( true ){
sleep( 1 );
pcntl_signal_dispatch();
} else if( 0 == $pid ) {
cli_set_process_title('php child process');
sleep( 20 );
exit;
}
运行结果如下: