PHP多进程
这个东西写的很早了, 最早在17年的时候写的.有些可能是直接借鉴别人的.具体是谁的已经忘了.如果侵权了,请联系我.
僵尸进程和孤儿进程
孤儿进程:
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。
每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。
这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。
僵尸进程:
一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
任何一个子进程(init除外)在exit()之后,并非马上就消失掉,
而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。
如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。
如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。
如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
僵尸进程危害场景:
unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到。
这种机制就是:
在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。
但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。
直到父进程通过wait / waitpid来取时才释放。
但这样就导致了问题,如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,
其进程号就会一直被占用,但是系统所能使用的进程号是有限的,
如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
例如有个进程,它定期的产生一个子进程,
这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,
但是,父进程只管生成新的子进程,至于子进程 退出之后的事情,则一概不闻不问,
这样,系统运行上一段时间之后,系统中就会存在很多的僵死进程,
倘若用ps命令查看的话,就会看到很多状态为Z的进程。 严格地来说,僵死进程并不是问题的根源,
罪魁祸首是产生出大量僵死进程的那个父进程。
因此,当我们寻求如何消灭系统中大量的僵死进程时,答案就是把产生大 量僵死进程的那个元凶枪毙掉(也就是通过kill发送SIGTERM或者SIGKILL信号啦)。
枪毙了元凶进程之后,它产生的僵死进程就变成了孤儿进 程,这些孤儿进程会被init进程接管,
init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵死的孤儿进程 就能瞑目而去了。
PHP的多进程,仅支cli下.
# 别忘了,禁用CLI之外的一切访问模式.
if (PHP_SAPI != 'cli'){
return;
}
多进程常用函数:
pcntl_alarm — 为进程设置一个alarm闹钟信号
pcntl_errno — 别名 pcntl_strerror
pcntl_exec — 在当前进程空间执行指定程序
pcntl_fork — 创建子进程,在当前进程当前位置产生分支(子进程)。译注:fork是创建了一个子进程,父进程和子进程 都从fork的位置开始向下继续执行,不同的是父进程执行过程中,得到的fork返回值为子进程 号,而子进程得到的是0。
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 — 安装一个信号处理器
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 — 返回导致子进程中断的信号
多进程中常用到的参数
# 获取当前进程的PID,返回进程 id 号
posix_getpid();
#
getmypid()
# 在当前进程当前位置产生分支(子进程)
# 创建一个子进程,这个子进程仅PID(进程号) 和PPID(父进程号)与其父进程不同
# 成功时,在父进程执行线程内返回产生的子进程的PID,在子进程执行线程内返回0。
# 失败时,在父进程上下文返回-1,不会创建子进程,并且会引发一个PHP错误。
pcntl_fork();
# 等待或返回fork的子进程状态(阻塞父进程,直到子进程结束)
# 该函数下面的代码同样会阻塞.
# 该函数上面的代码不受影响.会和子进程,一同执行.
# 所以,如果在使用阻塞进程的时候,如果父子代码还想同时进行,可以写在上面.
# 如果一个子进程在调用此函数时已经退出(俗称僵尸进程),此函数立刻返回
pcntl_wait($status);
#
pcntl_waitpid($pid, $status);
# 如果没有子进程退出立刻返回。(非阻塞方式,通过WNOHANG区分,父与子各走个的)
pcntl_wait($status, WNOHANG);
#
pcntl_waitpid($pid, $status, WNOHANG);
# 仅检查子进程当前是否已经停止()
# 此函数只有作用于使用了WUNTRACED作为
# option的pcntl_waitpid()函数调用产生的status时才有效。
pcntl_wifstopped ( int $status )
# 获取 PHP 进程的 ID
getmypid();
多进程流程
- 生成一个子进程
# 生成一个子进程
$processId = pcntl_fork();
# 子进程创建失败
if ($processId == '-1') {
exit('子进程创建失败');
} else if ($processId) {
# 创建成功返回了子进程的进程ID, 所以这里是父进程
# 这里是父进程的逻辑区
# 父进程阻塞着等待子进程的退出防止出现僵尸进程
pcntl_wait($status);
} else {
# 子进程的得到的进程ID为0, 所以这里是子进程区间
}
# Google的例子
$parentPid = getmypid(); // 这就是传说中16岁之前的记忆
$pid = pcntl_fork(); // 一旦调用成功,事情就变得有些不同了
if ($pid == -1) {
die('fork failed');
} else if ($pid == 0) {
$mypid = getmypid(); // 用getmypid()函数获取当前进程的PID
echo 'I am child process. My PID is ' . $mypid . ' and my father's PID is ' . $parentPid . PHP_EOL;
} else {
echo 'Oh my god! I am a father now! My child's PID is ' . $pid . ' and mine is ' . $parentPid . PHP_EOL;
}
生成多个子进程
# 下面代码生成多个,进程之后有问题.
# 既不是所有的子进程的父进程都为$processId
# 虽然, 能达到多进程的目的和效果, 但是建议不要用,还在找解决办法.
# 最直观的地方在于,被生成的子进程还会继续生成子进程, 导致进程数超出预期
# 有空的时候要研究一下,多进程的执行区间.for是不是也被子进程处理了
# 找到办法了,就是在子进程结束的时候exit.防止子进程泛滥
# 具体方式,可以参考另外一篇日志, PHP多进程编程详解
for ($i = 0; $i < 3; $i++) {
$processId = pcntl_fork();
if ($processId == '-1') {
exit('子进程创建失败');
} else if ($processId) {
# 这里是父进程的区间
} else {
# 这里是子进程的区间
}
}
子进程泛滥解决方案
子进程泛滥说明
此种方案生成的子进程的父进程都是1号进程.
所以需要单独处理,例如在运行区间进行判断.子进程中不可再生成进程.
官方文档是这样说的:
pcntl_fork()函数创建一个子进程,这个子进程仅PID(进程号) 和PPID(父进程号)与其父进程不同。fork怎样在您的系统工作的详细信息请查阅您的系统 的fork(2)手册。
成功时,在父进程执行线程内返回产生的子进程的PID,在子进程执行线程内返回0。失败时,在 父进程上下文返回-1,不会创建子进程,并且会引发一个PHP错误。
这样就可以创建一个子进程了,子进程创建成功以后会执行pcntl_fork()之后的方法。那么对于这个函数的返回值我们如何理解呢?
是这样的,我们调用函数创建进程的时候,函数执行时有时间的,而新的进程刚好是在函数执行开始和结束之间创建出来的,这样,新的进程也执行了这个函数,所以函数也需要有返回值。那么对于该函数一次执行之后,父进程和子进程都会受到该函数的返回值,由于父进程创建了子进程,而子进程并没有创建新的进程,所以子进程对于这个函数的返回结果是没有的,所以就给他赋了一个0。而父进程创建了子进程,子进程是存在pid的,所以就得到了那个进程的pid。
我们可以写个程序了解一下:
$pid = pcntl_fork();
var_dump($pid);
这个调用会输出两个值,但是我们如果直接print的只能看到一个值,也就是子进程的pid,但是使用var_dump我们就可以看到两个值,是0和子进程的pid。0这个值就是子进程返回过来的。
那么如何创建进程了解清楚之后,就可以开始创建进程了,我们需要创建5个进程,那么我就循环5次创建进程。得到如下代码:
$i=0;
while($i!=5){
$pid = pcntl_fork();
echo $pid."---------hahah".$i++.PHP_EOL;
}
这样就写好了,那么运行一下吧。啊?发现不是5个进程啊,发现有好多个进程,而且最后一个hahah4这个输出有32个,为什么是32呢?我们算一算。2^5=32,为什么最后的线程数以指数增长了呢?
想发现这个并不难,因为我们之后的每一条都执行了while循环,到最后成了进程的指数增长——也就是说fork的时候把while循环也带了进去。但是我们只是要5个进程而已。怎么办呢?
通过之前对函数的研究可以看到,子进程中会返回一个为0的值,那么我们就可以知道,0为子进程的标记。我们可以通过对子进程标记来结束进程执行。所以我们可以将我们的代码修改为如下形式:
$i=0;
while($i!=5){
$pid = pcntl_fork();
echo $pid."---------hahah".$i++.PHP_EOL;
if ($pid == 0) {
echo "子进程".PHP_EOL;
return;
}
}
因为0其实是对子进程的标记,那么pid这个变量在子进程里实际上是0的,所以当发现pid的值为0的时候,我们就可以断定我们当前进程为一个子进程,不需要在让他执行while并创建子进程的子进程了,所以在执行完我们的内容之后就return或者exit退出这个执行就好了。这样就能保证我们执行创建了5个进程而不是32个了。
避免子进程泛滥的案例
<?php
//最早的进程,也是父进程
$parentPid = getmypid();
echo '原始父进程:' . $parentPid . PHP_EOL;
//开启十个子进程
for($i = 0; $i < 10; $i++) {
$pid = pcntl_fork();
if($pid == -1) {
echo "Could not fork!\n";
exit(1);
}
//子进程
if(!$pid) {
//child process workspace
echo '子进程:' . getmypid() . PHP_EOL;
exit(); //子进程逻辑执行完后,马上退出,以免往下走再fork子进程,不好控制
} else {
echo '父进程:' . getmypid() . PHP_EOL;
}
}
echo getmypid() . PHP_EOL;
$aa = shell_exec("ps -af | grep index.php");
echo $aa;
注意:
通过pcntl_XXX系列函数使用多进程功能。注意:pcntl_XXX只能运行在php CLI(命令行)环境下,在web服务器环境下,会出现无法预期的结果,请慎用!
第二部分
http://www.cnblogs.com/coolworld/p/6606593.html
IT超级码农 - 博客园
应该是他抄的国外网站的,同时进行了一定的改造.
http://www.hackingwithphp.com/16/1/3/getting-into-multiprocessing
hackingwithphp
- 详解
php多进程编程详解
php多进程编程
前言
php单进程存在的问题:
多核处理器未充分利用,而单处理器通常需要等待其他操作完成之后才能再继续工作。 任何现代操作系统都可在幕后执行多任务,这意味着在很短时间内,计算机可以调度多个进程,以执行多个程序。
如果我们将所有的工作都局限在一个进程中,它只能一次做一件事,这意味着我们需要将我们的单进程任务变成一个多进程任务,以便我们可以利用 操作系统的多任务处理能力。
多进程与多线程
在继续之前,先解释下多进程和多线程之间的区别。
进程,是具有其自己的存储器空间,自己的进程ID号等的程序的唯一实例。
线程,可以被认为是一个虚拟进程,它没有自己的进程ID,没有自己的内存空间,但仍然能够利用多任务。
启用超线程的CPU,通过动态生成线程,以尽可能避免延迟,从而进一步推进。
虽然有些人可能不同意,但大多数Unix程序员具有一定程度的不信任的线程。 Unix系统总是首选多进程,然后才是多线程,部分原因是在Unix上创建一个进程(通常称为子进程的“生成”或“分叉”)是非常快的。 在其他操作系统中,如Windows,fork相当慢,所以线程概念更受欢迎。
考虑到这一点,毫不奇怪,所以目前只有在unix系统中支持php以fork多个进程,这个扩展是pcntl_fork函数
php如何进行多进程编程
在php中使用pcntl_fork扩展函数进行frok多个进程。
pcntl_fork返回值说明
当pcntl_fork函数被调用时,它将返回3个值。
如果返回值为-1,则fork失败,并且没有子进程。 这可能是由于缺少内存,或者因为已经达到对用户进程数量的系统限制。
如果返回值是大于0的任何数字,当前脚本是调用pcntl_fork()的父级,返回值是分叉的子进程的进程ID(PID)。 最后,如果返回值为0,则当前脚本是被分叉的子节点。
pcntl_fork执行原理
如果你成功的执行pcntl_fork()函数,将有两个PHP副本同时执行相同的脚本。 它们都从pcntl_fork()行继续执行,最重要的是,子进程获取父进程中设置的所有变量的副本,甚至是资源。 我们忘记的一个关键的事情是,资源的副本不是一个独立的资源,他们将指向同一个事情,这可能是有问题的,更多的详情,稍后将继续讨论。
现在,这里有一个基本使用pcntl_fork()的例子:
<?php
$pid = pcntl_fork();
switch($pid) {
case -1:
print "Could not fork!\n";
exit;
case 0:
print "In child!\n";
break;
default:
print "In parent!\n";
}
?>
上面的脚本只是在父进程和子进程中打印一条消息。 但是,它不显示父项的变量数据如何被复制到子项,它输出了2条信息,如下所示,说明已经是有2个进程在执行了(其中一个是主进程,一个是fork出来的子进程)
[root@25f0b49dc696 wwwroot]# php fork.php
In parent!
In child!
接着看下面的例子:
<?php
$pid1 = pcntl_fork(); //第一次fork
$pid2 = pcntl_fork(); //第二次fork
$pid3 = pcntl_fork(); //第三次fork
$current_process_id = posix_getpid();
echo "current_process_id===$current_process_id===pid1==$pid1===pid2===$pid2==pid3==$pid3\n";
上面的例子,输出结果如下:
current_process_id===13090===pid1==13091===pid2===13092==pid3==13093
current_process_id===13093===pid1==13091===pid2===13092==pid3==0
current_process_id===13092===pid1==13091===pid2===0==pid3==13094
current_process_id===13094===pid1==13091===pid2===0==pid3==0
current_process_id===13091===pid1==0===pid2===13095==pid3==13096
current_process_id===13096===pid1==0===pid2===13095==pid3==0
current_process_id===13095===pid1==0===pid2===0==pid3==13097
current_process_id===13097===pid1==0===pid2===0==pid3==0
分析上面的结果,
可以看出,主进程ID是13090
第一次fork
主13090 ->13091
第二次fork
主13090 ->13092
子13091 ->13095
第三次fork
主13090 ->13093
子13091 ->13096
子13092 ->13094
子13095 ->13097
至此,一共有8个进程在执行当前脚本
接着看下面的例子:
<?php
$main_process_id = posix_getpid();
echo "the main process id==$main_process_id\n";
for ($i = 1; $i <= 5; ++$i) {
$pid = pcntl_fork();
$current_process_id = posix_getpid();
if (!$pid) {
echo "child $i current process id==$current_process_id==pid==$pid\n";
sleep(1);
//sleep($i)
print "In child $i\n";
//这里设置sleep不会阻塞输出,1s后会自动结束进程
//sleep(1);
//结束当前子进程,不让子进程继续fork,不会阻止父进程继续fork
exit;
}
else{
echo "parent current process id==$current_process_id==pid==$pid\n";
print "In parent $i\n";
//fork完毕,退出父进程,不让下次参与fork,能保证执行顺序,但下一次的fork要等待子进程执行完成后才能fork
//exit;
}
}
这次五个子进程被fork创建成功,并且,因为每个子进程在父进程最后设置的时候获取$ i变量的副本,脚本打印出"In child 1", "In child 2", "In child 3", "In child 4", and "In child 5".
[root@25f0b49dc696 wwwroot]# php fork2.php
the main process id==13163
parent current process id==13163==pid==13164
In parent 1
parent current process id==13163==pid==13165
In parent 2
parent current process id==13163==pid==13166
In parent 3
parent current process id==13163==pid==13167
In parent 4
parent current process id==13163==pid==13168
In parent 5
child 3 current process id==13166==pid==0
child 2 current process id==13165==pid==0
child 4 current process id==13167==pid==0
child 5 current process id==13168==pid==0
child 1 current process id==13164==pid==0
[root@25f0b49dc696 wwwroot]# In child 3
In child 4
In child 5
In child 2
In child 1
然而,一切都不是那么简单,因为有两个关键的事情要注意,当你运行上述脚本。
首先,注意每个子脚本在打印出它的消息后调用exit。 在正常情况下,这将立即退出脚本,但在这里,它退出的是子PHP脚本,而不是父或任何其他子脚本。因此,每个其他子脚本和父脚本可以并且确实在一个孩子终止后继续执行。
其次,当脚本运行时,它的输出可能很混乱。
注意孩子们如何按顺序打印出他们的信息。 虽然这可能是很常见的情况,你不能依靠你的孩子被执行在一个特定的顺序。
这是多处理器的基本原则之一:一旦产生了进程,它就是操作系统决定何时执行它以及给出多少时间。
还要注意我如何立即返回到我的shell提示,然后调用五个孩子打印出他们的消息,尽管我显然已经有控制权。
这样做的原因是因为虽然孩子们附着在终端上,但他们基本上是在后台运行的。 一旦父终止,命令提示符将重新出现,你可以开始执行其他程序,但是,正如你可以看到,孩子们仍然会活跃,当他们想(因为孩子们不会做)。 在没有sleep命令情况下,这将不那么明显,但是重要的是记住子进程本质上有自己的运行环境。
PHP,像任何父母,可以使其监视其孩子,以确保他们做正确的事情。 这是通过两个新函数来实现的:
pcntl_waitpid(),它指示PHP等待子进程,
pcntl_wexitstatus(),它获取一个终止子进程返回的值。 我们已经看过exit()函数,以及如何使用它来向系统返回一个值
我们将使用这个值将值发送回父进程,然后检索使用pcntl_wexitstatus()。
在深入了解代码之前,让我先解释一下这些新函数是如何使用的。
pcntl_waitpid
int pcntl_waitpid ( int $pid , int &$status [, int $options = 0 ] )
默认情况下,pcntl_waitpid()将导致父进程无限期地暂停,等待子进程终止。
如果pid指定的子进程在此函数调用时已经退出(俗称僵尸进程),此函数 将立刻返回
至少需要两个参数,$pid-父类应该等待的子进程ID,$status-用来填充子进程状态的变量
$pid的值可以是以下之一:
< -1 等待任意进程组ID等于参数pid给定值的绝对值的进程。例如,如果传递-1802,pcntl_waitpid将等待进程组ID为1802的任何子进程。
-1 等待任意子进程;与pcntl_wait函数行为一致。
0 等待任意与调用进程组ID相同的子进程。这是最常用的值。
> 0 等待进程号等于参数pid值的子进程。也就是说,如果你传入1802,pcntl_waitpid将等待子进程1802终止。
$status
pcntl_waitpid()将会存储状态信息到status 参数上,这个通过status参数返回的状态信息可以用以下函数 pcntl_wifexited(), pcntl_wifstopped(), pcntl_wifsignaled(), pcntl_wexitstatus(), pcntl_wtermsig()以及 pcntl_wstopsig()获取其具体的值。
返回值
pcntl_waitpid()返回退出的子进程进程号,发生错误时返回-1
返回终止子进程的PID,然后用状态变量填充子进程退出的信息。
如果调用pcntl_waitpid并且没有子运行,则立即返回-1并且不填充状态变量。
因此,如果0作为第一个参数传递给函数,pcntl_waitpid()将等待它的任何子进程终止。 当它成立时,它返回子进程的PID,终止并填充第二个参数,并提供有关终止的子进程的信息。 因为我们有几个孩子,我们需要继续调用pcntl_waitpid(),直到它返回-1,每次返回一些东西,我们应该打印出来的子进程的返回值。
从我们的子进程返回一个值就像向exit()传递一个参数一样简单,而不仅仅是终止。 这通过pcntl_waitpid()的返回值返回父节点,返回一个状态代码。 此状态代码不直接求值为返回值,因为它包含两个位的信息:子节点如何终止,以及如果子节点终止,则返回它的退出代码。
现在我们只假设子节点自己终止,这意味着退出代码总是设置在pcntl_waitpid()的返回值里面。 要从返回值提取退出代码,使用pcntl_wexitstatus()函数,它将返回值作为其唯一参数,并返回子进程的退出代码。
这可能听起来很复杂,但是一旦查看下一个代码项目,它应该会变得清楚。 这个例子显示了我们讨论的一切:
<?php
for ($i = 1; $i <= 5; ++$i) {
$pid = pcntl_fork();
if (!$pid) {
sleep(1);
$current_process_id = posix_getpid();
print "In child $i===process_id===$current_process_id\n";
exit($i);
}
}
while (($pid = pcntl_waitpid(0, $status)) != -1) {
$status = pcntl_wexitstatus($status);
echo "Child $status completed==pid==$pid\n";
}
?>
上例将输出,同时也验证了pcntl_waitpid返回的pid是正确的
In child 1===process_id===13106
In child 5===process_id===13110
In child 4===process_id===13109
In child 3===process_id===13108
In child 2===process_id===13107
Child 4 completed==pid==13109
Child 5 completed==pid==13110
Child 1 completed==pid==13106
Child 3 completed==pid==13108
Child 2 completed==pid==13107
注意,通过使用exit($ i);每个子节点返回它在屏幕上打印出来的数字作为其退出代码。 主while循环再次调用pcntl_waitpid(),直到它返回-1(没有子节点),并且对于每个终止的子节点,它使用pcntl_wexitstatus()提取出口代码并打印出来。 注意,pcntl_waitpid()的第一个参数是0,这意味着它将等待所有的孩子。
请注意,通过使用exit($ i);每个孩子都会返回它在屏幕上打印的数字作为退出代码。main while循环一次又一次地调用pcntl_waitpid(),直到它返回-1(没有剩下的孩子),并且对于每个终止的孩子,它使用pcntl_wexitstatus()提取退出代码并将其打印出来。请注意,pcntl_waitpid()的第一个参数是0,这意味着它将等待所有的孩子。
运行该脚本应该停止显示命令提示符,直到所有五个孩子终止,这是理想的。
运行该脚本应该停止命令提示符,直到所有五个孩子终止,这是理想的。