-
博文分类专栏
- Jquery基础教程
-
- 文章:(15)篇
- 阅读:46549
- shell命令
-
- 文章:(42)篇
- 阅读:154227
- Git教程
-
- 文章:(36)篇
- 阅读:234843
- leetCode刷题
-
- 文章:(76)篇
- 阅读:131787
-
PHP中多进程控制之pcntl扩展详解2017-12-23 12:19 阅读(10226) 评论(0)
一、简介
PHP的进程控制(PCNTL)支持实现了Unix方式的进程创建, 程序执行, 信号处理以及进程的中断。可惜的是,进程控制不能被应用在Web服务器环境,当其被用于Web服务环境时可能会带来意外的结果。PCNTL现在使用了ticks作为信号处理的回调机制,可以使用declare() 语句在程序中指定允许发生回调的位置(关于PHP中declare的使用,可以参考“PHP中结构体之declare的使用”)。
需要注意的是:此扩展PCNTL在 Windows 平台上不可用。
二、安装PCNTL
在PHP中进程控制支持默认是关闭的。您需要使用 --enable-pcntl 配置选项重新编译PHP的 CGI或CLI版本以打开进程控制支持。
安装前,我们先来看看,当前php中是否已经安装了该扩展,我们可以使用下面任意一个命令即可
既然没有安装,那么我们就安装吧。幸好,我之前安装php-7.1.8的压缩包还没有删除,可以用来重新编译PHP。
1、首先,进入***/php-7.1.8/ext/pcntl目录,如下:
2、然后使用phpize工具,为PHP添加PCNTL扩展
我的PHP安装在/usr/local/php目录下,添加扩展,分别执行如下命令:
/usr/local/php/bin/phpize ./configure –with-php-config=/usr/local/php/bin/php-config make && make install
执行过程的效果如下:
安装完成后,可以看到php扩展目录下多了一个pcntl.so的文件。
当然,此时并没有完全搞定,还需要在配置文件中,添加 extension=pcntl.so
然后重启即可,现在可以来看看效果了。
可以看到,pcntl扩展已经成功安装了。
三、使用PCNTL
1、创建进程
使用pcntl_fork函数来创建进程,使用posix_getpid函数来获取当前进程的id。关于pcntl_fork简介如下:
int pcntl_fork ( void )
需要注意是,父进程和子进程都从调用pcntl_fork函数位置开始,分别向下继续执行。不同的是父进程在执行过程中,得到的pcntl_fork返回值为子进程号(PID),而子进程得到的是0,如果调用pcntl_fork创建进程失败,则会返回-1。
从调用pcntl_fork函数创建进程后,父进程和子进程无论是数据空间还是指令指针都完全一致,两者再也没有任何继承关系,可以看成是两个独立的进程,通过pcntl_fork返回值,来对他们进行区分。
下面,我们来创建一个进程,看看效果。
echo '下面开始创建进程了'.PHP_EOL; $pid = pcntl_fork(); //从这一行开始,父进程和子进程同时执行 $child = ''; if ($pid==-1) { die('fork失败'); } else if ($pid==0) { //子进程 $pName = '子进程'; } else if ($pid>0) { //父进程执行 $pName = '父进程'; $child = ';子进程(' . $pid . ')'; } $curPid = posix_getpid(); echo date('H:i:s') . $pName .':(PID:' . $curPid . ')' . $child . PHP_EOL; echo date('H:i:s') . $pName .'(PID:' . $curPid .')结束' . PHP_EOL; exit(0);
执行效果如下:
2、防止僵尸进程与孤儿进程
上面的程序会孤儿进程或是僵尸进程吗?为了能够理解这个问题,首先,我们了解什么是僵尸进程,什么又是孤儿进程。
孤儿进程:当一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将会成为孤儿进程。也就是说孤儿进程是没有父进程的进程,不会产生什么危害,因为孤儿进程会被init进程(进程号为1)所管理,并由init进程对它们完成状态收集工作。
僵尸进程:当子进程退出,而父进程仍在运行,且没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这类进程称之为僵死进程。僵尸进程,会一直占用有限的进程号,当系统的进程号用尽的时候,就不能创建新的进程了。
通过上面的概念,我们可以很清楚知道,上面的案例,会导致产生孤儿进程。下面我们来创建一个僵尸进程:
echo '下面开始创建进程了'.PHP_EOL; $pid = pcntl_fork(); //从这一行开始,父进程和子进程同时执行 $child = ''; if ($pid==-1) { die('fork失败'); } else if ($pid==0) { //子进程 $pName = '子进程'; } else if ($pid>0) { //父进程执行 $pName = '父进程'; $child = ';子进程(' . $pid . ')'; } $curPid = posix_getpid(); echo date('H:i:s') . $pName .':(PID:' . $curPid . ')' . $child . PHP_EOL; if ($pid>0) { sleep(20); } echo date('H:i:s') . $pName .'(PID:'.$curPid.')结束' . PHP_EOL;
上诉代码,因为子进程结束了,父进程仍在运行,且没有收集子进程的状态信息,故在父进程结束前,会产生僵尸进程。执行效果如下:
因为僵尸进程的状态为defunct,在执行期间,我们可以通过下面任一一种命令查看当前的僵尸进程
ps -ef | grep defunct ps aux | grep -w 'Z'
上面的程序运行期间,产生的僵尸进程如下:
通过结束父进程,可以清除僵尸进程,但是有的时候,父进程是一个守护进程,那么又怎样防止僵尸进程呢?
针对这个问题,PHP的pcntl扩展提供了pcntl_waitpid 函数,来收集子进程的信息。该函数定义如下:
int pcntl_waitpid ( int $pid , int &$status [, int $options = 0 ] )
参数如下:
1.pid
< -1等待任意进程组ID等于参数pid给定值的绝对值的进程。
-1等待任意子进程;与pcntl_wait函数行为一致。
0等待任意与调用进程组ID相同的子进程。
> 0等待进程号等于参数pid值的子进程。
2.status
pcntl_waitpid()将会存储状态信息到status 参数上,这个通过status参数返回的状态信息可以用以下函数 pcntl_wifexited(), pcntl_wifstopped(), pcntl_wifsignaled(), pcntl_wexitstatus(), pcntl_wtermsig()以及 pcntl_wstopsig()获取其具体的值。
3.options
如果您的操作系统(多数BSD类系统)允许使用wait3,您可以提供可选的options 参数。如果这个参数没有提供,wait将会被用作系统调用。如果wait3不可用,提供参数 options不会有任何效果。options的值可以是0 或者以下两个常量或两个常量“或运算”结果(即两个常量代表意义都有效)。options可用的值:WNOHANG,如果没有子进程退出立刻返回;WUNTRACED,子进程已经退出并且其状态未报告时返回。
返回值
pcntl_waitpid()返回退出的子进程进程号,发生错误时返回-1,如果提供了 WNOHANG作为option(wait3可用的系统)并且没有可用子进程时返回0。
案例如下:
echo '下面开始创建进程了'.PHP_EOL; $pid = pcntl_fork(); //从这一行开始,父进程和子进程同时执行 $childs = []; if ($pid==-1) { die('fork失败'); } else if ($pid==0) { //子进程 $pName = '子进程'; } else if ($pid>0) { //父进程执行 $pName = '父进程'; $childs[] = $pid; //收集子进程 } $curPid = posix_getpid(); echo date('H:i:s') . $pName .':(PID:' .$curPid. ')' . PHP_EOL; while(count($childs) > 0) { //搜索子进程的信息,并清除子进程 foreach($childs as $key => $pid) { $res = pcntl_waitpid($pid, $status, WNOHANG); // If the process has already exited if($res == -1 || $res > 0) unset($childs[$key]); } } if ($pid > 0) { //让父进程20s后退出 sleep(20); } echo date('H:i:s') . $pName .'(PID:'.$curPid.')结束' . PHP_EOL;
上诉案例中,尽管子进程先结束,父进程后结束,也不会产生僵尸进程。执行效果如下:
分析:在上面案例中,pcntl_waitpid 函数第三个参数,之所以使用“WNOHANG”,而不是用“WUNTRACED”,因为如果有多个子进程(pid1,pid2),如果主进程阻塞在获取pid1的执行状态的时候,pid2已完成,那么pid2就会成为僵尸进程。但是使用“WNOHANG”参数,会导致while语言中,大量的空转,从而消耗很大cpu。为了防止空转,我们可以使用pcntl_wait函数。如下:
echo '下面开始创建进程了'.PHP_EOL; $pid = pcntl_fork(); //从这一行开始,父进程和子进程同时执行 $childs = []; if ($pid==-1) { die('fork失败'); } else if ($pid==0) { //子进程 $pName = '子进程'; } else if ($pid>0) { //父进程执行 $pName = '父进程'; $childs[] = $pid; //收集子进程 } $curPid = posix_getpid(); echo date('H:i:s') . $pName .':(PID:' .$curPid. ')' . PHP_EOL; while(count($childs) > 0) { //搜索子进程的信息,并清除子进程 //阻塞主进程,直到一个子进程退出或接收到一个信号要求中断当前进程或调用一个信号处理函数,才往下处理 //这样可以防止,空转 pcntl_wait($status); foreach($childs as $key => $pid) { //检查每个子进程的状态 $res = pcntl_waitpid($pid, $status, WNOHANG); // If the process has already exited if($res == -1 || $res > 0) unset($childs[$key]); } } echo date('H:i:s') . $pName .'(PID:'.$curPid.')结束' . PHP_EOL;
当然,针对上面的情况,我们也可以仅仅使用 pcntl_wait函数,如下:
<?php echo '下面开始创建进程了'.PHP_EOL; echo date('H:i:s') .'pcntl_wait 开始' . PHP_EOL; $rs = pcntl_wait($status); echo date('H:i:s') .'pcntl_wait 结束:'.$rs . PHP_EOL; $pid = pcntl_fork(); //从这一行开始,父进程和子进程同时执行 $childs = []; if ($pid==-1) { die('fork失败'); } else if ($pid==0) { //子进程 $pName = '子进程'; } else if ($pid>0) { //父进程执行 $pName = '父进程'; $childs[$pid] = 1; //收集子进程 } $curPid = posix_getpid(); echo date('H:i:s') . $pName .':(PID:' .$curPid. ')' . PHP_EOL; while(count($childs) > 0) { //搜索子进程的信息,并清除子进程 //阻塞主进程,直到一个子进程退出或接收到一个信号要求中断当前进程或调用一个信号处理函数,才往下处理 //这样可以防止,空转 $childPid = pcntl_wait($status); if ($childPid != -1) { unset($childs[$childPid]); } } echo date('H:i:s') . $pName .'(PID:'.$curPid.')结束' . PHP_EOL;