PHP多进程编程初步

转自:https://www.pureweber.com/article/php-multi-process-programming-preview/php

 

羡慕火影忍者里鸣人的影分身么?没错,PHP程序是能够开动影分身的!想完成任务,又以为一个进程太慢,那么,试试用多进程来搞吧。这篇文章将会介绍一下PHP多进程的基本需求,如何建立多进程以及基本的信号控制,暂时不会告诉你如何进行进程间通讯和信息共享。web

1. 准备

在动手以前,请肯定你用的不是M$ Windows平台(由于我没有Windows)。Linux / BSD / Unix应该都是没问题的。确认好了工做环境之后一块儿来看看咱们须要的PHP模块是否都有。打开终端输入下面的命令:小程序

$ php -m

这个命令检查并打印当前PHP全部开启的扩展,看一下pcntlposix是否在输出的列表中。函数

1.1. pcntl

若是找不到pcntl,八成是编译的时候没把这个扩展编译进去。若是你和我同样是编译安装的PHP,那么须要从新编译安装PHP。在配置的时候记得加上--enable-pcntl参数便可。动画

$ cd /path/to/php_source_code_dir 
$ ./configure [some other options] --enable-pcntl
$ make && make install

1.2. posix

这货通常默认就会装上,只要你编译的时候没有加上--disable-posixspa

2. 预备知识

在继续以前,你还须要对Linux多进程有一点了解。多进程是咋回事呢?这里可跟火影忍者里的影分身稍微有点不一样。首先,鸣人从小长到大,好比16岁,咳。有一天他发动了影分身,分出了5个他。显然,这些分身也是16岁的鸣人而不是刚出生啥也不懂就会哭的婴儿(那叫克隆)。而后,不同的地方来了:分身们变成了独立的人各自去作各自的事,互相之间再也不知道其余分身和原身都作了什么(固然不会像动画片里同样积累经验给原身啦)。除非,他们互相之间有交流,否则,只有16岁以前的事情才是他们共同的记忆。操作系统

有同窗说了,老大你这不坑爹呢么?我又没看过火影忍者!那你去看一遍好了……code

最后,预备知识完了,就是大体了解一下主进程开出来的子进程是怎么回事。子进程的代码和主进程是彻底同样的,还有一部分同样的东西就是直到发动影分身以前执行的全部内容。具体请参见《操做系统》课程。token

3. 影分身之术

因此呢,没有点基础知识怎么能理解卷轴里的内容呢?打开卷轴首先看到了一个单词:fork。进程

3.1. fork

叉子?叉子是分岔的,一个变多个嘛!差很少就是这个意思。建立子进程就用这个命令。这里须要用到pcntl_fork()函数。(能够先简单看一下PHP手册关于这个函数的介绍。)建立一个PHP脚本:

$pid = pcntl_fork(); // 一旦调用成功,事情就变得有些不一样了 if ($pid == -1) { die('fork failed'); } else if ($pid == 0) { } else { } 

pcntl_fork()函数建立一个子进程,子进程和父进程惟一的区别就是PID(进程ID)和PPID(父进程ID)不一样。在终端下查看进程用ps命令(问问man看ps怎么用:man ps)。当函数返回值为-1的时候,说明fork失败了。试试在if前面加一句:echo $pid . PHP_EOL;。运行你的脚本,输出可能像下面这样(结果说明子进程和父进程的代码是相同的):

67789 # 这个是父进程打印的
0     # 这个是子进程打印的

pcntl_fork()函数调用成功后,在父进程中会返回子进程的PID,而在子进程中返回的是0。因此,下面直接用if分支来控制父进程和子进程作不一样的事。

3.2. 分配任务

而后咱们来讲说鸣人16岁那次影分身的事儿,给原身和分身分配两个简单的输出任务:

$parentPid = getmypid(); // 这就是传说中16岁以前的记忆 $pid = pcntl_fork(); // 一旦调用成功,事情就变得有些不一样了 if ($pid == -1) { die('fork failed'); } else if ($pid == 0) { $mypid = getmypid(); // 用getmypid()函数获取当前进程的PID echo 'I am child process. My PID is ' . $mypid . ' and my father's PID is ' . $parentPid . PHP_EOL; } else { echo 'Oh my god! I am a father now! My child's PID is ' . $pid . ' and mine is ' . $parentPid . PHP_EOL; } 

输出的结果多是这样:

Oh my god! I am a father now! My child's PID is 68066 and mine is 68065
I am child process. My PID is 68066 and my father's PID is 68065

再强调一下,pcntl_fork()调用成功之后,一个程序变成了两个程序:一个程序获得的$pid变量值是0,它是子进程;另外一个程序获得的$pid的值大于0,这个值是子进程的PID,它是父进程。在下面的分支语句中,因为$pid值的不一样,运行了不一样的代码。再次强调一下:子进程的代码和父进程的是同样的。因此就要经过分支语句给他们分配不一样的任务。

3.3. 子进程回收

刚刚有man ps么?通常我习惯用ps aux加上grep命令来查找运行着的后台进程。其中有一列STAT,标识了每一个进程的运行状态。这里,咱们关注状态Z:僵尸(Zombie)。当子进程比父进程先退出,而父进程没对其作任何处理的时候,子进程将会变成僵尸进程。Oops,又跟火影里的影分身不同了。鸣人的影分身被干死了之后就自动消失了,可是这里的子进程分身死了话还留着一个空壳在,直到父进程回收它。僵尸进程虽然不占什么内存,可是很碍眼,院子里一堆躺着的僵尸怎么都以为怪怪的。(别忘了它们还占用着PID)

通常来讲,在父进程结束以前回收挂掉的子进程就能够了。在pcntl扩展里面有一个pcntl_wait()函数,它会将父进程挂起,直到有一个子进程退出为止。若是有一个子进程变成了僵尸的话,它会当即返回。全部的子进程都要回收,因此多等等也不要紧啦!

3.4. 父进程先挂了

若是父进程先挂了怎么办?会发生什么?什么也不会发生,子进程依旧还在运行。可是这个时候,子进程会被交给1号进程,1号进程成为了这些子进程的继父。1号进程会很好地处理这些进程的资源,当它们结束时1号进程会自动回收资源。因此,另外一种处理僵尸进程的临时办法是关闭它们的父进程。

4. 信号

通常多进程的事儿讲到上面就完了,但是信号在系统中确实是一个很是重要的东西。信号就是信号灯,点亮一个信号灯,程序就会作出反应。这个你必定用过,好比说在终端下运行某个程序,等了半天也没什么反应,可能你会按 Ctrl+C 来关闭这个程序。实际上,这里就是经过键盘向程序发送了一个中断的信号:SIGINT。有时候进程失去响应了还会执行kill [PID]命令,未加任何其余参数的话,程序会接收到一个SIGTERM信号。程序收到上面两个信号的时候,默认都会结束执行,那么是否有可能改变这种默认行为呢?必须能啊!

4.1. 注册信号

人是活的程序也是活的,只不过程序须要遵循人制定的规则来运行。如今开始给信号从新设定规则,这里用到的函数是pcntl_signal()(继续以前为啥不先查查PHP手册呢?)。下面这段程序将给SIGINT从新定义行为,注意看好:

// 定义一个处理器,接收到SIGINT信号后只输出一行信息 function signalHandler($signal) { if ($signal == SIGINT) { echo 'signal received' . PHP_EOL; } } // 信号注册:当接收到SIGINT信号时,调用signalHandler()函数 pcntl_signal(SIGINT, 'signalHandler'); while (true) { sleep(1);  // do something pcntl_signal_dispatch(); // 接收到信号时,调用注册的signalHandler() } 

执行一下,随时按下 Ctrl+C 看看会发生什么事。

4.2. 信号分发

说明一下:pcntl_signal()函数仅仅是注册信号和它的处理方法,真正接收到信号并调用其处理方法的是pcntl_signal_dispatch()函数。试试把// do something替换成下面这段代码:

for ($i = 0; $i < 1000000; $i++) { echo $i . PHP_EOL; usleep(100000); } 

在终端下执行这个脚本,当它不停输出数字的时候尝试按下 Ctrl+C 。看看程序有什么响应?嗯……什么都没有,除了屏幕可能多了个^C之外,程序一直在不停地输出数字。由于程序一直没有执行到pcntl_signal_dispatch(),因此就并无调用signalHandler(),因此就没有输出signal received

4.3. 版本问题

若是认真看了PHP文档,会发现pcntl_signal_dispatch()这个函数是PHP 5.3以上才支持的,若是你的PHP版本大于5.3,建议使用这个方法调用信号处理器。5.3如下的版本须要在注册信号以前加一句:declare(ticks = 1);表示每执行一条低级指令,就检查一次信号,若是检测到注册的信号,就调用其信号处理器。想一想就挺不爽的,干吗一直都检查?仍是在咱们指定的地方检查一下就好。

4.4. 感觉僵尸进程

如今咱们回到子进程回收的问题上(差点忘了= =")。当你的一个子进程挂了(或者说是结束了),可是父进程还在运行中而且可能很长一段时间不会退出。一个僵尸进程今后站起来了!这时,保护伞公司(内核)发现它的地盘里出现了一个僵尸,这个僵尸是谁儿子呢?看一下PPID就知道了。而后,内核给PPID这个进程(也就是僵尸进程的父进程)发送一个信号:SIGCHLD。而后,你知道怎么在父进程中回收这个子进程了么?提示一下,用pcntl_wait()函数。

4.5. 发送信号

但愿刚刚有认真man过kill命令。它其实就是向进程发送信号,在PHP中也能够调用posix_kill()函数来达到相同的效果。有了它就能够在父进程中控制其余子进程的运行了。好比在父进程结束以前关闭全部子进程,那么fork的时候在父进程记录全部子进程的PID,父进程结束以前依次给子进程发送结束信号便可。

5. 实践

PHP的多进程跟C仍是挺像的,搞明白了之后用其余语言写的话也大同小异差很少都是这么个状况。若是有空的话,尝试写一个小程序,切身体会一下个中滋味:

  1. 16岁的鸣人发送影分身,分出5个分身
  2. 每一个分身随机生存10到30秒,每秒都输出点什么
  3. 保证原身能感觉到分身的结束,而后开动另外一个分身,保证最多有5个分身
  4. 不使用nohup,让原身在终端关闭后依旧可以运行
  5. 把分身数量(5)写进一个配置文件里,当给原身发送信号(能够考虑用SIGUSR1或SIGUSR2)时,原身读取配置文件并更新容许的分身最大数量
  6. 若是分身多了,关闭几个;若是少了,再分出来几个

提示:

  1. while循环保证进程运行,注意sleep以避免100%的CPU占用
  2. 运行进程的终端被关闭时,程序会收到一个SIGHUP信号
  3. 能够用parse_ini_file()函数解析INI配置文件
相关文章
相关标签/搜索