PHP进程通信之共享内存+UNIX Socket(二十四节)
大家好,老李是我;我永远都爱这样的我。
有人跟我说:「老李,你再也不是以前的你了」。他说这句话的时候,我仿佛感觉到了当年马克·查普曼在一枪干掉了约翰·列侬后,对着列侬的尸体说:“ 你变了 ”...
我怎么能变呢,我还是那个老李:就算整个晋西北乱成了一锅粥,我还是我,是颜色不一样的烟火,当年那个十里八乡有名的俊后生,当年雪上草地里背过锅,二营长意大利炮扛上来,老子打得就是他精锐,别说它一个板田联队,有一个师老子就TM敢打太原。你好好看看我哪儿变了?
硬核,风骚,还带着那么一丝丝小帅。就算说变,那也仅仅可能是在向「真*谢顶道人」的方向变,真到那个时候我就改号为「秃顶法师」。
恒河水一口下了肚,今天要搞一波儿流。还是有同学整不明白进程间通信的意义啊,结合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你们看起来应该是不费吹灰之力的,但是你们一定要把注意力分配到注释上:
/*********** 一侧代码 **********/
// 这里要注意的是,第一个参数用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 ) );
}
/*********** 另一侧代码 **********/
$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, 2048, 0 );
echo $recv_content;
大概就这样,能读能写,双向通信,成熟稳定,这瓜保熟。这代码你要是说你看不懂,村上春树都能被你气得直接下乡种树。
卧槽,老李,进程间通信已经完毕了,《PNP》是不是要完结撒花了?
...屁~我突然又临时想到有些「缺要查,有些「漏要补」...还差最后两个章节