php多进程编程

2016/07/03 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-用来填充子进程状态的变量 第三个参数可以设置函数是否为阻塞方式调用(设置WNOHANG时以非阻塞方式).

阻塞方式:阻塞当前进程,直到当前进程的一个子进程退出时返回,返回值为子进程的pid,如果发生错误则返回-1;

非阻塞方式:有子进程退出时返回子进程的pid,如果没有子进程退出则立刻返回,返回值为0;

$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,这意味着它将等待所有的孩子。

运行该脚本应该停止命令提示符,直到所有五个孩子终止,这是理想的。

关于进程阻塞

<?php
$pid = pcntl_fork();
if($pid) {
    pcntl_wait($status);
    $id = getmypid();
    echo "parent process,pid {$id}, child pid {$pid}\n";
}else{
    $id = getmypid();
    echo "child process,pid {$id}\n";
    sleep(2);
}

上面例子,输出结果如下。子进程在输出child process等字样之后sleep了2秒才结束,而父进程阻塞着直到子进程退出之后才继续运行。

[root@25f0b49dc696 test]# php fork3.php 
child process,pid 19445
parent process,pid 19444, child pid 19445

关于僵尸进程

在Linux进程的状态中,僵尸进程(Zombie)是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间.当一个进程结束了,而它的父进程没有等待它(wait/waitpid),那么它将会成为僵尸进程,僵尸进程不断累积会导致系统因为没有更多可用的进程号而无法创建新的进程.

看下代码

<?php
date_default_timezone_set("asia/shanghai");

$pid = pcntl_fork();
if ($pid==-1) {
    die('fork失败');
} else if ($pid==0) {
    //子进程执行
    $sec = 10;
    echo date('H:i:s') .'| 我是子进程 (PID:' . posix_getpid() . ')' . ',我休眠' . $sec . '秒后结束' . PHP_EOL;
    
} else if ($pid>0) {
    //父进程执行
    $sec = 15;
    echo date('H:i:s') . '| 我是父进程 (PID:' . posix_getpid() . '),我创建了一个子进程 (PID:' . $pid . ')' . ',我休眠' . $sec . '秒后结束' . PHP_EOL;
}
sleep($sec);
echo date('H:i:s') . '| 进程(PID:'.posix_getpid().')结束' . PHP_EOL;
exit(0);

/**********************************输出*************************************
16:22:13| 我是父进程 (PID:28082),我创建了一个子进程 (PID:28083),我休眠15秒后结束
16:22:13| 我是子进程 (PID:28083),我休眠10秒后结束
16:22:23| 进程(PID:28083)结束
16:22:28| 进程(PID:28082)结束
****************************************************************************/

在上面的例子中是否出现僵尸进程?答案是否,由于父进程休眠15秒后便退出了,子进程没有成为僵尸进程,因为当进程结束时系统会扫描所有进程,看看哪些进程是刚才结束的这个进程的子进程,此时这类子进程将成为“孤儿进程”,这些孤儿进程会过继给1号进程(init进程),init会负责处理这些孤儿进程的资源释放问题.

由此可见,存在僵尸进程必定存在其父进程,而当父进程没有等待子进程时,杀死父进程也可以清理僵尸进程.

当子进程比父进程先退出,而父进程没对其做任何处理的时候,子进程将会变成僵尸进程。

如果上面例子中的主进程是一个常驻的进程,永不退出,那么子进程就会成为僵尸进程,为了避免僵尸进程的产生,父进程需要对其子进程进行等待,pcntl扩展中通过pcntl_waitpid方法实现.

父进程先挂了

如果父进程先挂了怎么办?会发生什么?什么也不会发生,子进程依旧还在运行。但是这个时候,子进程会被交给1号进程,1号进程成为了这些子进程的继父。1号进程会很好地处理这些进程的资源,当它们结束时1号进程会自动回收资源。所以,另一种处理僵尸进程的临时办法是关闭它们的父进程。

预防僵尸进程有以下几种方法:

  1. 父进程通过wait和waitpid等函数使其等待子进程结束,然后再执行父进程中的代码,这会导致父进程挂起。上面的代码就是使用这种方式实现的,但在WEB环境下,它不适合子进程需要长时间运行的情况(会导致超时)。 使用wait和waitpid方法使父进程自动回收其僵尸子进程(根据子进程的返回状态),waitpid用于临控指定子进程,wait是对于所有子进程而言。
  2. 如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler,因为子进程结束后,父进程会收到该信号,可以在handler中调用wait回收
  3. 如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD, SIG_IGN)通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收,并不再给父进程发送信号,例如:
    <?php
    pcntl_signal(SIGCHLD, SIG_IGN);
    $pid = pcntl_fork();
    //....code
    ?>
    
  4. 还有一个技巧,就是fork两次,父进程fork一个子进程,然后继续工作,子进程再fork一个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收还要自己做。下面是一个例子:

提醒

摘自PHP手册:

Process Control should not be enabled within a webserver environment and unexpected results may happen if any Process Control functions are used within a webserver environment.

一句话:请勿在PHP WEB开发中试图通过PCNTL使用多进程!

http://hejunhao.me/archives/470


Search

    Table of Contents