/首页
/开源
/关于
PHP进程通信之管道与消息队列(二十三节)
发表@2020-03-07 16:47:06
更新@2023-04-27 15:07:23
大家好,我是老李,号「谢顶道人」。 我已经猛灌了两大口恒河水,当然了并不是为了来生做印度人,而是为了这个周末将《PHP网络编程》结束撒花。 为啥最后结尾突然开始介入进程间通信了?因为我这是强行按照《UNIX网络编程》的节奏来的。其实Workerman里我几乎没有到与进程间通信的相关内容,swoole里倒是不少,当然这地方就涉及到二者进程模型的不同了。如果说了解了进程间通信,就可以考虑魔改Workerman了,比如多搞出一组task进程出来。 众所周知,进程之间数据几乎都是相互隔离的,独自享用内存空间所以进程之间如果想飞数据,就只能靠进程间通信,人称IPC,全称InterProcess Communication。进程间通信也就那几个套路,一般面试官问来问去的,虽然平时工作中几乎不用: - 管道 - 消息队列 - 共享内存 - 信号量 - unix socket 总之你们不要想太多,没啥好高深的,就是为了让进程之间彼此蹭蹭交换数据,没别的目的。 ### 管道 管道是我们平时最常见进程间通信方法,一般说有全双工、半双工之说,全双工管道是说管道上的信息可以有来有往,半双工管道则是指只能传递单方向的数据,在APUE里这一部分涉及到的内容十分复杂繁琐,这些东西PHP看在眼里疼在蛋上,立志要为大家化「繁琐为简单」。先说下这个叫做posix_mkfifo()的函数,FIFO有些地方叫命名管道,本质上TA是一个文件,你可以用var_dump()来检验一下,FIFO是支持双向通信的: ```php 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 0 ) { // 在父进程中,打开命名管道,然后读取文本 echo "父进程等待读取数据".PHP_EOL; } ``` 你们猜子进程会咋样,你们可以跑一下然后再配合grep查看一下子进程状态,然后思考下。紧接着再做个改动:往父进程里添加一行代码,注意就是第25行: ```php 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 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程序 fork一个子进程 在子进程中执行bash // 这个管道就是在「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 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