php多进程总结

日常任务中,有时需要通过php脚本执行一些日志分析,队列处理等任务,当数据量比较大时,可以使用多进程来处理。

基本概念:

1)通俗的观念:程序的一个执行实例,或正在执行的程序。
2)内核的观点:担当分配系统资源(CPU时间和内存)的实体。

在引入进程实体的概念后,我们可以把传统操作系统中的进程定义为:”进程是具有独立功能的程序在一个数据集合上运行的过程,是系统进行资源分配和调度的一个独立单位。“

特征:

进程是由多程序的并发执行而引出的,它和程序是两个截然不同的概念。有着自身的特征:

  • 动态性:进程是程序的一次执行,它有着创建、活动、暂停、终止等过程,具有一定的生命周期,是动态地产生、变化和消亡的。动态性是进程最基本的特征。
  • 并发性:指多个进程实体,同存于内存中,能在一段时间内同时运行,并发性是进程的重要特征,同时也是操作系统的重要特征。引入进程的目的就是为了使程序能与其他进程的程序并发执行,以提高资源利用率。
  • 独立性:指进程实体是一个能独立运行、独立获得资源和独立接受调度的基本单位。凡未建立PCB的程序都不能作为一个独立的单位参与运行。
  • 交往特征:一个进程在执行过程中可与其他进程产生直接或间接关系。
  • 异步性:由于进程的相互制约,使进程具有执行的间断性,即进程按各自独立的、 不可预知的速度向前推进。异步性会导致执行结果的不可再现性,为此,在操作系统中必须配置相应的进程同步机制。
  • 结构性:每个进程都配置一个PCB(进程控制块: Process Control Block)对其进行描述。从结构上看,进程实体是由程序段、数据段和进程控制段三部分组成的。

多进程的使用:

场景:

日常任务中,有时需要通过php脚本执行一些日志分析,队列处理等任务,当数据量比较大时,可以使用多进程来处理。

准备:

php多进程需要依赖pcntl的pcntl_fork函数来实现。此函数本身是依赖操作系统的fork实现的,所以该函数只能在linux或者Unix操作系统下使用,Windows是不能使用的。需要php开启pcntl和posix的扩展,可以通过 php - m 查看,没安装的话需要重新编译php,加上参数—enable-pcntl,posix一般默认会有。

示例:

pcntl_fork():

创建一个进程,在父进程返回值是子进程的pid,在子进程返回值是0,-1表示创建进程失败。在父进程中执行会创建一个子进程并返回子进程的pid,在子进程中执行并不会创建新的进程,所以返回0。

pcntl_wait():

pcntl_wait ( int &$status [, int $options ] )
阻塞当前进程,直到任意一个子进程退出或收到一个结束当前进程的信号,注意是结束当前进程的信号,子进程结束发送的SIGCHLD不算。使用$status返回子进程的状态码,并可以指定第二个参数来说明是否以阻塞状态调用

  • 阻塞方式调用的,函数返回值为子进程的pid,如果没有子进程返回值为-1;
  • 非阻塞方式调用,函数还可以在有子进程在运行但没有结束的子进程时返回0。
pcntl_waitpid

pcntl_waitpid( int $pid , int &$status [, int $options ] ):
功能同pcntl_wait,区别为waitpid为等待指定pid的子进程。当pid为-1时pcntl_waitpid与pcntl_wait 一样。在pcntl_wait和pcntl_waitpid两个函数中的$status中存了子进程的状态信息。

一个fork子进程的基础示例:

<?php

$pid = pcntl_fork();
//父进程和子进程都会执行下面代码
if ($pid == -1) {
    //错误处理:创建子进程失败时返回-1.
     die('could not fork');
} else if ($pid) {
     //父进程会得到子进程号,所以这里是父进程执行的逻辑
     pcntl_wait($status); //等待子进程中断,防止子进程成为僵尸进程。
} else {
     //子进程得到的$pid为0, 所以这里是子进程执行的逻辑。
}

前面说到了这个函数是依赖操作系统的fork来实现的,操作系统对fork的实现在原进程(父进程)和子进程中并不相同,导致返回值不同。fork之后,操作系统会复制一个与父进程完全相同的子进程,虽说是父子关系,但是在操作系统看来,他们更像兄弟关系,这2个进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同,但只有一点不同,如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,如果fork失败,父进程会返回错误。

对子进程来说,fork()函数返回给它0, 但它自身的pid绝对不会是0;之所以fork()函数返回0给它,是因为它随时可以调用getpid()来获取自己的pid。


如果一个任务被分解成多个进程执行,就会减少整体的耗时。

比如有一个比较大的数据文件要处理,这个文件由很多行组成。如果单进程执行要处理的任务,量很大时要耗时比较久。这时可以考虑多进程。
多进程处理分解任务,每个进程处理文件的一部分,这样需要均分割一下这个大文件成多个小文件(进程数和小文件的个数等同就可以)。

比如该文件file.log有10万行数据,现在想分4个进程处理。需要分割2.5万行一个文件。命令split可以做到。
split的用法比较简单,可以man split查看下手册。

split -l 25000 -d file.log prefix_name

-l是按照行分割,-d是分割后的文件名按照数字,-a是分割后的文件个数位数(默认是2,做多就是99个;比如超过100个,-a可以写3)。自己尝试分割一下就知道了。

处理代码:

<?php

shell_exec('split -l 25000 -d file.log prefix_name');

// 3个子进程处理任务
for ($i = 0; $i < 3; $i++){
    $pid = pcntl_fork();

    if ($pid == -1) {
        die("could not fork");

    } elseif ($pid) {
        echo "I'm the Parent $i\n";

    } else {// 子进程处理
        $content = file_get_contents("prefix_name0".$i);
        // 业务处理 begin

        // 业务处理 end

        exit;// 一定要注意退出子进程,否则pcntl_fork() 会被子进程再fork,带来处理上的影响。
    }
}

// 等待子进程执行结束
while (pcntl_waitpid(0, $status) != -1) {
    $status = pcntl_wexitstatus($status);
    echo "Child $status completed\n";
}

僵尸进程

一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

僵尸进程是一个早已死亡的进程,但在进程表 (processs table)中仍占了一个位置(slot)。

它们是如何产生的?

当你运行一个程序时,它会产生一个父进程以及很多子进程。 所有这些子进程都会消耗内核分配给它们的内存和 CPU 资源。
这些子进程完成执行后会发送一个 Exit 信号然后死掉。这个 Exit 信号需要被父进程所读取。父进程需要随后调用 wait 命令来读取子进程的退出状态,并将子进程从进程表中移除。
若父进程正确第读取了子进程的 Exit 信号,则子进程会从进程表中删掉。
但若父进程未能读取到子进程的 Exit 信号,则这个子进程虽然完成执行处于死亡的状态,但也不会从进程表中删掉。

但是如果该进程的父进程已经先结束了,那么该进程就不会变成僵尸进程。
因为每个进程结束的时候,系统都会扫描当前系统中所运行的所有进程,看看有没有哪个进程是刚刚结束的这个进程的子进程,如果是的话,就由init进程来接管他,成为他的父进程,从而保证每个进程都会有一个父进程。而init进程会自动wait其子进程,因此被Init接管的所有进程都不会变成僵尸进程。

在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有的内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记录在进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不在占有任何内存空间。

它需要父进程来为它收尸…如果父进程结束了,那么init进程会自动接手这个子进程,为它收尸,它还是能被清除的,但是如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是为什么系统有时候会有很多的僵尸进程;

僵尸进程危害:

僵尸进程对系统有害吗?
不会。由于僵尸进程并不做任何事情, 不会使用任何资源也不会影响其它进程, 因此存在僵尸进程也没什么坏处。

但是如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。

你可以想象成这样:
你是一家建筑公司的老板。你每天根据工人们的工作量来支付工资。 有一个工人每天来到施工现场,就坐在那里, 你不用付钱, 他也不做任何工作。 他只是每天都来然后呆坐在那,仅此而已!
这个工人就是僵尸进程的一个活生生的例子。但是, 如果你有很多僵尸工人, 你的建设工地就会很拥堵从而让那些正常的工人难以工作。

Linux系统寻找和杀掉僵尸进程

首先,可以使用top命令来查看服务器当前是否有僵尸进程,下图中可以看到僵尸进程的提示,如果数字大于0,那么意味着服务器当前存在僵尸进程:

下面,用ps 命令和 grep命令寻找僵尸进程:

ps -A -ostat,ppid,pid,cmd | grep -e '^[Zz]'

命令注解:
-A 参数列出所有进程
-o 自定义输出字段,我们设定显示字段为stat(状态),ppid(父进程pid),pid(进程pid),cmd(命令行)这四个参数
状态为 z 或者 Z的进程为僵尸进程,所以我们使用grep 抓取stat 状态为zZ进程;
运行结果如下所示:

Z 12334 12339 /path/cmd

这时,我们可以使用kill -HUP 12339 来杀掉这个僵尸进程;
如果有多个僵尸进程,可以通过ps -A -ostat,ppid,pid,cmd | grep -e ‘^[Zz]’|awk ‘print{$2}’|xargs kill -9 处理。

僵尸进程解决办法

正常情况下我们可以用 SIGKILL 信号来杀死进程,但是僵尸进程已经死了, 你不能杀死已经死掉的东西。 因此你需要输入的命令应该是

kill -s SIGCHLD pid

将这里的 pid 替换成父进程的进程 id,这样父进程就会删除所有以及完成并死掉的子进程了。

你可以把它想象成:
你在道路中间发现一具尸体,于是你联系了死者的家属,随后他们就会将尸体带离道路了。

不过许多程序写的不是那么好,无法删掉这些子僵尸(否则你一开始也见不到这些僵尸了)。 因此确保删除子僵尸的唯一方法就是杀掉它们的父进程。kill掉父进程,它产生的僵死进程就变成了孤儿进程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源。


参考文献:
php多进程总结
PHP利用多进程处理任务
什么是僵尸进程,如何找到并杀掉僵尸进程?

1000

GS

北京 | php攻城狮

创作 35 粉丝 2

fighting