PHP实现系统编程之 多进程编程介绍及孤儿进程、僵尸进程
多进程编程也是系统编程的一个重要方面,但PHP程序员通常不需要关心多进程的问题,因为web服务器或者PHP-FPM已经帮我们管理好进程方面的问题了,但是如果我们想要用PHP来开发CLI程序,多进程编程是不可或缺的基本技术。
PHP中关于进程控制的方法主要使用到PCNTL(Process Control)扩展, 所以,在进行多进程编程之前,首先要确保你的PHP已经安装了最新的PCNTL扩展,可以输入php -m命令来查看当前已经安装的扩展:
该扩展给我们提供了一组用于进程操作的方法:
PCNTL 函数 pcntl_alarm — 为进程设置一个alarm闹钟信号 pcntl_errno — 别名 pcntl_get_last_error pcntl_exec — 在当前进程空间执行指定程序 pcntl_fork — 在当前进程当前位置产生分支(子进程)。 pcntl_get_last_error — Retrieve the error number set by the last pcntl function which failed pcntl_getpriority — 获取任意进程的优先级 pcntl_setpriority — 修改任意进程的优先级 pcntl_signal_dispatch — 调用等待信号的处理器 pcntl_signal_get_handler — Get the current handler for specified signal pcntl_signal — 安装一个信号处理器 pcntl_sigprocmask — 设置或检索阻塞信号 pcntl_sigtimedwait — 带超时机制的信号等待 pcntl_sigwaitinfo — 等待信号 pcntl_strerror — Retrieve the system error message associated with the given errno pcntl_wait — 等待或返回fork的子进程状态 pcntl_waitpid — 等待或返回fork的子进程状态 pcntl_wexitstatus — 返回一个中断的子进程的返回代码 pcntl_wifexited — 检查状态代码是否代表一个正常的退出。 pcntl_wifsignaled — 检查子进程状态码是否代表由于某个信号而中断 pcntl_wifstopped — 检查子进程当前是否已经停止 pcntl_wstopsig — 返回导致子进程停止的信号 pcntl_wtermsig — 返回导致子进程中断的信号
pcntl_fork — 在当前进程当前位置产生分支(子进程)。译注:fork是创建了一个子进程,父进程和子进程 都从fork的位置开始向下继续执行,不同的是父进程执行过程中,得到的fork返回值为子进程号,而子进程得到的是0。
fork出的子进程几近于完全的复制了父进程,父子进程共享代码段,虽然父子进程的数据段、堆、栈是相互独立的,但在一开始,子进程完全复制了父进程的这些数据,但之后的修改互不影响。
int pcntl_fork ( void )
创建5个子进程代码演示:
<?php for($i = 0; $i < 5; $i++) { $pid = pcntl_fork(); //创建子进程,子进程也是从这里开始执行。 if ($pid == 0) { break; //由于子进程也会执行循环的代码,所以让子进程退出循环,否则子进程又会创建自己的子进程。 } } sleep($i); //第一个创建的子进程将睡眠0秒,第二个将睡眠1s,依次类推...主进程会睡眠5秒 if ($i < 5) { exit("第 " . ($i+1) . " 个子进程退出..." . time() . PHP_EOL); } else { exit("父进程退出..." . time() . PHP_EOL); }
运行结果:
[root@localhost process]# php process.php 第 1 个子进程退出...1503322773 第 2 个子进程退出...1503322774 第 3 个子进程退出...1503322775 第 4 个子进程退出...1503322776 第 5 个子进程退出...1503322777 父进程退出...1503322778
对于pcntl_fork函数要重点理解:“fork是创建了一个子进程,父进程和子进程 都从fork的位置开始向下继续执行,不同的是父进程执行过程中,得到的fork返回值为子进程号,而子进程得到的是0”
把上面的代码稍作修改,不让进程退出,然后利用ps命令查看系统状态:
<?php for($i = 0; $i < 5; $i++) { $pid = pcntl_fork(); if ($pid == 0) { break; //由于子进程也会执行循环的代码,所以让子进程退出循环 } } sleep($i); //第一个创建的子进程将睡眠0秒,第二个将睡眠1s,依次类推...主进程会睡眠5秒 /* if ($i < 5) { exit("第 " . ($i+1) . " 个子进程退出..." . time() . PHP_EOL); } else { exit("父进程退出..." . time() . PHP_EOL); } */ while(1) { sleep(1); //执行死循环不退出 }
运行后输入 ps -ef | grep php 查看系统进程
[root@localhost ~]# ps -ef | grep php root 3670 3609 0 21:54 pts/0 00:00:00 php process.php root 3671 3670 0 21:54 pts/0 00:00:00 php process.php root 3672 3670 0 21:54 pts/0 00:00:00 php process.php root 3673 3670 0 21:54 pts/0 00:00:00 php process.php root 3674 3670 0 21:54 pts/0 00:00:00 php process.php root 3675 3670 0 21:54 pts/0 00:00:00 php process.php root 3677 3646 0 21:54 pts/1 00:00:00 grep php
可以看到6个 php process.php 进程,其中第二列是进程号,第三列是进程的父进程号,可以看到后面五个进程的父进程号都是第一个进程的进程号。
上面的代码子进程和父进程都是执行相同的代码,有没有办法让子进程和父进程做不同的事呢,最简单的办法就是if判断,子进程执行子进程的代码,父进程执行父进程的代码:
<?php $ppid = posix_getpid(); //记录父进程的进程号 for($i = 0; $i < 5; $i++) { $pid = pcntl_fork(); if ($pid == 0) { break; //由于子进程也会执行循环的代码,所以让子进程退出循环 } } if ($ppid == posix_getpid()) { //父进程 while(1) { sleep(1); } } else { //子进程 for($i = 0; $i < 100; $i ++) { echo "子进程" . posix_getpid() . " 循环 $i ...\n"; sleep(1); } }
[root@localhost process]# php process.php 子进程6677 循环 0 ... 子进程6676 循环 0 ... 子进程6678 循环 0 ... 子进程6680 循环 0 ... 子进程6679 循环 0 ... 子进程6677 循环 1 ... 子进程6676 循环 1 ... 子进程6678 循环 1 ... 子进程6680 循环 1 ... 子进程6679 循环 1 ... 子进程6677 循环 2 ... 子进程6676 循环 2 ... 子进程6678 循环 2 ... 子进程6680 循环 2 ...
其实上面的程序父子进程还是执行了相同的代码,只是进入的if分支不一样,而pcntl_exec则可以让子进程完全脱离父进程的影响,去执行新的程序。
pcntl_exec — 在当前进程空间执行指定程序
void pcntl_exec ( string $path [, array $args [, array $envs ]] )
path
path必须时可执行二进制文件路径或一个在文件第一行指定了 一个可执行文件路径标头的脚本(比如文件第一行是#!/usr/local/bin/perl的perl脚本)。 更多的信息请查看您系统的execve(2)手册。
args
args是一个要传递给程序的参数的字符串数组。
envs
envs是一个要传递给程序作为环境变量的字符串数组。这个数组是 key => value格式的,key代表要传递的环境变量的名称,value代表该环境变量值。
注意该方法的返回值比较特殊:当发生错误时返回 FALSE ,没有错误时没有返回,因为pcntl_exec调用成功,子进程就去运行新的程序 从父进程继承的代码段、数据段、堆、栈等信息全部被替换成新的,此时的pcntl_exec函数调用栈已经不存在了,所以也就没有返回了。代码示例:
<?php for($i = 0; $i < 3; $i++) { $pid = pcntl_fork(); if($pid == 0) { echo "子进程pid = " . posix_getpid() . PHP_EOL; $ret = pcntl_exec('/bin/ls'); //执行 ls 命令, 此处调用成功子进程将不会再回来执行下面的任何代码 var_dump($ret); // 此处的代码不会再执行 } } sleep(5); //睡眠5秒以确保子进程执行完毕,原因后面会说 exit( "主进程退出...\n");
运行结果:
[root@localhost process]# php pcntl_exec.php 子进程pid = 6728 子进程pid = 6729 子进程pid = 6727 pcntl_exec.php process.php pcntl_exec.php process.php pcntl_exec.php process.php 主进程退出... [root@localhost process]# ls pcntl_exec.php process.php
以上就是对PHP多进程开发的简单介绍,对于子进程不同的存续状态,引出孤儿进程和僵尸进程的概念,在linux系统中,init进程(1号进程)是所有进程的祖先,其他进程要么是该进程的子进程,要么是子进程的子进程,子进程的子进程的子进程...,linux系统中可以用 pstree 命令查看进程树结构:
在多进程程序中,如果父进程先于子进程退出,那么子进程将会被init进程收养,成为init进程的子进程,这种进程被称为孤儿进程,我们可以把上面的代码稍作修改来演示这种情况:
<?php $ppid = posix_getpid(); //记录父进程的进程号 for($i = 0; $i < 5; $i++) { $pid = pcntl_fork(); if ($pid == 0) { break; //由于子进程也会执行循环的代码,所以让子进程退出循环 } } if ($ppid == posix_getpid()) { //父进程直接退出,它的子进程都会成为孤儿进程 exit(0); } else { //子进程 for($i = 0; $i < 100; $i ++) { echo "子进程" . posix_getpid() . " 循环 $i ...\n"; sleep(1); } }
运行该程序,然后查看进程状态:
[root@localhost ~]# ps -ef | grep php root 2903 1 0 12:09 pts/0 00:00:00 php pcntl.fork.php root 2904 1 0 12:09 pts/0 00:00:00 php pcntl.fork.php root 2905 1 0 12:09 pts/0 00:00:00 php pcntl.fork.php root 2906 1 0 12:09 pts/0 00:00:00 php pcntl.fork.php root 2907 1 0 12:09 pts/0 00:00:00 php pcntl.fork.php root 2935 2912 0 12:10 pts/1 00:00:00 grep php
可以看到五个子进程的父进程号都是1了,并且这时控制台不再被程序占用,子进程转到了后台运行,这种孤儿进程被init进程收养的机制是实现后面将要介绍的守护进程的必要条件之一。
子进程还有一种状态叫僵尸进程,子进程结束时并不是完全退出,内核进程表中仍旧保有该进程的记录,这样做的目的是能够让父进程可以得知子进程的退出状态,以及子进程是自杀(调用exit或代码执行完毕)还是他杀(被信号终止),父进程可以调用pcntl_wait 或 pcntl_waitpid 方法来回收子进程(收尸),释放子进程占用的所有资源,并获得子进程的退出状态,如果父进程不做回收,则僵尸进程一直存在,如果这时父进程也退出了,则这些僵尸进程会被init进程接管并自动回收。
对于linux系统来说,一个长时间运行的多进程程序一定要回收子进程,因为系统的进程资源是有限的,僵尸进程会让系统的可用资源减少。
代码演示僵尸进程的产生:
<?php $ppid = posix_getpid(); //记录父进程的进程号 for($i = 0; $i < 5; $i++) { $pid = pcntl_fork(); if ($pid == 0) { break; //由于子进程也会执行循环的代码,所以让子进程退出循环 } } if ($ppid == posix_getpid()) { //父进程不退出,也不回收子进程 while(1) { sleep(1); } } else { //子进程退出,会成为僵尸进程 exit("子进程退出 $ppid ...\n"); }
运行之后查看进程状态:
[root@localhost ~]# ps -ef | grep php root 2971 2864 0 14:13 pts/0 00:00:00 php pcntl.fork.php root 2972 2971 0 14:13 pts/0 00:00:00 [php] <defunct> root 2973 2971 0 14:13 pts/0 00:00:00 [php] <defunct> root 2974 2971 0 14:13 pts/0 00:00:00 [php] <defunct> root 2975 2971 0 14:13 pts/0 00:00:00 [php] <defunct> root 2976 2971 0 14:13 pts/0 00:00:00 [php] <defunct> root 2978 2912 0 14:13 pts/1 00:00:00 grep php
僵尸进程会用 <defunct>(死者,死人) 来标识,除非我们结束父进程,否则这些僵尸进程会一直存在,也无法用kill命令来杀死。
PHP的pcntl扩展提供了两个回收子进程的方法供我们调用:
int pcntl_wait ( int &$status [, int $options = 0 ] ) int pcntl_waitpid ( int $pid , int &$status [, int $options = 0 ] )
pcntl_wait函数挂起当前进程的执行直到一个子进程退出或接收到一个信号要求中断当前进程或调用一个信号处理函数。 如果一个子进程在调用此函数时已经退出(俗称僵尸进程),此函数立刻返回。子进程使用的所有系统资源将被释放。
关于wait在您系统上工作的详细规范请查看您系统的wait(2)手册。这个函数等同于以-1作为参数pid 的值并且没有options参数来调用pcntl_waitpid() 函数。
代码示例:
<?php $ppid = posix_getpid(); //记录父进程的进程号 for($i = 0; $i < 5; $i++) { $pid = pcntl_fork(); if ($pid == 0) { break; //由于子进程也会执行循环的代码,所以让子进程退出循环 } } if ($ppid == posix_getpid()) { //父进程循环回收收子进程 while(($id = pcntl_wait($status)) > 0) //如果没有子进程退出, pcntl_wait 会一直阻塞 { echo "回收子进程:$id, 子进程退出状态值: $status...\n"; } exit("父进程退出 $id....\n"); //当子进程全部结束 pcntl_wait 返回-1 } else { //子进程退出,会成为僵尸进程 sleep($i); exit($i); }
运行结果:
[root@localhost php]# php pcntl.fork.php 回收子进程:3043, 子进程退出状态值: 0... 回收子进程:3044, 子进程退出状态值: 256... 回收子进程:3045, 子进程退出状态值: 512... 回收子进程:3046, 子进程退出状态值: 768... 回收子进程:3047, 子进程退出状态值: 1024... 父进程退出 -1....
这里只是对PHP多进程编程做了基本的介绍,后面会结合 信号、进程间通信以及守护进程 做更进一步的介绍,欢迎大家关注后续文章。
PHP是世界上最好的语言 That's all :)
相关推荐:
以上就是PHP实现系统编程之 多进程编程介绍及孤儿进程、僵尸进程 的详细内容,更多请关注其它相关文章!
上一篇: PHP 获取远程文件大小的3种解决方法
下一篇: 浅谈PHP变量的值类型和引用类型