最近在研究 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
function signalHandler ( $signal ) {
if ( $signal == SIGINT ) {
echo 'signal received' . PHP_EOL ;
}
}
pcntl_signal ( SIGINT , 'signalHandler' );
while ( true ) {
sleep ( 1 );
pcntl_signal_dispatch ();
}
复制
执行一下,随时按下 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
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" );
}
$pid = pcntl_fork ();
if ( - 1 === $pid ) {
throw new Exception ( "fork fail" );
} elseif ( 0 !== $pid ) {
exit ( 0 );
}
复制
END# 看完上面说了这么多,应该可以模仿 workerman 写一个简单的多进程控制了。希望对你有所帮助!