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 :)

  • 2
    点赞
  • 2
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:书香水墨 设计师:CSDN官方博客 返回首页
评论

打赏作者

代码一天不写我浑森蓝廋

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值