PCNTL扩展实现PHP多进程学习笔记

第一部分 初识PCNTL扩展

一、介绍

0、PCNTL是PHP进程控制扩展,是PHP源码中自带的扩展,支持实现了Unix方式的进程创建, 程序执行, 信号处理以及进程的中断。(也就是说该扩展不能在Windows下使用)

1、编译安装PHP,在生成编译文件的时候带上–enable-pcntl即可,这样安装PHP中就会有这个扩展。

php -m | grep pcntl
pcntl

二、初步使用(多进程之一个子进程)

使用pcntl创建子进程,多进程执行Task。

0、以下代码保存到文件pcntl.php中

class Pcntl_Test
{
    public static function forkTest()
    {
        $pid = pcntl_fork();
        switch ($pid) {
            case -1 :
                echo 'could not fork' . PHP_EOL;
                break;
            case 0 :
                echo 'child process task' . PHP_EOL;
                break;
            default :
                echo 'parent process task' . PHP_EOL;
                // Protect against Zombie children
                pcntl_wait($status);
                break;
        }
    }
}

Pcntl_Test::forkTest();

1、运行

php pcntl.php
parent process task
child process task

三、原理说明

0、多进程的体现(父子进程)
我们执行pcntl.php一次,却得到了普通程序两次执行的结果。

1、多进程的原理
1.0、我们在程序中调用了pcntl_fork函数,这个函数底层调用了操作系统内核函数fork(通常所说的系统调用)。

PHP源码中pcntl_fork函数

/* {{{ proto int pcntl_fork(void)
   Forks the currently running process following the same behavior as the UNIX fork() system call*/
PHP_FUNCTION(pcntl_fork)
{
	pid_t id;

	if (zend_parse_parameters_none() == FAILURE) {
		RETURN_THROWS();
	}

	id = fork();
	if (id == -1) {
		PCNTL_G(last_error) = errno;
		php_error_docref(NULL, E_WARNING, "Error %d", errno);
	}

	RETURN_LONG((zend_long) id);
}
/* }}} */

1.1、fork函数的功能就是在现有的进程中创建一个新进程。
fork函数是操作系统内核函数,函数定义描述如下,函数体可见《UNIX环境高级编程》进程控制章节。

#include <unistd.h>
pid_t fork(void);

返回值: 子进程返回0,父进程返回子进程ID,出错返回-1。

通过man 可以在服务器上看到更多fork的信息

man fork

FORK(2)                    Linux Programmer’s Manual                   FORK(2)
NAME
       fork - create a child process
SYNOPSIS
       #include <unistd.h>
       pid_t fork(void);
DESCRIPTION
       fork()  creates  a new process by duplicating the calling process.  The new process, referred to as the child,
       is an exact duplicate of the calling process, referred to as the parent, except for the following points:

       *  The child has its own unique process ID, and this PID does not match the ID of any existing  process  group
          (setpgid(2)).

       *  The child’s parent process ID is the same as the parent’s process ID.

1.2、fork函数调用一次返回两次,根据返回值的不同区分父子进程。
注:PHP手册pcntl_fork中文译注内容很容易产生误解(pcntl_fork 在当前进程当前位置产生分支(子进程)。fork是创建了一个子进程,父进程和子进程 都从fork的位置开始向下继续执行,不同的是父进程执行过程中,得到的fork返回值为子进程 号,而子进程得到的是0。)

能看英文pcntl_fork还是尽量看英文吧,虽然说明少,但比起错误的信息未尝不是好事。想到一句话,有些感概,免费的信息更贵,错误的信息更致命。

1.3、父子进程执行先后顺序。
一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的,这取决内核所使用的调度算法。

在CentOS release 6.5 (Final)和MacBook Pro (13-inch, 2017, Two Thunderbolt 3 ports)上多次测试,都是父进程先执行。

1.4、父子进程的差异
子进程是父进程的副本,子进程获得父进程数据空间、堆、栈的副本,父子进程不共享存储空间,共享正文段(代码指令集)
更多差异可查看手册

man fork

2、子进程资源回收
通过pcntl_wait函数, 该函数的功能是等待或返回子进程的状态。
这个函数会挂起当前进程的执行直到一个子进程退出或接收到一个信号要求中断当前进程或调用一个信号处理函数。如果一个子进程在调用此函数时已经退出(俗称僵尸进程),此函数立刻返回。子进程使用的所有系统资源将被释放。

3、需要支持pcntl_fork, 可以提前检查

if (!function_exists('pcntl_fork')) die('PCNTL functions not available on this PHP installation');

4、进程控制相关的函数只能在cli模式下使用
PCNTL进程控制不能被应用在Web服务器环境,当其被用于Web服务环境时可能会带来意外的结果。
可以提前检查

if (strtolower(php_sapi_name()) != 'cli') {
    die("请在cli模式下运行");
}

第二部分 PCNTL实现多进程

一、创建多个进程

0、创建多个子进程(超过1个),以下代码保存文件multi_pcntl.php

class Pcntl_Test
{
    public static function forkMultiTest()
    {
        $childs = [];

        // Fork some process.
        for($i = 0; $i < 3; $i++) { 
            $pid = pcntl_fork(); 
            if($pid == -1) die('Could not fork');
            // current pid, alse can use posix_getpid();
            $cur_pid = getmypid();
            if ($pid) { 
                $cur_pid = getmypid();
                echo "parent $i [pid={$cur_pid}] fork child $pid \n";
                $childs[] = $pid; 
            } else { 
                // Sleep $i+1 (s). The child process can get this parameters($i).
                echo 'child ' . $cur_pid . ' is runing...' . PHP_EOL;
                sleep($i + 1);
                echo 'child ' . $cur_pid . ' is done' . PHP_EOL;
                // The child process needed to end the loop.
                exit();
            }
        }
        echo '*******************************', PHP_EOL;
        while(count($childs) > 0) {
            foreach($childs as $key => $pid) {
                $res = pcntl_waitpid($pid, $status, WNOHANG);
            
                // If the process has already exited
                if($res == -1 || $res > 0) {
                    echo "Parent get child $pid 's status: $status res: $res\n";
                    unset($childs[$key]);
                }
            }
        
            sleep(1);
        }
    }
}

Pcntl_Test::forkMultiTest();

1、运行结果

php multi_pcntl.php
parent 0 [pid=64205] fork child 64206
parent 1 [pid=64205] fork child 64207
child 64206 is runing...
parent 2 [pid=64205] fork child 64208
*******************************
child 64208 is runing...
child 64207 is runing...
child 64206 is done
child 64207 is done
Parent get child 64206 's status: 0 res: 64206
child 64208 is done
Parent get child 64207 's status: 0 res: 64207
Parent get child 64208 's status: 0 res: 64208

二、原理

0、多次调用pcntl_fork,是基于同一个父进程创建的子进程。

1、我们看到代码并不是顺序执行的,*号被提前打印了。如果把子进程sleep时间统一10s, 会发现先fork的进程并不一定先结束。

2、pcntl_waitpid等待或返回子进程的状态。

3、子进程执行后,需要调用exit, 不然子进程会fork自己的子进程。

三、思考

0、POSIX扩展中有获取进程ID的方法posix_getpid,PCNTL为啥没有?
1、PHP本身也有方法getmypid获取进程ID,为啥需要这么多?

参考:
《UNIX环境高级编程》
PHP多进程之pcntl扩展的使用
PHP中多进程控制之pcntl扩展详解
PHP多进程之pcntl扩展的使用详解

掘金:PHP 进程及进程间通信
掘金:从0到1优雅的实现PHP多进程管理
掘金:PHP实现daemon
掘金:PHP编程中的并发
掘金:PHP 爬虫之百万级别知乎用户数据爬取与分析
sf:php爬虫:知乎用户数据爬取和分析

发表评论

电子邮件地址不会被公开。 必填项已用*标注