vlambda博客
学习文章列表

PHP进程通信之共享内存+UNIX Socket(二十四节)

大家好,老李是我;我永远都爱这样的我。



有人跟我说:「老李,你再也不是以前的你了」。他说这句话的时候,我仿佛感觉到了当年马克·查普曼在一枪干掉了约翰·列侬后,对着列侬的尸体说:“ 你变了 ”...


我怎么能变呢,我还是那个老李:就算整个晋西北乱成了一锅粥,我还是我,是颜色不一样的烟火,当年那个十里八乡有名的俊后生,当年雪上草地里背过锅,二营长意大利炮扛上来,老子打得就是他精锐,别说它一个板田联队,有一个师老子就TM敢打太原。你好好看看我哪儿变了?


PHP进程通信之共享内存+UNIX Socket(二十四节)



硬核,风骚,还带着那么一丝丝小帅。就算说变,那也仅仅可能是在向「真*谢顶道人」的方向变,真到那个时候我就改号为「秃顶法师」。


恒河水一口下了肚,今天要搞一波儿流。还是有同学整不明白进程间通信的意义啊,结合WM简单说一种情况,众所周知WM的进程模型是Master-Worker类型的,具体表现大概就是Master fork出Worker,每个Worker持有一个Event-Loop,这是一种与Nginx十分相似的进程模型,但我估计不少老哥平时用WM没准能开大几十甚至上百个Worker进程,虽然用起来是没问题,但总归很奇怪,你见过Nginx进程开这么多的么(你要说你服务器是128核那当我没说)?所以李亮在WM问答社区里推荐一种做法就是再在WM后面接入一群任务服务器,前后之间利用异步方式通信,大概就是类似于挡在前面这一坨WM服务器负责高吞吐数据转发,业务逻辑的处理则是交给后面的一坨任务服务器。其实吧,用过Swoole多进程模式的铁子们应该比较熟悉,其实就是Swoole里Worker进程与Tasker进程的故事。


上面叨逼叨这么一堆,你应该意识到如果WM自己本身带一组Task进程也挺好的,Worker进程就可以靠高性能的进程间通信与Task进程数据交互。


用线程不好吗?行算泥狠,我后面补一个PHP多线程系列


在关于进程间通信这里,一牵涉到共享内存就脑袋大,多个进程/线程访问同一块儿共享内存,你就是抠脚趾头都能预料到读写锁的问题,所以还得有个信号量来「相濡以沫」一下:


一个是坦克,另一个是坦克行车记录仪

一个是航母,另一个是航母雨刮器

一个是潜艇,另一个是潜艇风湿活血理疗仪

一个是飞机,另一个是飞机耐磨防刮漆


也就是说信号量和共享内存是分不开的,要用也是搭配着用。*NIX的一些书籍中甚至不建议新手轻易使用这种进程间通信的方式,因为这是一种极易产生死锁的解决方案。共享内存顾名思义,就是一坨内存中的区域,可以让多个进程进行读写。为了解决这个问题才引入了信号量,信号量是一个计数器,是配合共享内存使用的,一般情况下流程如下:

  • 当前进程获取将使用的共享内存的信号量

  • 如果信号量大于0,那么就表示这块儿共享资源可以使用,然后进程将信号量减1

  • 如果信号量为0,则进程进入休眠状态一直到信号量大于0,进程唤醒开始从1一个进程不再使用当前共享资源情况下,就会将信号量减1。


这个地方,信号量的检测并且减1是原子性的,也就说两个操作必须一起成功,这是由系统内核来实现的。在PHP中,信号量和共享内存先后一共也就这几个函数:



其中sem前缀的是信号操作相关函数,shm前缀是共享内存相关函数。


<?php// sem key$sem_key = ftok( __FILE__, 'b' );$sem_id = sem_get( $sem_key );// shm key$shm_key = ftok( __FILE__, 'm' );$shm_id = shm_attach( $shm_key, 1024, 0666 );const SHM_VAR = 1;$child_pid = [];// fork 2 child process// 下面逻辑很简单,就是两个子进程去操作同一块共享内存// 每个进程在试图读或者写共享内存前,都必须先获得锁// 当然,操作完毕后,一定记得要释放锁for( $i = 1; $i <= 2; $i++ ){ $pid = pcntl_fork(); if( $pid < 0 ){ exit(); } else if( 0 == $pid ) { // 子进程中,在操作shm共享内存前,首先获取sem锁 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 ); 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 ] ); }}// 休眠2秒钟,2个子进程都执行完毕了sleep( 2 );echo '最终结果'.shm_get_var( $shm_id, SHM_VAR ).PHP_EOL;// 记得删除共享内存数据,删除共享内存是有顺序的,先remove后detach,顺序反过来php可能会报错shm_remove( $shm_id );shm_detach( $shm_id );



其实有些老哥在折腾MySQL的时候应该注意到了,一个叫做mysql.sock的文件;或者折腾php-fpm与Nginx的时候,有个php-fpm.sock。一般约定俗成的话,这种后缀为sock的文件就是UNIX本地socket。


拿php-fpm里这个sock来说,当你把Nginx服务器与php-fpm部署在同一台机器上的时候,你完全可以考虑使用本地socket的方式让Nginx与php-fpm进行数据交换,很明显这种省略掉网络开销的通信方式应该是更高效的,不过我曾经在网上看到过一个中论调,大概是说「php-fpm这种unix socket通信方式不稳定」而且还感染了相当一批人,但是通篇也没有看到这种论调的论据是什么:


言论自由没问题,言论是否要负责呢?


如果你知道这种「不稳定」的原因,可以后台提供一下,我下篇文章打补丁。


由于前面我们说过socket相关的操作函数,所以下面的demo你们看起来应该是不费吹灰之力的,但是你们一定要把注意力分配到注释上:


/***********  一侧代码  **********/<?php// 这里要注意的是,第一个参数用AF_UNIX表示要创建一个本地unix域// 但是最后一个参数。。。给老子折腾的不清,只有用0才不会报错// 但是,他TM文档里压根没提这档子事!fk$listen_socket = socket_create( AF_UNIX, SOCK_STREAM, 0 );// 这会儿就不需要把socket bind到IP+PORT上了// 而是bind到一个sock文件上$file = "./server.sock";socket_bind( $listen_socket, $file );socket_listen( $listen_socket );while ( true ) { $connection_socket = socket_accept( $listen_socket ); $ret = socket_recv( $connection_socket, $recv_content, 2048, 0 ); echo $recv_content; $encode_ret = "higood";    socket_write( $connection_socket, $encode_ret, strlen( $encode_ret ) );}
/***********  另一侧代码  **********/<?php$conn_socket = socket_create( AF_UNIX, SOCK_STREAM, 0 );$file = "./server.sock";socket_connect( $conn_socket, $file );socket_write( $conn_socket, "HI,I am coming." );socket_recv( $conn_socket, $recv_content, 20480 );echo $recv_content;


大概就这样,能读能写,双向通信,成熟稳定,这瓜保熟。这代码你要是说你看不懂,村上春树都能被你气得直接下乡种树


  • 卧槽,老李,进程间通信已经完毕了,《PNP》是不是要完结撒花了?

  • ...屁~我突然又临时想到有些「缺要查,有些「要补」...还差最后两个章节