前面几篇文章,我们介绍了PHP的多进程创建及一些进程的特殊状态。今天这篇文章我们来讲解下进程间的通信。

背景

前面我们也提到过,在 fork 执行之后,父进程和子进程的内存空间是独立的了,所以它们是无法直接进行通信的。但是复杂的应用需求要求我们需要父进程与子进程可以相互通信,了解各自的状态好协调运行完成任务。

通信的方式

在 linux 中,进程间的通信主要有下面几种:

  • 管道
  • 消息队列
  • 共享内存
  • 信号量
  • socket
  • ...

阅读完本篇文章之后,建议小伙伴搜索上述的资料看看。

共享内存通信

本篇文章,我们就来讲解下共享内存的通信。首先,我们来分析下共享内存通信的优缺点。最大的优点就是简单快速,缺点也很明显,存在竞争情况,多进程的共享访问会导致很多意外情况的发生。这个我们也会在接下来会涉及到。

在 PHP 中我们可以借助 Semaphore 扩展实现共享内存通信。这个扩展默认是直接安装的,所以不需要我们额外安装啦。我们来看下下面代码,结合代码一起分析下。

执行 php -m |grep sysvsem 查看是否安装了扩展。

PHP 共享内存通信

<?php
pcntl_async_signals(true);

$key = 1;
// 10kb
$size = 1024 * 10;
$perm = 0666;

$pid = pcntl_fork();
if ($pid === -1) {
    exit('无法创建子进程');
} elseif ($pid > 0) {
    // 父进程
    $pid = getmypid();
    echo "父进程({$pid}):我开始运行了\n";
    // 创建一个共享内存
    $mem = shm_attach($key, $size, $perm);
    // 注册信号
    pcntl_signal(SIGINT, function () use ($mem) {
        pcntl_wait($status);
        echo "子进程已退出,状态:{$status}\b";
        shm_remove($mem);
        exit;
    });
    if ($mem === false) {
        exit('共享内存区创建失败');
    }
    while (1) {
        echo "父进程:写入当前时间到共享内存区\n";
        shm_put_var($mem, 10, date('Y-m-d H:i:s'));
        sleep(3);
    }
} else {
    // 这里是子进程
    $pid = getmypid();
    echo "子进程({$pid}):我开始运行了\n";
    // 创建一个共享内存
    $mem = shm_attach($key, $size, $perm);
    // 注册信号
    pcntl_signal(SIGINT, function () use ($mem) {
        echo "子进程:收到ctrl+c命令,我要停止啦\n";
        shm_detach($mem);
        exit;
    });
    while (1) {
        if (shm_has_var($mem, 10)) {
            echo shm_get_var($mem, 10),"\n";
        } else {
            echo "变量不存在\b";
        }
        sleep(3);
    }
}

简单的看完代码之后,我们来简单的分析下。

首先,fork 一个子进程出来,然后,在父进程通过函数 shm_attach 创建一块共享内存,同样,在子进程也通过该函数创建一块共享内存,可以看到的是,父进程和子进程调用 shm_attach 函数的参数都是一样的,所以它们虽然分别在自己的进程中调用了,但是它们访问的是同一块内存资源,也就共享内存!!!

shm_attach() 第二个参数是指定共享内存的大小单位是字节,第三个参数指定的是共享内存的权限,默认是 666 。这个权限怎么来的呢,其实我猜应该和 linux 的文件权限同出一源,在 linux 中 4=可读,2=可写 => 6=可读可写。

然后,父进程会有个循环,每3秒通过函数 shm_put_var() 写出变量值到共享内存区。该函数第一个参数是上面 shm_attach() 的返回值,也就是 resource 类型;第二个参数是变量名,需要注意的是,这里的变量名必须是 int 类型;第三个参数是变量值,支持 PHP 默认的所有变量类型(或者说可以执行 serialize() 序列化的变量都可以。)

在子进程中,也有一个循环,每3秒从共享内存中读取变量值,也就是通过函数 shm_get_var() ,参数同上。其中通过 shm_has_var() 这个函数来检测共享内存区是否含有当前变量,参数同上。

到这里的话,我们就学习到了四个共享内存的操作函数:

# 创建共享内存区块
shm_attach()
# 写入变量
shm_put_var()
# 读取变量
shm_get_var()
# 检测变量
shm_has_var()

在程序里面,同时我们也定义了信号的处理,也就是我们通过 pcntl_signal() 函数注册了 SIGINT 信号(也就ctrl+c),当在命令行终止的时候,我们做一下资源的清除动作,也就是通过函数 shm_detach() 来断开与共享内存区的连接(也就是删除引用)。最终,在父进程中 pcntl_wait() 等待子进程完全退出,调用 shm_remove() 函数彻底的删除共享内存区块。

到这里,程序的逻辑就完成了。我们来看下输出的结果:

父进程(17781):我开始运行了
父进程:写入当前时间到共享内存区
子进程(17782):我开始运行了
2020-06-14 02:31:31
父进程:写入当前时间到共享内存区
2020-06-14 02:31:34
2020-06-14 02:31:34
父进程:写入当前时间到共享内存区
2020-06-14 02:31:37
父进程:写入当前时间到共享内存区
^C子进程:收到ctrl+c命令,我要停止啦
子进程已退出,状态:0

从结果中可以看到,父进程和子进程可以正常的通信。这就是一个简单的共享内存通信的示例。

现在我们需要思考几个问题:

  • 如果多个进程同时给某一个变量写入不同的值,会产生什么样的情况?
  • 如何才能保证消费进程读取的变量值是最新的?

上述问题,我们将会在下一篇文章解答。