初窥PHP下多进程应用

进程 · curry · 于 3个月前 发布 · 131 次阅读

首先,不要在apache或者fpm下使用php的多进程,这会产生不可预估的结果。

在php中使用多进程需要开启pcntl扩展,安装完成之后使用php -m 或者 php --ri pcntl,查看是否安装成功。

一.业务场景

在日常开发过程中,我们 需要定时的执行一个任务量巨大的业务,比如对接了某个外部接口的数据,需要做大量的处理,如果这时候单纯的使用一个进程去处理,耗费的时间可想而知,这时候我们需要开启多个进程并发的处理业务,加快任务处理的速度。

二.创建多进程

在php中使用pcntl_fork(),fork创建一个子进程,父进程和子进程都从fork的位置往下面执行,不同的是在父进程中,返回的是子进程的进程id,而子进程得到的是0.下面来先来一个最基础的片段。

<?php
$pid=pcntl_fork();
if($pid>0){
    echo '我是父进程'.PHP_EOL;
}else if($pid==0){
    echo "我是子进程".PHP_EOL;
}else{
    echo "fork失败".PHP_EOL;
}

运行结果

这里需要注意的:

1.子进程和父进程共享程序的正文段

2.子进程拥有父进程的数据空间,堆,栈的副本,不是共享数据.

3.父进程和子进程将会继续fork之后的代码

4.fork之后,是父进程先执行还是子进程先执行取决于系统的调度情况

关于第二点,在大部分的情况下并不是真正的生成一份副本,为了节约空间,采用的是写时复制的方式,如果父进程和子进程都没对数据进行修改,那么他们实际上使用的还是同一份数据,只要当父进程或者子进程对数据进行改动的到时候,才会进行数据另外复制。

下面我们来验证第二点是副本而不是数据共享。可以看下结果。

<?php

$num=1;
$pid=pcntl_fork();
if($pid>0){
    $num +=1;
    echo '我是父进程$num='.$num.PHP_EOL;
}elseif ($pid==0){
    $num +=5;
    echo '我是子进程$num='.$num.PHP_EOL;
}else{
    echo 'fork失败';
}

再来看看第三点是什么意思,我们先看一段代码,问题是屏幕会输出多少个我是子进程?换句话说这里会生成多少个子进程?你先在脑海中思考结果,先不要看答案。

<?php
for($i=1;$i<=3;$i++){
    $pid=pcntl_fork();
    if($pid>0){
        //
    }elseif ($pid==0){
        echo "我是子进程".PHP_EOL;
    }
}

什么情况?为什么不是三个子进程而是7个啊?还是刚才第三点,fork完之后,继续fork之后的程序。下面给出三次循环生成子进程的情况。

$i=1 主进程fork出一个子进程A    子进程数量1
$i=2 主进程fork出子进程B,子进程A fork出子进程C 子进程数量1+2=3
$i=3 主进程fork出子进程D  子进程A,b,c各自fork出子进程 最终3+4=7
然后我们再调整一下在子进程中添加exit,再查看结果
for($i=1;$i<=3;$i++){
    $pid=pcntl_fork();
    if($pid>0){
        //
    }elseif ($pid==0){
        echo "我是子进程".PHP_EOL;
        exit;
    }
}

答案是三个,也就是说,如果不在子进程中退出,那么子进程就会递归多进程,如果在父进程中退出的话那就是终止多进程了。

三.孤儿进程和僵尸进程

前面的测试中,我们只管fork,之后就不管子进程的死活了,原来你就是现实中的渣男渣女。操作系统的资源是有限的,如果fork出来的进程不做处理,总有一天系统也会垮了,就和人一样,996的工作加上669的夜生活这谁顶的住。

所谓的孤儿进程:就是在父进程fork出子进程之后,自己先挂了,子进程就变成了孤儿,具体孤儿进程被谁收养,感兴趣的可以去了解一下。

所谓的僵尸进程:父进程fork出子进程,在子进程结束任务之后,父进程并没有对子进程做善后的清理工作,导致子进程进程id,文件描述符依然存在于系统中,占用系统资源。我们通过 ps -aux来查看进程,标记[Z+]就是僵尸进程.

在php中,父进程使用pcntl_wait()和pcntl_waitpid()来接受fork出来的子进程状态。

下面演示孤儿进程

<?php
$pid=pcntl_fork();
if($pid>0){
    //getmypid()显示父进程进程id
    echo "父进程id: ".getmypid().'但是两秒我就挂了'.PHP_EOL;
    sleep(2);
}elseif ($pid==0){
    for ($i=1;$i<=5;$i++){
        sleep(1);
        //posix_getppid()获取当前进程的父进程的进程id
        echo "当前父进程pid是".posix_getppid().PHP_EOL;
    }
}else{
    echo "fork error.".PHP_EOL;
}

演示结果

在两秒内,子进程的父进程进程id还是fork它的父进程进程id,但是两秒后父进程挂了,子进程变成了孤儿进程,此时子进程的父进程进程id变成了1,我上面说过,感兴趣的朋友可以去查阅下,其实两秒后的孤儿进程被init进程收走了。

下面演示僵尸进程

$pid=pcntl_fork();
if($pid>0){
    echo '父进程: '.getmypid().PHP_EOL;
    cli_set_process_title('father process');
    sleep(50);  //父进程休息50秒
}elseif ($pid == 0 ){
    cli_set_process_title('child process'); //为当前进程起名字
    sleep(10); //子进程休息10秒,但是子进程结束父进程未做处理
}else{
    echo 'fork error'.PHP_EOL;
}

十秒之后,子进程变成僵尸进程

这个时候为了避免僵尸进程的出现,使用pcntl_wait()和pcntl_waitpid(),这两个函数返回的都是退出的子进程的进程号,发生错误时返回-1。一开始php文档上并没有看出这两个函数有什么区别,后面也是Github上看到别人的demo才知道区别。

pcntl_wait()在父进程执行该函数后,就会挂起,等待子进程的结束或者子进程由于某种原因终止,这个过程是阻塞的,也就是说,在等待的过程中,父进程干不了其他事情。来做个demo

<?php

$pid=pcntl_fork();
if($pid>0){
    echo '父进程: '.getmypid().PHP_EOL;
    $result=pcntl_wait($status);
    echo $result.PHP_EOL;
    echo '这里阻塞了,需要等待子进程返回';
    echo $status.PHP_EOL;
    sleep(50);  //父进程休息50秒
}elseif ($pid == 0 ){
    cli_set_process_title('child process'); //为当前进程起名字
    sleep(10); //子进程休息10秒,但是子进程结束父进程未做处理
}else{
    echo 'fork error'.PHP_EOL;
}

运行结果

你可以从图一中看到,在父进程执行pcntl_wait()函数之后,并没有立刻在控制台打印输入的值,而是挂起,等待我们设定的子程序10秒钟退出,父进程接收到子进程退出的信息,然后继续往下面执行,这个过程是阻塞的,第二张图你也看到十秒钟之后子进程进程id30966消失了,也就是被父进程回收了,没有变成僵尸进程。

接下来实验另一个函数pcntl_waitpid(),这个函数可以设置三个参数。

<?php
$pid=pcntl_fork();
if($pid>0){
    echo '父进程: '.getmypid().PHP_EOL;
    //$pid子进程进程id
    $result=pcntl_waitpid($pid,$status,WNOHANG); 
    echo $result.PHP_EOL;
    echo '现在不阻塞了';
    echo $status.PHP_EOL;
    sleep(50);  //父进程休息50秒
}elseif ($pid == 0 ){
    cli_set_process_title('child process'); //为当前进程起名字
    sleep(10); 
}else{
    echo 'fork error'.PHP_EOL;
}

结果展示

从图一已经看出主进程已经 不在阻塞,但是看到图二,这又是什么鬼,为什么子进程又变成僵尸进程了,姿势不对?

再分析一下上面的代码,因为主进程在执行pcntl_waitpid()之前没有任何睡眠,不阻塞直接执行下去了,子进程足足十秒钟之后才结束,状态也就无法回收了,一个方案是在执行函数之前设置主进程比十秒大的睡眠时间,这样就能在子进程结束的时候回收,但是问题是10s到主进程设置之间还是会有时间差,这个时间区间子进程依旧是个僵尸进程,那就要去下一篇写的信号学了,即学即用。

本文由 curry 创作,采用 知识共享署名 3.0 中国大陆许可协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。

共收到 0 条回复 jobs
没有找到数据。
添加回复 (需要登录)
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册
吴亲库里的深夜食堂