前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >PHP进程通信之管道与消息队列(二十三节)

PHP进程通信之管道与消息队列(二十三节)

作者头像
老李秀
发布2020-03-11 10:43:10
1.4K0
发布2020-03-11 10:43:10
举报

我已经猛灌了两大口恒河水,当然了并不是为了来生做印度人,而是为了这个周末将《PHP网络编程》结束撒花。

为啥最后结尾突然开始介入进程间通信了?因为我这是强行按照《UNIX网络编程》的节奏来的。其实Workerman里我几乎没有到与进程间通信的相关内容,swoole里倒是不少,当然这地方就涉及到二者进程模型的不同了。如果说了解了进程间通信,就可以考虑魔改Workerman了,比如多搞出一组task进程出来。

众所周知,进程之间数据几乎都是相互隔离的,独自享用内存空间所以进程之间如果想飞数据,就只能靠进程间通信,人称IPC,全称InterProcess Communication。进程间通信也就那几个套路,一般面试官问来问去的,虽然平时工作中几乎不用:

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

总之你们不要想太多,没啥好高深的,就是为了让进程之间彼此蹭蹭交换数据,没别的目的。


管道

管道是我们平时最常见进程间通信方法,一般说有全双工、半双工之说,全双工管道是说管道上的信息可以有来有往,半双工管道则是指只能传递单方向的数据,在APUE里这一部分涉及到的内容十分复杂繁琐,这些东西PHP看在眼里疼在蛋上,立志要为大家化「繁琐为简单」。先说下这个叫做posix_mkfifo()的函数,FIFO有些地方叫命名管道,本质上TA是一个文件,你可以用var_dump()来检验一下,FIFO是支持双向通信的:

代码语言:javascript
复制
<?php
// 管道文件绝对路径
$pipe_file = __DIR__.DIRECTORY_SEPARATOR.'test.pipe';
// 如果这个文件存在,那么使用posix_mkfifo()的时候是返回false,否则,成功返回true
if( !file_exists( $pipe_file ) ){
    if( !posix_mkfifo( $pipe_file, 0666 ) ){
        exit( 'create pipe error.'.PHP_EOL );
    }
}
// fork出一个子进程
$pid = pcntl_fork();
if( $pid < 0 ){
    exit( 'fork error'.PHP_EOL );
} else if( 0 == $pid ) {
    // 在子进程中,打开命名管道,并写入一段文本
    $file = fopen( $pipe_file, "w" );
    fwrite( $file, "I am children." );
    fclose( $file );
    // 然后以读方式再次打开管道文件,并从中读取数据
    $file = fopen( $pipe_file, "r" );
    // ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
    // fread 管道上
    $content = fread( $file, 1024 );
    echo $content.PHP_EOL;
    exit;
} else if( $pid > 0 ) {
    // 在父进程中,打开命名管道,然后读取文本
    $file = fopen( $pipe_file, "r" );
    $content = fread( $file, 1024 );
    echo $content.PHP_EOL;
    fclose( $file );
    // 以写方式打开管道,向其中写数据
    $file = fopen( $pipe_file, "w" );
    fwrite( $file, "I am father." );
    fclose( $file );
    // 注意此处再次阻塞,等待回收子进程,避免僵尸进程
    pcntl_wait( $status );
}

管道这玩意一旦创建后准备投产使用,那么使用的时候一定必须是「一读一写」要齐全,不然有一方就会陷入无限等待中,举个例子:

代码语言:javascript
复制
<?php
// 管道文件绝对路径
$pipe_file = __DIR__.DIRECTORY_SEPARATOR.'test.pipe';
// 如果这个文件存在,那么使用posix_mkfifo()的时候是返回false,否则,成功返回true
if( !file_exists( $pipe_file ) ){
    if( !posix_mkfifo( $pipe_file, 0666 ) ){
        exit( 'create pipe error.'.PHP_EOL );
    }
}
// fork出一个子进程
$pid = pcntl_fork();
if( $pid < 0 ){
    exit( 'fork error'.PHP_EOL );
} else if( 0 == $pid ) {
    $pid = posix_getpid();
    // 在子进程中,以读方式打开命名管道
    echo "{$pid} child before fopen FIFO".PHP_EOL;
    $file = fopen( $pipe_file, "r" );
    echo "{$pid} child after fopen FIFO".PHP_EOL;
} else if( $pid > 0 ) {
    // 在父进程中,打开命名管道,然后读取文本
    echo "父进程等待读取数据".PHP_EOL;
}

你们猜子进程会咋样,你们可以跑一下然后再配合grep查看一下子进程状态,然后思考下。紧接着再做个改动:往父进程里添加一行代码,注意就是第25行

代码语言:javascript
复制
<?php
// 管道文件绝对路径
$pipe_file = __DIR__.DIRECTORY_SEPARATOR.'test.pipe';
// 如果这个文件存在,那么使用posix_mkfifo()的时候是返回false,否则,成功返回true
if( !file_exists( $pipe_file ) ){
    if( !posix_mkfifo( $pipe_file, 0666 ) ){
        exit( 'create pipe error.'.PHP_EOL );
    }
}
// fork出一个子进程
$pid = pcntl_fork();
if( $pid < 0 ){
    exit( 'fork error'.PHP_EOL );
} else if( 0 == $pid ) {
    $pid = posix_getpid();
    // 在子进程中,打开命名管道,并写入一段文本
    echo "{$pid} child before fopen FIFO".PHP_EOL;
    $file = fopen( $pipe_file, "r" );
    echo "{$pid} child after fopen FIFO".PHP_EOL;
} else if( $pid > 0 ) {
    // 在父进程中,打开命名管道,然后读取文本
    // ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
    // 这里也是以 r 方式打开的管道,而不是 w
    echo "父进程等待读取数据".PHP_EOL;
    $file = fopen( $pipe_file, "r" );
}

运行一看试试?然后你再将25行的" r "模式修改为" w "模式再试试。这个是非常简单,总之再使用FIFO的时候一定是「一读一写」同时是要配对存在才是正确的用法,如果缺少一个总是会有各种奇怪的现象,再PHP这里表现为进程会阻塞在fopen操作上(纠错:在Advanced-PHP里我错误地认为是阻塞在fread上)。

除了posix_mkfifo()外,PHP里还有一个叫做popen()的函数,原型是popen ( string $command , string $mode )。前者呢本质上说是我们自己手动显示地创建一个管道,然后针对这个管道进行读写操作;后者实际上替我们屏蔽了「创建管道」这个操作,而是隐藏替我们完成了,TA的工作原理是这样的,popen首先执行fork操作,然后在子进程中exec参数中的$command同时向我们返回一个文件指针,而管道就已经在执行popen这一步的过程中已经被「隐式」地创建完成了,下面一坨demo你们感受一下:

代码语言:javascript
复制
<?php
$handle = popen('ls -l', 'r');
$read = fread($handle, 2096);
echo $read;
pclose($handle);

上面demo的意思非常简单,就是创建一个读取类型的管道,这个管道从可以读取到" ls -l "命令的执行结果,只是这个管道是个单向的。这个函数的好处就是帮我们屏蔽掉了手工创建管道的操作,可惜只能是半双工,如果你想要全双工版本的popen,那么下面这个proc_open()函数将会拍的上场,这个函数除了可以创建全双工管道外,还额外提供了大量控制配置参数。

代码语言:javascript
复制
<?php
// 这个数组是描述选项,它的构成是这样的
// 它的索引是文件描述符
// 它的索引对应的值是一个数组,数组的第一个元素有两个可选值pipe或文件
// 数组的第二个元素就是r w 或者a mode
// 下面的case里,众所周知
// 0表示标准输入
// 1表示标准输出
// 2表示标准错误
// 任何一个进程打开后,默认都会打开0 1 2三个文件描述符
// 这里通过a_pipe_desc将新进程默认打开的0 1 2文件描述
// 指向自己配置的pipe管道和file文件
// 你还可以自己手动往数组里添加新的文件描述符
$a_pipe_desc = array(
    0 => array("pipe", "r"),
    1 => array("pipe", "w"),
    2 => array("file", "./debug.log", "a"),
    // 比如,你还有有一个文件描述符5
    // 你想让5为一个file
    //5 => array("file", "./test.log", "a"),
);
// 这个测试PHP程序的工作目录,我设置为当前了
$s_cwd = './';

// 这个管道就是在「PHP程序」与「bash程序」之间
// 这个管道是双向的,管道就在$a_pipes中
$r_process = proc_open('bash', $a_pipe_desc, $a_pipes, $s_cwd, NULL);

// 可以打印一下看看
print_r( $a_pipes );

// 而通过proc_get_status可以获取「PHP程序」
// 打开的子进程「bash」的相关信息
$a_process_info = proc_get_status($r_process);
print_r( $a_process_info );

// 啥意思呢?就是说:
// PHP程序向$a_pipes[0]中写内容,而bash从$a_pipes[0]中读内容
// PHP程序从$a_pipes[0]中读内容,而bash向$a_pipes[1]中写内容
// 而错误将会被记录到
fwrite($a_pipes[0], 'ls -l');
fclose($a_pipes[0]);
echo stream_get_contents($a_pipes[1]);
fclose($a_pipes[1]);
// 一定要及时关闭不用的管道,正如前面posix_mkfifo()演示的那样
// 管道如果处理不好,很容易让程序陷入无限等待中,出现异常
proc_close($r_process);

所以简单总结一下PHP语言中的管道:

  • posix_mkfifo():手工显示创建一个全双工管道,操作上可以细腻,使用上需要注意「锁」的问题
  • popen():隐式创建半双工管道,代码使用上比较简单
  • proc_open():隐式创建全双工管道,还有众多的控制细节

消息队列

这个怕是很多人都听过,不过印象往往停留在kafka、rabbitmq之类的用于业务解耦的网络消息队列软件上。然而这里的消息队列是说操作系统中内置的一种数据结构,消息队列是消息的链接表(一种常见的数据结构),但是这种消息队列存储于系统内核中(不是用户态),一般我们外部程序使用一个key来对消息队列进行读写操作,在PHP中,是通过msg_*系列函数完成消息队列操作的。

这种消息队列的状态是由操作系统来维护的,每个消息队列在操作系统内部都有一个标志符,但是这种标志符是操作系统内部使用的,在外我们使用的则是消息队列的ID或者KEY,而这个ID或KEY的生成方式可以使用ftok()函数;除此之外,既然这种消息队列是系统维护的,所以理论上只要外界程序知道这个消息队列的ID或KEY,那么跨语言之间也可以通过这个消息队列进行通信,比如使用PHP向消息队列中写入数据,使用Python语言从消息队列中读取消息。

下面这坨代码是「父进程」与「子进程」间利用消息队列互飞数据:

代码语言:javascript
复制
<?php
// 使用ftok创建一个键名,注意这个函数的第二个参数“需要一个字符的字符串”
$key = ftok( __DIR__, 'a' );
// 然后使用msg_get_queue创建一个消息队列
$queue = msg_get_queue( $key, 0666 );
// 使用msg_stat_queue函数可以查看这个消息队列的信息,而使用msg_set_queue函数则可以修改这些信息
//var_dump( msg_stat_queue( $queue ) );
// fork进程
$pid = pcntl_fork();
if( $pid < 0 ){
    exit( 'fork error'.PHP_EOL );
} else if( $pid > 0 ) {
    // 在父进程中
    // 使用msg_receive()函数获取消息
    msg_receive( $queue, 0, $msgtype, 1024, $message );
    echo $message.PHP_EOL;
    // 用完了记得清理删除消息队列
    msg_remove_queue( $queue );
    pcntl_wait( $status );
} else if( 0 == $pid ) {
    // 在子进程中
    // 向消息队列中写入消息
    // 使用msg_send()向消息队列中写入消息,具体可以参考文档内容
    msg_send( $queue, 1, "helloword" );
    exit;
}

然后老李亲手再给你表演一下利用消息队列实现跨语言进程通信,就Python吧,用Python读取,用PHP写入,我告诉你别小瞧你李哥,你李哥活儿全:

代码语言:javascript
复制
<?php
// 使用ftok创建一个键名,注意这个函数的第二个参数“需要一个字符的字符串”
$key = ftok( "/Users/didi/python", "a" );
// 然后使用msg_get_queue创建一个消息队列
$queue = msg_get_queue( $key, 0666 );
// 使用msg_stat_queue函数可以查看这个消息队列的信息,而使用msg_set_queue函数则可以修改这些信息
//var_dump( msg_stat_queue( $queue ) );
// 向消息队列中写入消息
// 使用msg_send()向消息队列中写入消息,具体可以参考文档内容
msg_send( $queue, 1, "helloword" );
代码语言:javascript
复制
#coding:utf-8
import sysv_ipc

# Main
msg_queue_key = sysv_ipc.ftok( "/Users/didi/python", 97 )
msg_queue     = sysv_ipc.MessageQueue( msg_queue_key, sysv_ipc.IPC_CREAT, 0666 )
content = msg_queue.receive()
print( content )

上述Pyton与PHP这个案例里,ftok这里可能大家会有些疑惑,为什么PHP第二个参数是字母a,而Python里是数字97,实际上我这里得说一下,咱们来把老祖宗的标准先拿出来,在XSI标准里,粗暴点儿说就是你在*NIX下搞系统级编程,C语言提供的ftok函数实际上第二个参数确实是个整形数字,范围是0-255,我也不知道PHP为啥用字母;如果你搞过C,你应该知道实际上在C里字符本质上是数字,确切说字母a就是ASCII的数字97,明白了吧。

之所以写这个demo,还是想以前经常强调的那个中心思想,别老折腾语言表层的玩意折腾来折腾去的,贼没意思贼没劲,更别参与语言争论,一地鸡毛没有任何收获。好好把底层夯实了,语言本身是工具,你要真有劲好好把POSIX.1标准API编程搞一搞,好好研究研究操作系统原理,一天天地连怼都怼不到点上:

你喷我环境难搞,我怼你依赖乱跑

你骂我性能垃圾,我叱你乱吹牛逼

你夸你语法优雅,我赞我是一朵花

你评你生态完善,我论我未来美好 顺带批判时政,下班一地鸡毛

去瞅瞅你加的各种「技术大佬」群里是不是天天就这些内容?总结四个字:

积沙成雕

还别批判人家「一个人事一天天就不干人事」,我还得说说你「一个开发一天天就不干开发」。

  • 哎吆老李,蹭热点?
  • ...不敢不敢,我哪儿敢蹭热点
  • 得了吧,蹭蹭就蹭蹭,没事儿
  • 不敢蹭蹭,怕擦枪走火
  • 你又不进来你怕啥?...
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-03-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 高性能API社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
消息队列 CMQ 版
消息队列 CMQ 版(TDMQ for CMQ,简称 TDMQ CMQ 版)是一款分布式高可用的消息队列服务,它能够提供可靠的,基于消息的异步通信机制,能够将分布式部署的不同应用(或同一应用的不同组件)中的信息传递,存储在可靠有效的 CMQ 队列中,防止消息丢失。TDMQ CMQ 版支持多进程同时读写,收发互不干扰,无需各应用或组件始终处于运行状态。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档