php 多进程

前言

最近在工作中遇到了一个php多进程的问题,脚本我是能写的,但是没有深入的了解过相关的内容。这篇就是对php处理多进程的一个简单的整理吧。
一搬来说,php都是一个单进程执行的,为了应对web并发请求的问题,我们借助服务器或PHP-FPM实现处理。但php的另一种使用场景是在cli模式下跑脚本或daemon来进行数据处理,这种情况下,就需要php自身对多进程的支持。
我们知道php是有很多扩展的,实现php的多进程主要依赖pcntl和posix两种扩展。下面主要针对这两种扩展进行介绍。

多进程处理扩展

php和进程相关的扩展其实很多的,这些扩展目前来说可以解决我们大多数对进程控制的场景,但很多我们并不经常用。经常用到的主要就是两个,一个PCNTL,一个POSIX。如果想了解的更多,可以参考进程控制扩展

pcntl扩展

参考:PCNTL官方说明

扩展说明

pcntl扩展提供了一系列进程控制函数来实现进程创建、程序执行、信号处理及进程中断处理。但是这类方法不能用于web服务器环境。

主要函数

pcntl控制函数很多,本人基于个人认知,大致分为了一下几类,会重点介绍其中的几个。

  1. 进程控制
  • pcntl_exec( string $path [, array $args [, array $envs ]] )在当前进程空间执行指定程序
    • path:可执行文件路径
    • args:传递给程序的参数的字符串数组
    • envs:要传递给程序作为环境变量的字符串数组
  • pcntl_fork():创建一个子进程(从当前位置往后是子进程内容),子进程仅pid和父进程不同
    返回值:0(当前在子进程中);-1(创建进程失败);>0,在父进程中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?php
    $pid = pcntl_fork();
    //父进程和子进程都会执行下面代码
    if ($pid == -1) {
    //错误处理:创建子进程失败时返回-1.
    die('could not fork');
    } else if ($pid) {
    //父进程会得到子进程号,所以这里是父进程执行的逻辑
    pcntl_wait($status); //等待子进程中断,防止子进程成为僵尸进程。
    } else {
    //子进程得到的$pid为0, 所以这里是子进程执行的逻辑。
    }
    ?>
  • pcntl_getpriority:查询指定pid进程的优先级
    int pcntl_getpriority ([ int $pid = getmypid() [, int $process_identifier = PRIO_PROCESS ]] )

  • pcntl_setpriority: 设置指定pid进程的优先级
    bool pcntl_setpriority ( int $priority [, int $pid = getmypid() [, int $process_identifier = PRIO_PROCESS ]] )
  • pcntl_wait:等待或返回fork的子进程状态
    int pcntl_wait ( int &$status [, int $options = 0 ] )
    挂起当前进程的执行直到一个子进程退出或接收到一个信号要求中断当前进程或调用一个信号处理函数。
  • pcntl_waitpid:挂起当前进程的执行直到参数pid指定的进程号的进程退出, 或接收到一个信号要求中断当前进程或调用一个信号处理函数
    如果pid指定的子进程在此函数调用时已经退出(俗称僵尸进程),此函数 将立刻返回
  1. 信号处理
  • pcntl_alarm(int $seconds):为进程创建闹钟信号,在指定的秒数后向进程发送一个SIGALRM信号
  • pcntl_async_signals(bool $on = NULL ] )启用/禁用异步信号处理,如果不传参数,返回是否启用了异步信号处理
  • pcntl_signal_dispatch:调用等待信号的处理器,等待信号的处理器使用pcntl_signal() 创建安装
  • pcntl_signal_get_handler:获取指定信号的当前处理程序
  • pcntl_signal:为指定编号的信号安装一个信号处理器
    bool pcntl_signal ( int $signo , callback $handler [, bool $restart_syscalls = true ] )
    • signo 信号编号
    • handler 信号处理器(程序)可以是用户创建的函数或方法的名字,也可以是系统常量 SIG_IGN(译注:忽略信号处理程序)或SIG_DFL(默认信号处理程序).
    • restart_syscalls:指定当信号到达时系统调用重启是否可用(详见官网说明)
  • pcntl_sigprocmask:用来增加,删除或设置阻塞信号
  • pcntl_sigtimedwait:带超时机制的信号等待
  • pcntl_sigwaitinfo:暂停调用脚本的执行直到接收到set 参数中列出的某个信号,
  1. 其他:
  • pcntl_errno:函数pcntl_get_last_error()的别名
  • pcntl_get_last_error:返回最近一次pcntl函数错误的错误码
  • pcntl_strerror:根据错误编号获取系统错误描述
  • pcntl_wexitstatus:返回一个中断的子进程的返回代码
  • pcntl_wifexited:检查子进程状态代码是否代表正常退出。
  • pcntl_wifsignaled:检查子进程是否是由于某个未捕获的信号退出的。
  • pcntl_wifstopped:检查子进程当前是否已经停止
  • pcntl_wstopsig:返回导致子进程停止的信号
  • pcntl_wtermsig:返回导致子进程中断的信号

应用实例

如果不需要涉及到信号量处理相关的内容,那么一个最简单的php多进程脚本代码大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
for ($i=0; $i<3; ++$i){
$pids[$i] = pcntl_fork();
if ($pid[$i] == -1){
die ("cannot fork" );
}
else if ($pid > 0){
echo "parent continue \n";
}
else if ($pid == 0)
{
echo "child start, pid ", getmypid(), "\n" ;
exit (0);
}
}
//对进程进行回收
foreach ($pids as $pid)
{
if($pid) {
pcntl_waitpid($pid, $status);
}
}

特别说明

在php多进程脚本中,有几个需要重点理解的点:

  1. pcntl_fork的返回值:>0,在父进程中;=0,创建子进程成功,进入子进程;<0(就是-1)创建失败
  2. pcntl_wait和pcntl_waitpid的区别:二者都是挂起进程直到进程退出或中断后进行后续,但是pcntl_wait无法指定pid,所以必须在创建子进程的循环中出现,但是这样会造成脚本变为串行,所以可以使用pcntl_waitpid,在创建完所有的子进程后,对进程进行回收,防止出现僵尸进程(也可能和参数有关,后续调研)
  3. 子进程执行的代码是pcntl_fork之后的代码

posix扩展

参考

扩展说明

POSIX表示可移植操作系统接口(Portable Operating System Interface of UNIX,缩写为 POSIX ),POSIX标准定义了操作系统应该为应用程序提供的接口标准,是IEEE为要在各种UNIX操作系统上运行的软件而定义的一系列API标准的总称.
posix扩展包含了定义在 IEEE 1003.1(POSIX.1)标准文档里的函数的接口,(windows不可用,无检测,不需要的情况下建议关闭)
所以posix扩展中的一些函数通常会与pcntl扩展配合使用。

主要函数

posix_access(string $file[, int $mode = POSIX_F_OK]) :查看用户对文件是否具有指定的权限
$file - 文件名
$mode - 权限,包含:POSIX_F_OK, POSIX_R_OK, POSIX_W_OK, POSIX_X_OK的一个或多个。
posix_ctermid():返回当前进程所在的当前控制终端的路径名
posix_errno():posix_get_last_error()别名
posix_get_last_error():返回最近一次最后的posix函数调用失败的错误号。错误号关联的错误消息,可通过 ‘posix_strerror()’ 来获取
posix_strerror(int $errno):通过给定的错误号,返回关联的POSIX系统错误消息
posix_getcwd():取当前脚本的工作目录的绝对路径
posix_getegid()返回当前进程的有效用户组ID
posix_geteuid()返回当前进程的有效用户ID
posix_getgid()返回当前进程的真实用户组ID
posix_getuid():返回当前进程的真实用户ID
上述四个方法中有效和真实的含义参见inux进程的实际用户ID和有效用户ID
posix_getgrgid(int $gid)通过传入组ID,获取给定的用户组的相关信息,返回值如下

1
2
3
4
5
6
7
8
9
10
11
12
Array
(
[name] => staff
[passwd] => *
[members] => Array
(
[0] => root
[1] => muse
)
[gid] => 20
)

posix_getgrnam(string $name):通过传入组名称,获取给定的用户组的相关信息,返回值同上
posix_getgroups():获取当前进程的用户组集合
posix_getlogin():返回拥有当前进程的用户的登陆名
posix_getpgid(int $pid):获取指定进程的进程组标识符(进程组id),返回整数
注意:
该函数不是POSIX函数,但是常见于BSD和System V的系统上。如果系统不支持该函数,在编译时就不会被包含进来。应该提前使用 ‘function_exists()’ 检查,存在再使用
posix_getpgrp():获取当前进程的进程组标识符(进程组id),返回整数
posix_getpid():获取当前进程的进程标识符(进程id)
posix_getppid():获取当前进程的父进程标识符(父进程id)
posix_getpwnam(string $username):通过用户名,获取给定用户的信息。返回值如下:

1
2
3
4
5
6
7
8
9
10
Array
(
[name] => jack //用户名,和传入参数一致,太长的话会被截断,保留16个字符
[passwd] => ******** //密码
[uid] => 502 //用户id
[gid] => 20 //用户组id
[gecos] => "jack,," //一个过时的元素,包含了 ','分隔的用户的全名、办公室电话、办公室号码以及家庭电话号码。大多数的系统上,只有用户的全名有效。
[dir] => /Users/tom //用户根目录
[shell] => /bin/bash //可执行的用户的默认shell的绝对路径
)

posix_getpwuid(int $uid):通过用户id,获取给定用户的信息,返回结果同上
posix_getrlimit():返回一个关于当前资源的软限制和硬限制的信息数组,软硬限制可查询百度
posix_getsid(int $pid):返回指定进程的session ID。进程的session ID是会话领导者(session leader)的进程组id
posix_initgroups(string $name, int $base_group_id):对指定的用户,计算其组访问列表
posix_isatty(mixed $fd):检查文件描述符是否是一个有效的终端类型的设置(是否是tty)
posix_kill(int $pid, int $sig):给指定的进程发送一个$sig指定的信号!
参数:
$pid - 进程id
$sig - PCNTL信号预定义常量
posix_mkfifo(string $pathname, int $mode):创建一个特殊的FIFO文件,存在于文件系统,并且作为进程的双向通信桥梁(其实就是创建一个先入先出的管道,可以使用文件操作方法写入和读取文件)
参数:
$pathname - FIFO文件(管道)
$mode - 必须是8进制格式。新创建的FIFO的权限,也依赖于当前的umask()设置。新创建的文件权限是(mode & ~umask)
posix_mknod(string $pathname, int $mode[, int $major = 0[, int $minor = 0]]):创建一个特殊的或者一般的文件
posix_setegid(int $gid):设置当前进程的有效组ID。这是个特权函数,需要操作系统上具有特殊权限(通常是root权限),才能执行该函数。
posix_seteuid(int $uid):设置当前进程的有效用户ID。这是个特权函数,需要操作系统上具有特殊权限(通常是root权限),才能执行该函数。
posix_setgid(int $gid):设置当前进程的真实用户组ID。这是个特权函数,需要操作系统上具有特殊权限(通常是root权限),才能执行该函数。函数调用的适当的顺序是:首先调用 posix_setgid(),最后调用 posix_setuid()。
posix_setpgid(int $pid, int $pgid):设置指定进程的进程组ID
posix_setrlimit(int $resource, int $softlimit, int $hardlimit):设置给定系统资源的软限制和硬限制。
posix_setsid():设置当前进程为session leader(会话领导者)
posix_setuid(int $uid):设置当前进程的真实用户ID。这是个特权函数,需要操作系统上具有特殊权限(通常是root权限),才能执行该函数。
posix_times():获取当前CUP使用信息。警告:函数不可靠,对于高时间(high times)可能返回负值。

1
2
3
4
5
6
7
8
Array
(
[ticks] => 152559307291 //重启到现在,已经过去的 clock ticks 个数
[utime] => 5 //当前进程使用的用户时间
[stime] => 6 //当前进程使用的系统时间
[cutime] => 0 //当前进程和子进程使用的用户时间
[cstime] => 0 //当前进程和子进程使用的系统时间
)

posix_ttyname($mixed $fd):返回当前打开的文件描述符所在的终端设备的绝对路径
posix_uname():获取系统相关信息(同uname -a 命令)。

Array
(
    [sysname] => Darwin  //操作系统名,如Linux
    [nodename] => localhost  //系统名称
    [release] => 16.0.0  //操作系统发布版
    [version] => Darwin Kernel Version 16.0.0: Mon Aug 29 17:56:20 PDT 2016; root:xnu-3789.1.32~3/RELEASE_X86_64//操作系统版本
    [machine] => x86_64   //系统平台
    [domainname] => (none) //dns域名
)

参考

1.PHP多进程编程
2.从0到1优雅的实现PHP多进程管理
3.PHP多进程实践
4.POSIX函数
5.POSIX百度百科