PHP 进程是如何理解的?

244 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第28天,点击查看活动详情

进程是什么?

如果你搜索“什么是进程?”,你大概都会看到这样的答案:“进程是程序的基本执行实体”,“进程是系统进行资源分配和调度的基本单位”,...等等。

看了后你还是不懂什么是进程,因为这个回答太官方了,除了让你不明觉厉之外没有作用。

那为什么进程的概念很难说清楚呢,或者说为什么向人描述清楚“进程是什么?”的这个问题不容易呢。

这要从我们写的代码是怎么在计算机中被执行的说起了,并不是像我们想象中的那样,自上而下,独占CPU,整体一遍过执行完的,真实的代码变成指令,再被调度执行很复杂,这里只做一个引子,感兴趣的读者可以自行查阅相关资料

代码的真实执行过程不是我们所想象的那样,但是程序员喜欢简单,不想关心底层复杂的具体执行,毕竟写上层业务功能就已经不容易了。于是操作系统就满足了程序员这个愿望,满足的方式就是创建了进程的概念给程序员理解,屏蔽掉其它复杂的概念。

这就是进程。

等等,我好像还没有说“进程是什么”。

但我想你已经明白了,你会有自己的思考和理解,可能每个人理解都会不一样,可至少对于这个问题你已经有了自己的思考,不会再只有不明觉厉。


进程的概念

进程:一个具有一定独立功能的程序关于某个数据集合的一次运行活动,是系统进行资源分配和调度运行的基本单位。

更多的信息需要你自行搜索相关资料以加深理解,详细讲解进程的概念不在本文的范畴内。

php的进程操作需要安装 pcntl 扩展。


多进程

在php中使用 pcntl_fork 函数创建进程。

从官方文档的介绍中,我们总结出pcntl_fork()有以下几点重要的特性:

  1. 从当前进程pcntl_fork()调用的位置处产生分支(子进程)
  2. 父进程和子进程 都从fork的位置开始向下继续执行,不同的是父进程执行过程中,得到的fork返回值为子进程的进程id,而子进程得到的是0。
  3. 返回值:成功时,在父进程执行线程内返回产生的子进程的PID,在子进程执行线程内返回0。失败时,在 父进程上下文返回-1,不会创建子进程,并且会引发一个PHP错误。根据返回值我们就知道父进程与子进程的代码部分。
  4. 子进程仅PID(进程号) 和PPID(父进程号)与其父进程不同。子进程拥有父进程完整的副本(函数、变量等),而不是共享,进程间相互独立,互不影响,每次fork都将以当前环境产生新的副本。
  5. fork之后,是父进程先执行还是子进程先执行无法确认,准确的说是,进程的执行顺序不受控制,而是取决于系统调度。
  6. 在跳转分支结构中,子进程可能会出现“往回执行”的情况,此时应特别注意。通常出现此问题是由于子进程没有退出的原因造成的。

如果你有过其他语言的多进程编程经验的话,那么以上理解起来应该很容易,否则的话,你很有可能会轻视了fork调用的原理,下面的例子将帮助和加深你对它的认识。

初探: 一个简单的多进程例子

<?php

// 外面所有的代码都是父进程的环境

// 可以获取父进程的ID

// 子进程拥有父进程的完整副本,所以此函数自然也被子进程继承了
function shutdown_function()
{
    global $i;
    $pid = getmypid();
    // 情况2时,这个i一直是4的,待研究……
    echo PHP_EOL . "................ process shutdown i:{$i} pid: $pid " . isStartPid() . '......................' . PHP_EOL;
    sleep(20);
}

register_shutdown_function('shutdown_function');

$startPid = getmypid();
echo "start parent startPid: {$startPid}" . PHP_EOL;

function isStartPid() {
    global $startPid;
    if (getmypid() == $startPid) {
        return ' [is startPid] ';
    } else {
        return ' ';
    }
}

// 这个for在父进程的环境内
for ($i = 1; $i <= 3; $i++) {

    // 由于自进程可能会继续执行for结构的原因(子进程往回执行),所以这里也可能会是子进程的部分

    $_pid = getmypid();

    // $i=1 时,fork()之前的部分属于父进程所有($_pid一定为开始父进程pid,此后就不一定了)

    echo PHP_EOL . PHP_EOL . "=====>>>>>>>>>>==== for i:{$i} pid: $_pid " . isStartPid() . '=====>>>>>>>>>======' . PHP_EOL;

    $pid = pcntl_fork();

    // 父进程fork出来的子进程拥有父进程的副本

    // 那么现在就不止父进程的环境了

    // 那哪部分是父进程的代码,哪部分是子进程的代码呢,通过 $pid 判断

    if ($pid < 0) {
        exit('fork error');
    }

    // 接下来的代码,有两部分,子进程和父进程

    // 父进程和子进程将继续执行fork之后的程序代码

    // fork之后的代码属于父进程和子进程共同所有,通过判断 $pid 分隔开

    if ($pid > 0) {

        // 父进程部分

        $parentPid = getmypid();

        // $pid 其实是子进程的PID(本次fork()的子进程id)

        // 这里也可能会出现儿子逆袭成父的进程(子进程“往回”执行时)

        // 情况1 $parentPid = $startPid ;情况2 不一定

        echo PHP_EOL . "i:{$i} 父  parentPid: $parentPid " . isStartPid() . "   (子: child_pid: $pid)" . PHP_EOL;
    } else if (0 == $pid) {

        $child_pid = getmypid();

        // 子进程部分

        echo PHP_EOL . "    i:{$i} 子  child_pid: $child_pid " . isStartPid() . PHP_EOL;

        // 这行未注释时会出现子进程“往回”执行的情况,为情况2,见分析2。注释时,为情况1,见分析1
        exit;
    }

    // 判断外部分属于父进程和子进程共同所有

    // sleep(1);

    // 但是由于这是for结构,子进程执行到这里后还会继续执行,所以会继续执行for结构,这使得子进程“往回”执行了(要知道本来子进程都在fork之后的)
    // 当为子进程“往回”执行的情况,此时有一些微妙的细节需要注意,此结构会再次执行 pcntl_fork(),注意此时的上下文已经发生变化了
    // 此子进程会再次fork一个子进程,而他本身就成为了另一个父进程(儿子逆袭当父)

    // 父进程更不用说了,继续执行for结构,父进程都是同一个(没发生“往回”执行的情况时),为: start parent pid

    echo PHP_EOL . "=====<<<<<<<<<<<==== for end i:{$i} pid: $_pid " . isStartPid() . '======<<<<<<<<<<<=====' . PHP_EOL . PHP_EOL;
}

// 这儿就完全是父进程的部分了

// 但这儿真的就一定是父进程才会执行的部分吗?那个在“往回”执行时逆势成父的进程呢?它算真正的父进程吗?会执行到这里吗?

$pid = getmypid();
echo PHP_EOL . "=========== for after i:{$i} pid: {$pid} " . isStartPid() . PHP_EOL;

打印分析:

分析1:

print:

start parent startPid: 5955


=====>>>>>>>>>>==== for i:1 pid: 5955  [is startPid] =====>>>>>>>>>======

i:1   parentPid: 5955  [is startPid]    (子: child_pid: 5956)

=====<<<<<<<<<<<==== for end i:1 pid: 5955  [is startPid] ======<<<<<<<<<<<=====



=====>>>>>>>>>>==== for i:2 pid: 5955  [is startPid] =====>>>>>>>>>======

    i:1   child_pid: 5956  

................ process shutdown i:1 pid: 5956  ......................

i:2   parentPid: 5955  [is startPid]    (子: child_pid: 5957)

=====<<<<<<<<<<<==== for end i:2 pid: 5955  [is startPid] ======<<<<<<<<<<<=====



=====>>>>>>>>>>==== for i:3 pid: 5955  [is startPid] =====>>>>>>>>>======

    i:2   child_pid: 5957  

................ process shutdown i:2 pid: 5957  ......................

i:3   parentPid: 5955  [is startPid]    (子: child_pid: 5958)

=====<<<<<<<<<<<==== for end i:3 pid: 5955  [is startPid] ======<<<<<<<<<<<=====


=========== for after i:4 pid: 5955  [is startPid] 

................ process shutdown i:4 pid: 5955  [is startPid] ......................

    i:3   child_pid: 5958  

................ process shutdown i:3 pid: 5958  ......................

process tree:

进程执行完毕后就自动退出了,那还怎么再用 pstree 命令打印进程树呢。只有让每个进程在最后时刻休眠一下,以不让其马上退出才行,要实现这个要求,shutdown_function 函数刚好合适,且只需在最初的父进程中定义一次即可,其后所有子进程都将拥有其副本。

php(5955)─┬─php(5956)
          ├─php(5957)
          └─php(5958)

分析2:

print:

start parent startPid: 6063


=====>>>>>>>>>>==== for i:1 pid: 6063  [is startPid] =====>>>>>>>>>======

i:1   parentPid: 6063  [is startPid]    (子: child_pid: 6064)

=====<<<<<<<<<<<==== for end i:1 pid: 6063  [is startPid] ======<<<<<<<<<<<=====



=====>>>>>>>>>>==== for i:2 pid: 6063  [is startPid] =====>>>>>>>>>======

    i:1   child_pid: 6064  

=====<<<<<<<<<<<==== for end i:1 pid: 6063  ======<<<<<<<<<<<=====



=====>>>>>>>>>>==== for i:2 pid: 6064  =====>>>>>>>>>======

i:2   parentPid: 6063  [is startPid]    (子: child_pid: 6066)

=====<<<<<<<<<<<==== for end i:2 pid: 6063  [is startPid] ======<<<<<<<<<<<=====



=====>>>>>>>>>>==== for i:3 pid: 6063  [is startPid] =====>>>>>>>>>======

i:2   parentPid: 6064     (子: child_pid: 6065)

=====<<<<<<<<<<<==== for end i:2 pid: 6064  ======<<<<<<<<<<<=====



=====>>>>>>>>>>==== for i:3 pid: 6064  =====>>>>>>>>>======

i:3   parentPid: 6063  [is startPid]    (子: child_pid: 6067)

=====<<<<<<<<<<<==== for end i:3 pid: 6063  [is startPid] ======<<<<<<<<<<<=====


=========== for after i:4 pid: 6063  [is startPid] 

................ process shutdown i:4 pid: 6063  [is startPid] ......................

    i:3   child_pid: 6067  

=====<<<<<<<<<<<==== for end i:3 pid: 6063  ======<<<<<<<<<<<=====


=========== for after i:4 pid: 6067  

................ process shutdown i:4 pid: 6067  ......................

    i:2   child_pid: 6066  

=====<<<<<<<<<<<==== for end i:2 pid: 6063  ======<<<<<<<<<<<=====



=====>>>>>>>>>>==== for i:3 pid: 6066  =====>>>>>>>>>======

    i:3   child_pid: 6069  

=====<<<<<<<<<<<==== for end i:3 pid: 6066  ======<<<<<<<<<<<=====


i:3   parentPid: 6064     (子: child_pid: 6068)

=====<<<<<<<<<<<==== for end i:3 pid: 6064  ======<<<<<<<<<<<=====


    i:2   child_pid: 6065  

=====<<<<<<<<<<<==== for end i:2 pid: 6064  ======<<<<<<<<<<<=====



=====>>>>>>>>>>==== for i:3 pid: 6065  =====>>>>>>>>>======

    i:3   child_pid: 6068  

=====<<<<<<<<<<<==== for end i:3 pid: 6064  ======<<<<<<<<<<<=====


=========== for after i:4 pid: 6068  

................ process shutdown i:4 pid: 6068  ......................

=========== for after i:4 pid: 6069  

................ process shutdown i:4 pid: 6069  ......................

i:3   parentPid: 6066     (子: child_pid: 6069)

=====<<<<<<<<<<<==== for end i:3 pid: 6066  ======<<<<<<<<<<<=====


=========== for after i:4 pid: 6066  

................ process shutdown i:4 pid: 6066  ......................

=========== for after i:4 pid: 6064  

................ process shutdown i:4 pid: 6064  ......................

i:3   parentPid: 6065     (子: child_pid: 6070)

=====<<<<<<<<<<<==== for end i:3 pid: 6065  ======<<<<<<<<<<<=====


    i:3   child_pid: 6070  

=====<<<<<<<<<<<==== for end i:3 pid: 6065  ======<<<<<<<<<<<=====


=========== for after i:4 pid: 6070  

=========== for after i:4 pid: 6065  

................ process shutdown i:4 pid: 6065  ......................

................ process shutdown i:4 pid: 6070  ......................

process tree:

php(6063)─┬─php(6064)─┬─php(6065)───php(6070)
          │           └─php(6068)
          ├─php(6066)───php(6069)
          └─php(6067)

通过这个打印过程和进程树分析,大家应该可以清晰地看到进程交错的执行过程、和退出的时机,以及子进程的执行细节。

如“往回执行”成为父进程再生子,注意每次“往回执行”时所携带的当前副本与每次fork的副本的关系,这些微妙的细节造成了这些效果。