前言
进程间通信简称 IPC,全称 InterProcess Communication。常见的进程间通信方式有:管道(分无名和有名两种)、消息队列、信号量、共享内存和socket。
管道和FIFO
管道是最初的 IPC 形式,我们平时使用命令 ps aux | grep php,这里的 | 就是管道。管道最大的局限是没有名字,从而只能由有亲缘关系的进程使用。这一点在 FIFO 出现后得到改进。因而 FIFO 有时也称为命名管道(named pipe)。管道一般是半双工的,但有些系统实现了全双工。
php 使用命名管道通信,创建一个管道的函数叫做posix_mkfifo(),管道创建完成后其实就是一个文件,然后就可以用任何与读写文件相关的函数对其进行操作了,代码大概演示一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <?php
$pipe_file = __DIR__.DIRECTORY_SEPARATOR.'test.pipe';
if( !file_exists( $pipe_file ) ){ if( !posix_mkfifo( $pipe_file, 0666 ) ){ exit( 'create pipe error.'.PHP_EOL ); } }
$pid = pcntl_fork(); if( $pid < 0 ){ exit( 'fork error'.PHP_EOL ); } else if( 0 == $pid ) { $file = fopen( $pipe_file, "w" ); fwrite( $file, "helo world." ); exit; } else if( $pid > 0 ) { $file = fopen( $pipe_file, "r" ); $content = fread( $file, 1024 ); echo $content.PHP_EOL; pcntl_wait( $status ); }
|
运行结果如下:
1 2
| $ php fifo.php hello world
|
管道的唯一限制为:
OPEN_MAX 一个进程在任意时刻打开的最大描述符数(Posix 要求至少为16);
PIPE_BUF 可原子地写往一个管道或 FIFO 的最大数据量(Posix 要求至少为512)
消息队列
这里的消息队列是存储于系统内核中(不是用户态)的一个链表,因而在一个进程发出消息时,不需要另外某个进程等待,这与管道相反。一般我们外部程序使用一个key来对消息队列进行读写操作。在PHP中,是通过msg_*系列函数完成消息队列操作的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <?php
$key = ftok( __DIR__, 'a' );
$queue = msg_get_queue( $key, 0666 );
$pid = pcntl_fork(); if( $pid < 0 ){ exit( 'fork error'.PHP_EOL ); } else if( $pid > 0 ) { msg_receive( $queue, 0, $msgtype, 1024, $message ); echo $message.PHP_EOL; msg_remove_queue( $queue ); pcntl_wait( $status ); } else if( 0 == $pid ) { msg_send( $queue, 1, "hello world" ); exit; }
|
运行结果如下:
1 2
| $ php msg.php hello world
|
同步与信号量
为了同步多个进程的活动,就要允许在进程间共享数据。如果要多个进程读写同一个数据,就要引入锁。在多线程的情况下,本身有共享数据缓冲区,上锁与解锁非常简单。对于多进程上锁与解锁,可以使用信号量(semaphore)。
对于多线程,php 有 pthreads 扩展,不过这个扩展需要将 php 编译成线程安全(ZTS)版本,具体参考其 github 页面。这里给出一个 pthreads 互斥锁(mutex)和条件等待(cond)的演示,但是注意,这两个类在最新版里已经删除,新版使用 synchronized 函数。这里之所以还使用旧版,是因为旧版更接近原 c 语言的用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <?php
$mutex = Mutex::create(); $cond = Cond::create(); $condition = false; function produce() { global $condition,$mutex,$cond; Mutex::lock($mutex); echo "pth2\n"; $condition = true; Cond::signal($cond); Mutex::unlock($mutex); }
function comsume() { global $condition,$mutex,$cond; Mutex::lock($mutex); while(!$condition){ Cond::wait($cond, $mutex); } echo "pth1\n"; Mutex::unlock($mutex); }
comsume();
produce();
Cond::destroy($cond);
Mutex::unlock($mutex);
Mutex::destroy($mutex);
|
运行结果如下,pth2永远在pth1前,即两个线程通过 mutex 和 cond 的结合使其线程间同步
题外话:如果没有 mutex,signal 可能在 wait 之前执行,这样 wait 永远等不到 signal。mutex 和 cond 都是锁死等待,之所以需要 cond 是因为 Cond::wait() 后线程会释放锁,进入休眠,不再循环判断条件。在Cond::wait() 释放 mutex 之前,线程依靠 while() 保证程序不会执行到 echo。
对于信号量,php 提供 sem_acquire(), sem_get(), sem_release(), sem_remove() 4个函数。因为信号量一般和共享内存一起使用,所以代码在下一节共享内存中演示。
共享内存
共享内存是最快是进程间通信方式,因为n个进程之间并不需要数据复制,而是直接操控同一份数据。实际上信号量和共享内存是分不开的,要用也是搭配着用。*NIX的一些书籍中甚至不建议新手轻易使用这种进程间通信的方式,因为这是一种极易产生死锁的解决方案。共享内存顾名思义,就是一坨内存中的区域,可以让多个进程进行读写。这里最大的问题就在于数据同步的问题,比如一个在更改数据的时候,另一个进程不可以读,不然就会产生问题。所以为了解决这个问题才引入了信号量,信号量是一个计数器,是配合共享内存使用的,一般情况下流程如下:
- 当前进程获取将使用的共享内存的信号量
- 如果信号量大于0,那么就表示这块儿共享资源可以使用,然后进程将信号量减1
- 如果信号量为0,则进程进入休眠状态一直到信号量大于0,进程唤醒开始从1
一个进程不再使用当前共享资源情况下,就会将信号量减1。这个地方,信号量的检测并且减1是原子性的,也就说两个操作必须一起成功,这是由系统内核来实现的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| <?php
$sem_key = ftok( __FILE__, 'b' ); $sem_id = sem_get( $sem_key );
$shm_key = ftok( __FILE__, 'm' ); $shm_id = shm_attach( $shm_key, 1024, 0666 ); const SHM_VAR = 1; $child_pid = [];
for( $i = 1; $i <= 2; $i++ ){ $pid = pcntl_fork(); if( $pid < 0 ){ exit(); } else if( 0 == $pid ) { sem_acquire( $sem_id ); if( shm_has_var( $shm_id, SHM_VAR ) ){ $counter = shm_get_var( $shm_id, SHM_VAR ); $counter += 1; shm_put_var( $shm_id, SHM_VAR, $counter ); } else { $counter = 1; shm_put_var( $shm_id, SHM_VAR, $counter ); } sem_release( $sem_id ); sem_remove( $sem_id ); exit; } else if( $pid > 0 ) { $child_pid[] = $pid; } } while( !empty( $child_pid ) ){ foreach( $child_pid as $pid_key => $pid_item ){ pcntl_waitpid( $pid_item, $status, WNOHANG ); unset( $child_pid[ $pid_key ] ); } }
sleep( 2 ); echo '最终结果'.shm_get_var( $shm_id, SHM_VAR ).PHP_EOL;
shm_remove( $shm_id ); shm_detach( $shm_id );
|
运行结果如下:
确切说,如果不用sem的话,上述的运行结果在一定概率下就会产生1而不是2。但是只要加入sem,那就一定保证100%是2,绝对不会出现其他数值。
php 守护进程和 socket 通信
进程间通信的前提是 php 需要是守护进程,不然还没收到信息就退出了。php 守护进程需要用到 pcntl_fork() 生成子进程。socket 通信需要用到 socket_ 系列函数。这两个参考资料中的 advanced-php 已经有详细介绍,这篇文章就不写了。也可以看官方文档了解。
参考资料:
《unix网络编程:第二卷》
advanced-php
PCNTL函数
Sockets函数
posix函数
Semaphore函数