PCNTL在PHP中进程控制支持默认是关闭的。您需要使用 --enable-pcntl 配置选项重新编译PHP的 CGI或CLI版本以打开进程控制支持。
Note: 进程控制不能用在Web服务器环境。PCNTL扩展在windows上不可用。
常用函数
pcntl_fork 创建新进程(调用一次,返回两个值,子进程得到的是0,父进程得到的是子进程的id) pcntl_waitpid 等待或返回fork的子进程状态 posix_getpid 返回当前的进程id posix_getppid 取得父进程id pcntl_wait 会挂起当前进程,直到子进程退出,如果子进程在调用此函数之前就已退出,此函数会立刻返回。子进程使用的资源将被释放。
eg:
<?php echo "Master process id=".posix_getpid().PHP_EOL; $pid = pcntl_fork(); switch($pid) { case -1: die("Create failed"); break; case 0: //Child echo "Child process id=".posix_getpid().PHP_EOL; break; default: //Parent echo "Parent process id=".posix_getpid().PHP_EOL; break; } //输出结果 [root@kafka1 pcntl]# php index.php Master process id=3915 Parent process id=3915 Child process id=3916
父进程和子进程的执行是相对独立的,没有先后之分。
<?php echo "Master process id=".posix_getpid().PHP_EOL; $pid = pcntl_fork(); switch($pid) { case -1: die("Create failed"); break; case 0: //Child echo "Child process id=".posix_getpid().PHP_EOL; sleep(2); echo "I will exit \n"; break; default: //Parent if($exit_id = pcntl_waitpid($pid, $status, WUNTRACED)) { echo "Child($exit_id) exited \n"; } echo "Parent process id=".posix_getpid().PHP_EOL; break; }
pcntl_fork
int pcntl_fork ( void )
用于创建子进程。成功时,在父进程执行线程内返回产生的子进程的PID,在子进程执行线程内返回0。失败时,在父进程上下文返回-1,不会创建子进程,并且会引发一个PHP错误。
新建 fork.php <?php $pid = pcntl_fork(); if($pid == -1){ //错误处理:创建子进程失败时返回-1. die( 'could not fork' ); }elseif($pid){ //父进程会得到子进程号,所以这里是父进程执行的逻辑 $id = getmypid(); echo "Parent process,pid {$id}, child pid {$pid}\n"; }else{ //子进程得到的$pid为0, 所以这里是子进程执行的逻辑 $id = getmypid(); echo "Child process,pid {$id}\n"; sleep(10); }
命令行运行:
[root@bogon www.cuikai.com]# php fork.php Parent process,pid 3914, child pid 3915 Child process,pid 3915
该例里父进程还没有来得及等子进程运行完毕就自动退出了,子进程由 init进程接管。通过 ps-ef|grep php 看到子进程还在运行:
[root@bogon www.cuikai.com]# ps -ef | grep php root 4346 1 0 00:42 pts/0 00:00:00 php fork.php UID PID PPID C STIME TTY TIME CMD
子进程成为孤立进程,ppid(父进程id)变成1了。如果在父进程里也加个 sleep(5),你会看到子进程ppid本来是大于1的,后来就变成1了。
注:如果是docker环境,孤立进程的ppid可能是0。
pcntl_wait
pcntl_wait()函数用来让父进程等待子进程退出,默认情况下会阻塞主进程。
紧接着上面的例子,如果想等子进程运行结束后父进程再退出,该怎么办?那就用到 pcntl_wait了。
int pcntl_wait ( int &$status [, int $options = 0 ] )
该函数阻塞当前进程,只到当前进程的一个子进程退出或者收到一个结束当前进程的信号。
我们修改代码:
<?php $pid = pcntl_fork(); if($pid == -1){ exit("fork fail"); }elseif($pid){ $id = getmypid(); echo "Parent process,pid {$id}, child pid {$pid}\n"; pcntl_wait($status); //pcntl_waitpid($pid, $status); }else{ $id = getmypid(); echo "Child process,pid {$id}\n"; sleep(10); }
此时再次运行程序,父进程就会一直等待子进程运行结束然后退出。
pcntl_waitpid()和 pcntl_wait()功能相同。前者第一个参数支持指定pid参数,当指定-1作为 pid的值等同于后者。
int pcntl_waitpid ( int $pid , int &$status [, int $options = 0 ] )
当已知子进程pid的时候,可以使用 pcntl_waitpid()。
这两个函数返回退出的子进程进程号(>1),发生错误时返回-1,如果提供了 WNOHANG 作为option(wait3可用的系统)并且没有可用子进程时返回0。
返回值为退出的子进程进程号时,想了解如何退出,可以通过 $status 状态码反应。
pcntl_wait()默认情况下会阻塞主进程,直到子进程执行完毕才继续往下运行。如果设置最后一个参数为常量 WNOHANG,那么就不会阻塞主进程,而是继续执行后续代码, 此时 pcntl_waitpid 就会返回0。
示例:
<?php $pid = pcntl_fork(); if($pid == -1){ exit("fork fail"); }elseif($pid){ $id = getmypid(); echo "Parent process,pid {$id}, child pid {$pid}\n"; while(1){ $res = pcntl_wait($status, WNOHANG); //$res = pcntl_waitpid($pid, $status, WNOHANG); if ($res == -1 || $res > 0){ sleep(10);//此处为了方便看效果,实际不需要 break; } } }else{ $id = getmypid(); echo "Child process,pid {$id}\n"; sleep(2); }
该示例里只有一个子进程,看不出来非阻塞的好处,我们修改一下:
<?php $child_pids = []; for($i=0;$i<3; $i++){ $pid = pcntl_fork(); if($pid == -1){ exit("fork fail"); }elseif($pid){ $child_pids[] = $pid; $id = getmypid(); echo time()." Parent process,pid {$id}, child pid {$pid}\n"; }else{ $id = getmypid(); $rand = rand(1,3); echo time()." Child process,pid {$id},sleep $rand\n"; sleep($rand); //#1 故意设置时间不一样 exit(); //#2 子进程需要exit,防止子进程也进入for循环 } } while(count($child_pids)){ foreach ($child_pids as $key => $pid) { // $res = pcntl_wait($status, WNOHANG); $res = pcntl_waitpid($pid, $status, WNOHANG);//#3 if ($res == -1 || $res > 0){ echo time()." Child process exit,pid {$pid}\n"; unset($child_pids[$key]); }else{ // echo time()." Wait End,pid {$pid}\n"; //#4 } } }
#3处首先先去掉 WNOHANG参数,运行:
$ php fork.1.php 1528637334 Parent process,pid 6600, child pid 6601 1528637334 Child process,pid 6601,sleep 2 1528637334 Parent process,pid 6600, child pid 6602 1528637334 Child process,pid 6602,sleep 2 1528637334 Parent process,pid 6600, child pid 6603 1528637334 Child process,pid 6603,sleep 1 1528637336 Child process exit,pid 6601 1528637336 Child process exit,pid 6602 1528637336 Child process exit,pid 6603
我们看到,6603号进程运行时间最短,但是是最后回收。我们再加上 WNOHANG参数,运行:
$ php fork.1.php 1528637511 Parent process,pid 6695, child pid 6696 1528637511 Child process,pid 6696,sleep 2 1528637511 Parent process,pid 6695, child pid 6697 1528637511 Child process,pid 6697,sleep 1 1528637511 Parent process,pid 6695, child pid 6698 1528637511 Child process,pid 6698,sleep 3 1528637512 Child process exit,pid 6697 1528637513 Child process exit,pid 6696 1528637514 Child process exit,pid 6698
6697进程最先回收!说明确实是异步非阻塞的。感兴趣的朋友还可以开启 #4处代码,未使用 WNOHANG参数的时候,里面的代码是不会运行的。
在 pcntl_wait和 pcntl_waitpid两个函数中的 $status中存了子进程的状态信息,这个参数可以用于 pcntl_wifexited、 pcntl_wifstopped、 pcntl_wifsignaled、 pcntl_wexitstatus、 pcntl_wtermsig、 pcntl_wstopsig、 pcntl_waitpid这些函数。
代码片段:
while(1){ $res = pcntl_wait($status); if ($res == -1 || $res > 0){ if(!pcntl_wifexited($status)){ //进程非正常退出 echo "service exit unusally; pid is $pid\n"; }else{ //获取进程终端的退出状态码; $code = pcntl_wexitstatus($status); echo "service exit code: $code;pid is $pid \n"; } if(pcntl_wifsignaled($status)){ //不是通过接受信号中断 echo "service term not by signal;pid is $pid \n"; }else{ $signal = pcntl_wtermsig($status); echo "service term by signal $signal;pid is $pid\n"; } if(pcntl_wifstopped($status)){ echo "service stop not unusally;pid is $pid \n"; }else{ $signal = pcntl_wstopsig($status); echo "service stop by signal $signal;pid is $pid\n"; } break; }
附:
3、PHP多进程学习(三)父进程与子进程的执行顺序,进程中共享数据
本文为崔凯原创文章,转载无需和我联系,但请注明来自冷暖自知一抹茶ckhttp://www.cksite.cn