了解php的扩展在liunx下实现多进程
虽然PHP本身不支持多进程,但基于LINUX的PHP扩展PCNTL却可以提供多进程编程。网络上很多同类文章,但笔者进行多次尝试后发现,不是难以控制进程数量,就是有潜在产生僵尸进程或孤儿进程的危险,或者父进程阻塞难以获得更大的并发效果,且大多没有介绍FORK的原理,使得PHP程序员学习PCNTL并发编程尤为困难。本文力求解决这个问题。
FORK编程的大概原理是,每次调用fork函数,操作系统就会产生一个子进程,儿子进程所有的堆栈信息都是原封不动复制父进程的,而在fork之后,父进程与子进程实际上是相互独立的,父子进程不会相互影响。也就是说,fork调用位置之前的所有变量,父进程和子进程是一样的,但fork之后则取决于各自的动作,且数据也是独立的;因为数据已经完整的复制给了子进程。而唯一能够区分父子进程的方法就是判断fork的返回值。如果为0,表示是子进程,如果为正数,表示为父进程,且该正数为子进程的PID(进程号),而如果是-1,表示子进程创建失败。
需要大量的并发进程为同时为我们处理事情。这时,我们就需要fork多次,而产生的子进程数量需要在我们的控制之中,否则无限制的fork只会拖垮服务器。笔者曾经有过经历,几秒钟服务器负载从0.3左右飙到800多,吓的一身冷汗。
而子进程的使用通常会涉及到两种:子进程执行完任务直接退出;子进程常驻内存,等待任务。以上两种方式适用于不同情况。第一种情况大多我们不需要考虑太多,除非子进程的创建是循环进行的。而第二种则需要考虑进程间通信。
无论哪一种,无可避免的一个问题就是僵尸进程。僵尸进程就是子进程退出后,父进程没有及时回收,系统仍然保留子进程的执行信息(例如PID,退出状态等),留待其他程序读取。如果僵尸进程数量很少,我们可以忽略掉。但如果是在一个循环中fork(并发编程中常见的死循环),这个问题就不能无视了,父进程必须定期回收已经退出的子进程。子进程的回收我们采用pcnt_wait函数来完成。
父进程必须等待一个子进程退出后,再创建另外一个。额,这还是串行执行的不是吗?是的,解决办法就是将pcntl_wait函数替换成pcntl_waitpid()并添加WNOHANG参数。该函数可以在没有子进程退出的情况下立刻跳出执行后续代码。
$max
= 800000;
$workers
= 20;
$pids
=
array
();
for
(
$i
= 0;
$i
<
$workers
;
$i
++){
$pids
[
$i
] = pcntl_fork();
switch
(
$pids
[
$i
]) {
case
-1:
echo
"fork error : {$i} \r\n"
;
exit
;
case
0:
$param
=
array
(
'lastid'
=>
$max
/
$workers
*
$i
,
'maxid'
=>
$max
/
$workers
* (
$i
+1),
);
$this
->executeWorker(
$input
,
$output
,
$param
);
exit
;
default
:
break
;
}
}
foreach
(
$pids
as
$i
=>
$pid
) {
if
(
$pid
) {
pcntl_waitpid(
$pid
,
$status
);
}
}
循环创建子进程是一件非常浪费操作系统资源的事情。既然使用了死循环来处理任务,那么就说明任务是一个可以队列化的数据结构。我们可以采用进程间的通信,解决子进程退出重建的问题。而通信的机制主要有信号量、管道、共享内存等。然后我们需要一个生产者和消费者的模型。而基于fork的这种代码编写方式,非常不利于我们编写复杂的业务逻辑。所以建议将进程控制与业务处理的代码进程抽象隔离。进程间通信本文暂不涉及,如果读者有需要可以阅读关于管道、共享内存和信号量的文章。
根据上面所说,循环创建子进程会造成系统资源的浪费,而循环创建往往意味着任务可以队列化。我们可以创建子进程后,让子进程常驻内存,持续执行等待任务到达。而这类模型往往可以用生产-消费模型来实现。生产者负责将任务写入队列,而子进程从队列中取出任务并执行。队列的实现最好采用本身支持互斥的方式,这样可以降低代码的复杂度,管道是个不错的选择。
基于fork方式实现的多进程,由于我们只能使用Pid来做代码隔离,所以进程控制中会充斥的各种if、else或者switch。这对实生产者和消费者模型造成一定难度。以下是一个生产者消费者的模型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
<?php /** * @author:Jenner * @date 2014-01-14 */ class JetMultiProcess { //最大队列长度 private $size ; private $curSize ; //生产者 private $producer ; //消费者 private $worker ; private $queueName ; private $httpsqs ; /** * 构造函数 * @param string $worker 需要创建的消费者类名 * @param int $size 最大子进程数量 * @param $producer 需要创建的消费者类名 */ public function __construct( $producer , $worker , $size =10){ $this ->producer = new $producer ; $this ->worker = $worker ; $this ->size = $size ; $this ->curSize = 0; } public function start(){ $producerPid = pcntl_fork(); if ( $producerPid == -1) { die ( "could not fork" ); } else if ( $producerPid ) { // parent while (true){ $pid = pcntl_fork(); if ( $pid == -1) { die ( "could not fork" ); } else if ( $pid ) { // parent $this ->curSize++; if ( $this ->curSize>= $this ->size){ $sunPid = pcntl_wait( $status ); } } else { // worker $worker = new $this ->worker; $worker ->run(); exit (); } } } else { // producer $this ->producer->run(); exit (); } } } |
以上代码,通过size控制多进程数量,通过构造函数传入生产者和消费者的类型。父进程第一次fork产生一个子进程生产者,然后再进行size次fork创建多个消费者。类似方法可以创建多个生产者和多个消费者协同工作。生产者和消费者都必须实现run方法,并在run方法中创建死循环。循环写入和读取队列进行协同工作。该类没有提供进程间通信的功能。通信需要在生产者和消费者类中实现。这样能够使得进程控制的代码看起来更加简洁
-
-
-
-
-
-
-
-
using namespace std;
-
-
-
-
int
-
main(int argc, char* argv[])
-
{
-
pid_t pid, rmpid;
-
int i;
-
-
for(i=0; i<FORK_NUM; i++){
-
pid = fork();
-
if(pid == 0){
-
break;
-
}else if(pid > 0){
-
printf("creat %dth son \n", i+1);
-
if(i == 2){
-
rmpid = pid;
-
}
-
}else{
-
printf("creat %dth son error\n", i+1);
-
}
-
}
-
-
sleep(i+1);
-
-
if(i==FORK_NUM){
-
int status;
-
printf("I am parent\n");
-
-
if(waitpid(rmpid, &status, 0) == -1){// only wait 3th son progress
-
perror("waipid error");
-
exit(1);
-
}
-
if(WIFEXITED(status)!=0 && WIFSIGNALED(status)==0){
-
printf("son progress normal exit\n");
-
printf("son progress exit status : %d \n", WEXITSTATUS(status));
-
}else{// 0
-
printf("son expretion exit,signal ID:%d \n", WTERMSIG(status));
-
}
-
printf("the %dth is waitpided by me\n", i+1);
-
while(1)sleep(1);
-
-
do{
-
rmpid = waitpid(-1, &status, WNOHANG);
-
if(rmpid == 0){
-
sleep(1);
-
continue;
-
}
-
else if(rmpid == -1)break;
-
else{
-
i--;
-
printf("has %d son progress wll to be wait\n", i);
-
}
-
-
}while(i > 0);
-
-
while(1) sleep(1);
-
-
-
}else{
-
printf("I am child %d \n", i+1);
-
}
-
-
-
return 0;
-
}