unix iorp等待队列_PHP进程通信之管道与消息队列(二十三节)

大家好,我是老李,号「谢顶道人」。

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

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

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

  • 管道

  • 消息队列

  • 共享内存

  • 信号量

  • unix socket

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


管道

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

<?php // 管道文件绝对路径$pipe_file = __DIR__.DIRECTORY_SEPARATOR.'test.pipe';// 如果这个文件存在,那么使用posix_mkfifo()的时候是返回false,否则,成功返回trueif( !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 );}

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

<?php // 管道文件绝对路径$pipe_file = __DIR__.DIRECTORY_SEPARATOR.'test.pipe';// 如果这个文件存在,那么使用posix_mkfifo()的时候是返回false,否则,成功返回trueif( !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行

<?php // 管道文件绝对路径$pipe_file = __DIR__.DIRECTORY_SEPARATOR.'test.pipe';// 如果这个文件存在,那么使用posix_mkfifo()的时候是返回false,否则,成功返回trueif( !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你们感受一下:

<?php $handle = popen('ls -l', 'r');$read = fread($handle, 2096);echo $read;pclose($handle);

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

<?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语言从消息队列中读取消息。

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

<?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写入,我告诉你别小瞧你李哥,你李哥活儿全:

<?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" );
#coding:utf-8import sysv_ipc# Mainmsg_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 )

26cbab4b0d1ba737f25f22c5e63d7ce2.png

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

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

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

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

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

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

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

积沙成雕

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

  • 哎吆老李,蹭热点?

  • ...不敢不敢,我哪儿敢蹭热点

  • 得了吧,蹭蹭就蹭蹭,没事儿

  • 不敢蹭蹭,怕擦枪走火

  • 你又不进来你怕啥?...

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值