最近在研究 workerman 的运行原理。其中运用到了 php 的多进程处理,本文将我的学习程记录下来。首先 php 要安装了 pcntl 和 posix 扩展。一般默认都安装了的,可以使用命令 php -m 确定是否已开启。
什么是多进程?#
齐天大圣孙悟空就有这样的本领。抓一把猴毛,变化出多个分身,有了这么多分身就可以制定多种作战方式,这样打妖怪的效率将大大提升。主进程相当于孙悟空本身,子进程就相当于这些猴儿子。有了多个进程也能更好的利用多核 CPU。
php 生孩子的特殊技巧#
php 生孩子的特殊技巧使用到了 pcntl_fork() 函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| //创建子进程(生孩子)
$son = pcntl_fork();
if ($son === -1) {
//生孩子失败
throw new Exception("fork fail");
} else if ($son === 0) {
//生孩子成功,这个 else if 代码块中都是儿子的领地,让他存活 10 秒,每秒都说句话
$liveTime = 10;
for ($j = 0; $j < $liveTime; $j++) {
sleep(1);
echo "son: " . $j . PHP_EOL;
}
exit(0);
}
//主体生存 20 秒,也每秒都说句话
$liveTime = 20;
for($i = 0; $i < $liveTime; $i++) {
sleep(1);
echo "father: " . $i . PHP_EOL;
}
exit(0);
|
在命令行执行 php 文件名.php 会发现爸爸和儿子是在同时打怪兽。得到的结果如下:
1
2
3
4
5
6
7
8
9
| father: 0
son: 0
father: 1
son: 1
father: 2
son: 2
father: 3
son: 3
....
|
僵尸进程#
当子进程比父进程先退出,而父进程没对其做任何处理的时候,子进程将会变成僵尸进程。但是这里的子进程死了话还留着一个空壳在,直到父进程回收它。僵尸进程虽然不占什么内存,但是很碍眼。
上面的代码运行十秒后,子进程退出,主进程还在运行。这样就能出现僵尸进程的情况(子进程比父进程先退出,并且父进程没有对它做处理)。top 命令 zombie 参数前面的数字就是僵尸进程数量。
一般来说,在父进程结束之前回收挂掉的子进程就可以了。在 pcntl 扩展里面有一个 pcntl_wait() 函数,它会将父进程挂起,直到有一个子进程退出为止。如果有一个子进程变成了僵尸的话,它会立即回收。把上面的代码改为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| //生三个孩子
for($i = 0; $i < 3; $i++) {
//创建子进程(生孩子)
$son = pcntl_fork();
if ($son === -1) {
//生孩子失败
throw new Exception("fork fail");
} else if ($son === 0) {
//生孩子成功,这个 else if 代码块中都是儿子的领地,让他存活 10 秒,每秒都说句话
$liveTime = 10;
//获得这个孩子的进程 id
$son_pid = getmypid();
for ($j = 0; $j < $liveTime; $j++) {
sleep(1);
echo "{$son_pid} son: {$j}" . PHP_EOL;
}
exit(0);
}
}
while(1) {
sleep(1);
//挂起主进程,主进程用来控制子进程
$status = 0;
$pid = pcntl_wait($status, WUNTRACED);
if ($pid > 0) {
//如果有子进程退出,可以做一些事情。比如记录日志,重新生成一个子进程
}
}
|
这样就不会出现僵尸进程了,当然我们也可以做到发现有子进程退出,就重新 fork 一个补充上去。
当然还存在一种情况就是父进程比子进程先退出,然而子进程依旧还在正常运行。但是这个时候,子进程会被交给 1 号进程,1 号进程成为了这些子进程的继父。1 号进程会很好地处理这些进程的资源,当它们结束时 1 号进程会自动回收资源。所以,另一种处理僵尸进程的临时办法是关闭它们的父进程。
信号在系统中是一个非常重要的东西。信号就是信号灯,点亮一个信号灯,程序就会做出反应。这个你一定用过,比如说在终端下运行某个程序,等了半天也没什么反应,可能你会按 Ctrl+C 来关闭这个程序。实际上,这里就是通过键盘向程序发送了一个中断的信号:SIGINT。有时候进程失去响应了还会执行 kill [PID] 命令,未加任何其他参数的话,程序会接收到一个 SIGTERM 信号。程序收到上面两个信号的时候,默认都会结束执行,那么是否有可能改变这种默认行为呢?必须能啊!
pcntl_signal() 函数是用来注册信号处理方法的。下面这段程序将给 SIGINT 重新定义行为:
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 定义一个处理器,接收到 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 看看会发生什么事。
说明一下:pcntl_signal() 函数仅仅是注册信号和它的处理方法,真正接收到信号并调用其处理方法的是 pcntl_signal_dispatch() 函数。
系统中的 kill 命令其实就是向进程发送信号,在 PHP 中也可以调用 posix_kill() 函数来达到相同的效果。有了它就可以在父进程中控制其他子进程的运行了。比如在父进程结束之前关闭所有子进程,那么 fork 的时候在父进程记录所有子进程的 PID,父进程结束之前依次给子进程发送结束信号即可。workerman 就是通过信号来控制程序的 stop reload 的。
重定向标准输出#
之前我们进程的输出都是显示在终端里,一个项目正式运行的时候都是要离开终端,让程序在后台以 守护进程运行。然后程序的输出内容定向到一个文件中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| //自定义标准输出和标准错误保存文件
$stdoutFile = "/tmp/phpout.log";
//全局的标准输出和标准错误
global $STDOUT, $STDERR;
$handle = fopen($stdoutFile,"a");
if($handle) {
unset($handle);
//重新设置标准输出和标准错误的保存位置
@fclose(STDOUT);
@fclose(STDERR);
$STDOUT = fopen($stdoutFile,"a");
$STDERR = fopen($stdoutFile,"a");
} else {
throw new Exception('can not open stdoutFile ' . $stdoutFile);
}
|
守护进程#
程序要脱离终端运行,很简单。先 fork 一个子进程,把原来的主进程结束掉,然后把子进程设置为新的会话,这样就脱离了终端。为了保险起见,我们再 fork 一次,结束掉刚刚设置的新会话 leader。这样就没有可能再回到终端了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| //让 fork 出的子进程拥有最大权限
umask(0);
$pid = pcntl_fork();
if(-1 === $pid) {
throw new Exception('fork fail');
} elseif ($pid > 0) {
exit(0);
}
if(-1 === posix_setsid()) {
throw new Exception("setsid fail");
}
// fork again avoid SVR4 system regain the control of terminal
$pid = pcntl_fork();
if(-1 === $pid) {
throw new Exception("fork fail");
} elseif (0 !== $pid) {
exit(0);
}
|
END#
看完上面说了这么多,应该可以模仿 workerman 写一个简单的多进程控制了。希望对你有所帮助!