开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第28天,点击查看活动详情
进程是什么?
如果你搜索“什么是进程?”,你大概都会看到这样的答案:“进程是程序的基本执行实体”,“进程是系统进行资源分配和调度的基本单位”,...等等。
看了后你还是不懂什么是进程,因为这个回答太官方了,除了让你不明觉厉之外没有作用。
那为什么进程的概念很难说清楚呢,或者说为什么向人描述清楚“进程是什么?”的这个问题不容易呢。
这要从我们写的代码是怎么在计算机中被执行的说起了,并不是像我们想象中的那样,自上而下,独占CPU,整体一遍过执行完的,真实的代码变成指令,再被调度执行很复杂,这里只做一个引子,感兴趣的读者可以自行查阅相关资料。
代码的真实执行过程不是我们所想象的那样,但是程序员喜欢简单,不想关心底层复杂的具体执行,毕竟写上层业务功能就已经不容易了。于是操作系统就满足了程序员这个愿望,满足的方式就是创建了进程的概念给程序员理解,屏蔽掉其它复杂的概念。
这就是进程。
等等,我好像还没有说“进程是什么”。
但我想你已经明白了,你会有自己的思考和理解,可能每个人理解都会不一样,可至少对于这个问题你已经有了自己的思考,不会再只有不明觉厉。
进程的概念
进程:一个具有一定独立功能的程序关于某个数据集合的一次运行活动,是系统进行资源分配和调度运行的基本单位。
更多的信息需要你自行搜索相关资料以加深理解,详细讲解进程的概念不在本文的范畴内。
php的进程操作需要安装 pcntl 扩展。
多进程
在php中使用 pcntl_fork 函数创建进程。
从官方文档的介绍中,我们总结出pcntl_fork()有以下几点重要的特性:
- 从当前进程
pcntl_fork()调用的位置处产生分支(子进程) - 父进程和子进程 都从fork的位置开始向下继续执行,不同的是父进程执行过程中,得到的fork返回值为子进程的进程id,而子进程得到的是0。
- 返回值:成功时,在父进程执行线程内返回产生的子进程的PID,在子进程执行线程内返回0。失败时,在 父进程上下文返回-1,不会创建子进程,并且会引发一个PHP错误。根据返回值我们就知道父进程与子进程的代码部分。
- 子进程仅PID(进程号) 和PPID(父进程号)与其父进程不同。子进程拥有父进程完整的副本(函数、变量等),而不是共享,进程间相互独立,互不影响,每次fork都将以当前环境产生新的副本。
- fork之后,是父进程先执行还是子进程先执行无法确认,准确的说是,进程的执行顺序不受控制,而是取决于系统调度。
- 在跳转分支结构中,子进程可能会出现“往回执行”的情况,此时应特别注意。通常出现此问题是由于子进程没有退出的原因造成的。
如果你有过其他语言的多进程编程经验的话,那么以上理解起来应该很容易,否则的话,你很有可能会轻视了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的副本的关系,这些微妙的细节造成了这些效果。