├── 1. php进程daemon化的正确做法.md ├── 10. PHP socket初探---关于IO的一些枯燥理论.md ├── 11. PHP socket初探---select系统调用.md ├── 12. PHP socket初探 --- 颤颤抖抖开篇epoll(一).md ├── 13. PHP socket初探 --- 硬着头皮继续libevent(二).md ├── 14. PHP socket初探 --- 含着泪也要磕完libevent(三).md ├── 15. PHP socket初探 --- 一些零碎细节的拾漏补缺(一).md ├── 16. swoole的协程是个什么鬼.md ├── 17. PHP中的yield(上).md ├── 18.填坑之PHP的yield和协程在一起的日子里.md ├── 2. php多进程初探---开篇.md ├── 3. php多进程初探---孤儿和僵尸.md ├── 4. php多进程初探---信号.md ├── 5. swoole的进程模型.md ├── 6. php多进程初探---利用多进程开发点儿东西.md ├── 7. php多进程初探---再次谈daemon进程.md ├── 8. php多进程初探---进程间通信二三事.md ├── 9. PHP Socket初探---先从一个简单的socket服务器开始.md ├── LICENSE ├── README.md └── 进程状态图.jpeg /1. php进程daemon化的正确做法.md: -------------------------------------------------------------------------------- 1 | ###### daemon 音标 : ['di:mən] , 中文含义为守护神或精灵的意思 . 其实它还有个意思 : 守护进程 . 2 | ###### 守护进程简单地说就是可以脱离终端而在后台运行的进程 . 这在Linux中是非常常见的一种进程 , 比如apache或者mysql等服务启动后 , 就会以守护进程的方式进驻在内存中 . 3 | ###### 以PHP为例 , 假如我有个耗时间的任务需要跑在后台 : 将所有mysql中user表中的2000万用户全部导入到redis中做缓存预热, 那么这个任务估计一时半会是不会结束的 , 这个时候就需要编写一个php脚本以daemon形式运行在系统中 , 结束后自动退出. 4 | ###### 在Linux中 , 大概有三种方式实现脚本后台化 : 5 | ###### 1 . 在命令后添加一个&符号 , 比如 php task.php & . 这个方法的缺点在于 如果terminal终端关闭 , 无论是正常关闭还是非正常关闭 , 这个php进程都会随着终端关闭而关闭 , 其次是代码中如果有echo或者print_r之类的输出文本 , 会被输出到当前的终端窗口中 . 6 | ###### 2 . 使用nohup命令 , 比如 nohup php task.php & . 默认情况下 , 代码中echo或者print_r之类输出的文本会被输出到php代码同级目录的nohup.out文件中 . 如果你用exit命令或者关闭按钮等正常手段关闭终端 , 该进程不会被关闭 , 依然会在后台持续运行 . 但是如果终端遇到异常退出或者终止 , 该php进程也会随即退出 . 本质上 , 也并非稳定可靠的daemon方案 . 7 | ###### 3 . 使用fork和setsid , 我暂且称之为 : *nix解决方案 . 具体看下代码 : 8 | ```php 9 | 0 ) { 15 | exit( ' parent process. ' ); 16 | } 17 | // 将当前子进程提升会会话组组长 这是至关重要的一步 18 | if ( ! posix_setsid() ) { 19 | exit( ' setsid error. ' ); 20 | } 21 | // 二次fork 22 | $pid = pcntl_fork(); 23 | if( $pid < 0 ){ 24 | exit( ' fork error. ' ); 25 | } else if( $pid > 0 ) { 26 | exit( ' parent process. ' ); 27 | } 28 | 29 | // 真正的逻辑代码们 下面仅仅写个循环以示例 30 | for( $i = 1 ; $i <= 100 ; $i++ ){ 31 | sleep( 1 ); 32 | file_put_contents( 'daemon.log', $i, FILE_APPEND ); 33 | } 34 | ?> 35 | ``` 36 | ###### 另外还有一种运行脚本的方式也值得一提:利用 screen / tmux 等软件,将脚本运行在可以在一个虚拟终端之上。 -------------------------------------------------------------------------------- /10. PHP socket初探---关于IO的一些枯燥理论.md: -------------------------------------------------------------------------------- 1 | 要想更好了解socket编程,有一个不可绕过的环节就是IO. 2 | 在Linux中,一切皆文件.实际上要文件干啥?不就是读写么?所以,这句话本质就是"IO才是王道".用php的fopen打开文件关闭文件读读写写,这叫本地文件IO.在socket编程中,本质就是网络IO. 3 | 所以,在开始进一步的socket编程前,我们必须先从概念上认识好IO. 4 | 如果到这里你还对IO没啥概念,那么我就通过几个词来给你一个大概的印象:同步,异步,阻塞,非阻塞,甚至是同步阻塞,同步非阻塞,异步阻塞,异步非阻塞.是不是晕了?截至到目前为止,你可以简单地认为只要搞明白这几个名词的含义以及区别,就算弄明白IO了,至少了可以继续往下看了. 5 | 先机械记忆一波儿:IO分为两大种,同步和异步. 6 | ###### 同步IO: 7 | - 阻塞IO 8 | - 非阻塞IO 9 | - IO多路复用(包括select,poll,epoll三种) 10 | - 信号驱动IO 11 | 12 | ###### 异步IO 13 | 那么如何理解区别这几个概念呢?尤其是同步和阻塞,异步和非阻塞,看起来就是一样的. 14 | 我先举个例子结合自己的理解来说明一下: 15 | 1. 你去甜在心馒头店买太极馒头,阿梅说:"暂时没,正在蒸呢,你自己看着点儿!".于是你就站在旁边只等馒头.此时的你,**是阻塞的**,**是同步的**.阻塞表现在你除了等馒头,别的什么都不做了.同步表现在等馒头的过程中,阿梅不提供通知服务,你不得不自己要等到"馒头出炉"的消息. 16 | 2. 你去甜在心馒头店买太极馒头,阿梅说:"暂时没,正在蒸呢,你自己看着点儿!".于是你就站在旁边发微信,然后问一句:"好了没?",然后发QQ,然后再问一句:"好了没?".此时的你,**是非阻塞的**,**是同步的**.非阻塞表现在你除了等馒头,自己还干干别的时不时会主动问问馒头好没好.同步表现在等馒头的过程中,阿梅不提供通知服务,你不得不自己要等到"馒头出炉"的消息. 17 | 3. 你去甜在心馒头店买太极馒头,阿梅说:"暂时没,正在蒸呢,蒸好了我打电话告诉你!".但你依然站在旁边只等馒头,此时的你,**是阻塞的**,**是异步的**.阻塞表现在你除了等馒头,别的什么都不做了.异步表现在等馒头的过程中,阿梅提供电话通知"馒头出炉"的消息,你只需要等阿梅的电话. 18 | 4. 你去甜在心馒头店买太极馒头,阿梅说:"暂时没,正在蒸呢,蒸好了我打电话告诉你!".于是你就走了,去买了双新球鞋,看了看武馆,总之,从此不再过问馒头的事情,一心只等阿梅电话.此时的你,**是非阻塞的**,**是异步的**.非阻塞表现在你除了等馒头,自己还干干别的时不时会主动问问馒头好没好.异步表现在等馒头的过程中,阿梅提供电话通知"馒头出炉"的消息,你只需要等阿梅的电话. 19 | 20 | 如果你仔细品过上面案例中的每一个字,你就能慢慢体会到之所以异步和非阻塞,同步和阻塞容易混淆,仅仅是因为二者的表现形式稍微有点儿相似而已. 21 | 阻塞和非阻塞关注的是:在等馒头的过程中,你在干啥. 22 | 同步和异步关注的是:等馒头这件事,你是一直等到"馒头出炉"的结果,还是立即跑路等阿梅告诉你的"馒头出炉".重点的是你是如何得知"馒头出炉"的. 23 | 所以现实世界中,最傻的人才会采用异步阻塞的IO方式去写程序.其余三种方式,更多的人都会选择同步阻塞或者异步非阻塞.同步非阻塞最大的问题在于,你需要不断在各个任务中忙碌着,导致你的大脑混乱,非常累. 24 | -------------------------------------------------------------------------------- /11. PHP socket初探---select系统调用.md: -------------------------------------------------------------------------------- 1 | [socket初探 --- 先从一个简单的socket服务器开始](https://t.ti-node.com/thread/6445811931457519616 "PHP socket初探 --- 先从一个简单的socket服务器开始")>中依次讲解了三个逐渐进步的服务器: 2 | - 只能服务于一个客户端的服务器 3 | - 利用fork可以服务于多个客户端的额服务器 4 | - 利用预fork派生进程服务于多个客户端的服务器 5 | 6 | 最后一种服务器的进程模型基本上的大概原理其实跟我们常用的apache是非常相似的. 7 | 其实这种模型最大的问题在于需要根据实际业务预估进程数量,依旧是需要大量进程来解决问题,可能会出现CPU浪费在进程间切换上,还有可能会出现惊群现象(简单理解就是100个进程在等带客户端连接,来了一个客户端但是所有进程都被唤醒了,但最终只有一个进程为这个客户端服务,其余99个白白折腾),那么,有没有一种解决方案可以使得少量进程服务于多个客户端呢? 8 | 答案就是在<[PHP socket初探 --- 关于IO的一些枯燥理论](https://t.ti-node.com/thread/6445811931549794305 "PHP socket初探 --- 关于IO的一些枯燥理论")>中提到的"IO多路复用".多路是指多个客户端连接socket,复用就是指复用少数几个进程,多路复用本身依然隶属于同步通信方式,只是表现出的结果看起来像异步,这点值得注意.目前多路复用有三种常用的方案,依次是: 9 | 10 | - select,最早的解决方案 11 | - poll,算是select的升级版 12 | - epoll,目前的最终解决版,解决c10k问题的功臣 13 | 14 | 今天说的是select,这个东西本身是个Linux系统调用.在Linux中一切皆为文件,socket也不例外,每当Linux打开一个文件系统都会返回一个对应该文件的标记叫做文件描述符.文件描述符是一个非负整数,当文件描述数达到最大的时候,会重新回到小数重新开始(题外话:按照传统,一般情况下标准输入是0,标准输出是1,标准错误是2).对文件的读写操作就是利用对文件描述符的读写操作.一个进程可以操作的文件描述符的数量是有限制的,不同系统有不同的数量,在linux中,可以通过调整ulimit来调整控制. 15 | 先通过一个简单的例子说明下select的作用和功能.双11到了,你给少林足球队买了很多很多球鞋,分别有10个快递给你运送,然后你就不断地电话询问这10个快递员,你觉得有点儿累.阿梅很心疼你,于是阿梅就说:"这事儿你不用管了,你去专心练大力金刚腿吧,等任何一个快递到了,我告诉你".当其中一个快递来了后,阿梅就喊你:"下来啦,有快递!",但是,这个阿梅比较缺心眼,她不告诉你是具体哪双鞋子的快递,只告诉你有快递到了.所以,你只能依次查询一遍所有快递单的状态才能确认是哪个签收了. 16 | 上面这个例子通过结合术语演绎一遍就是,你就是服务器软件,阿梅就是select,10个快递就是10个客户端(也就是10个连接socket fd).阿梅负责替你管理着这10个连接socket fd,当其中任何一个fd有反应了也就是可以读数据或可以发送数据了,阿梅(select)就会告诉你有可以读写的fd了,但是阿梅(select)不会告诉你是哪个fd可读写,所以你必须轮循所有fd来看看是哪个fd,是可读还是可写. 17 | 是时候机械记忆一波儿了: 18 | 当你启动select后,需要将三组不同的socket fd加入到作为select的参数,传统意义上这种fd的集合就叫做fd_set,三组fd_set依次是可读集合,可写集合,异常集合.三组fd_set由系统内核来维护,每当select监控管理的三个fd_set中有可读或者可写或者异常出现的时候,就会通知调用方.调用方调用select后,调用方就会被select阻塞,等待可读可写等事件的发生.一旦有了可读可写或者异常发生,需要将三个fd_set从内核态全部copy到用户态中,然后调用方通过轮询的方式遍历所有fd,从中取出可读可写或者异常的fd并作出相应操作.如果某次调用方没有理会某个可操作的fd,那么下一次其余fd可操作时,也会再次将上次调用方未处理的fd继续返回给调用方,也就是说去遍历fd的时候,未理会的fd依然是可读可写等状态,一直到调用方理会. 19 | 上面都是我个人的理解和汇总,有错误可以指出,希望不会误人子弟.下面通过php代码实例来操作一波儿select系统调用.在php中,你可以通过stream_select或者socket_select来操作select系统调用,下面演示socket_select进行代码演示: 20 | ```php 21 | 0 ){ 49 | // 判断listen_socket有没有发生变化,如果有就是有客户端发生连接操作了 50 | if( in_array( $listen_socket, $read ) ){ 51 | // 将客户端socket加入到client数组中 52 | //socket_accept创建一个可用套接字传送数据,准备给其他客户端发送数据用的 53 | $client_socket = socket_accept( $listen_socket ); 54 | //下面这句很有用,避免了 unset( $read[ $key ] )后,在while时,客户端进来再次用 $client赋值给$read 55 | $client[] = $client_socket; 56 | // 然后将listen_socket从read中去除掉 57 | $key = array_search( $listen_socket, $read ); 58 | unset( $read[ $key ] ); 59 | } 60 | // 查看去除listen_socket中是否还有client_socket 61 | //已经进行telnet连接后,会直接走这一步,不会进去上面代码的in_array, 62 | if( count( $read ) > 0 ){ 63 | $msg = 'hello world'; 64 | foreach( $read as $socket_item ){ 65 | // 从可读取的fd中读取出来数据内容,然后发送给其他客户端 66 | $content = socket_read( $socket_item, 2048 ); 67 | // 循环client数组,将内容发送给其余所有客户端 68 | foreach( $client as $client_socket ){ 69 | // 因为client数组中包含了 listen_socket 以及当前发送者自己socket,$client_socket != $socket_item 再次排除自已,所以需要排除二者 70 | if( $client_socket != $listen_socket && $client_socket != $socket_item ){ 71 | socket_write( $client_socket, $content, strlen( $content ) ); 72 | } 73 | } 74 | } 75 | } 76 | } 77 | // 当select没有监听到可操作fd的时候,直接continue进入下一次循环 78 | else { 79 | continue; 80 | } 81 | 82 | } 83 | ``` 84 | 将文件保存为server.php,然后执行php server.php运行服务,同时再打开三个终端,执行telnet 127.0.0.1 9999,然后在任何一个telnet终端中输入"I am xiaoming!",再看其他两个telnet窗口,是不是感觉很屌? 85 | 不完全截图图下: 86 | ![](http://static.ti-node.com/6389738812670476289) 87 | 还没意识到问题吗?如果我们看到有三个telnet客户端连接服务器并且可以彼此之间发送消息,但是我们只用了一个进程就可以服务三个客户端,如果你愿意,可以开更多的telnet,但是服务器只需要一个进程就可以搞定,这就是IO多路复用diao的地方! 88 | 最后,我们重点解析一些socket_select函数,我们看下这个函数的原型: 89 | ```php 90 | int socket_select ( array &$read , array &$write , array &$except , int $tv_sec [, int $tv_usec = 0 ] ) 91 | ``` 92 | 值得注意的是$read,$write,$except三个参数前面都有一个&,也就是说这三个参数是引用类型的,是可以被改写内容的.在上面代码案例中,服务器代码第一次执行的时候,我们要把需要监听的所有fd全部放到了read数组中,然而在当系统经历了select后,这个数组的内容就会发生改变,由原来的全部read fds变成了只包含可读的read fds,这也就是为什么声明了一个client数组,然后又声明了一个read数组,然后read = client.如果我们直接将client当作socket_select的参数,那么client数组内容就被修改.假如有5个用户保存在client数组中,只有1个可读,在经过socket_select后client中就只剩下那个可读的fd了,其余4个客户端将会丢失,此时客户端的表现就是连接莫名其妙发生丢失了. 93 | -------------------------------------------------------------------------------- /12. PHP socket初探 --- 颤颤抖抖开篇epoll(一).md: -------------------------------------------------------------------------------- 1 | 正如标题所言,颤颤抖抖开篇epoll。颤颤抖抖的原因大概也就是以前几乎没有亲自“手刃”epoll的经验,仅仅靠epoll的理论知识骗吃骗喝骗人事哄小孩儿装高手,现如今,没有了大师兄的铁头功照顾,没有了六师弟的轻功水上漂背,没有了阿梅的太极功护身,不得不自己个儿当一次排头兵了。 2 | 3 | #### **说到底,还是因为自己虚。** 4 | 5 | ![](https://static.ti-node.com/6396286943289671681) 6 | 7 | 先立个flag,那就是epoll比select牛逼,尽管select是POSIX标准。即便是select的高配版本poll,也比epoll差太多太多。网络如此发达的今天,epoll是解决c10k问题的功臣,这是没有办法的事情。epoll虽然是后出生的,但是却有着与生俱来的高傲,就像王思聪;select就是普通屌丝,花点儿钱使劲装扮自己也顶多就是个poll。这poll和epoll,可差一个e呢,没办法,与生俱来的差距。 8 | 9 | 坊间传闻,在epoll出世前,QQ用户量剧增,但是select以及select的高配版本poll都无法解决他们的问题,于是乎QQ当年的服务器就不得不用UDP协议来避规这个问题,一直到后来有了epoll,QQ开始逐步在PC客户端中的配置项中允许用户选择UDP服务器或TCP服务器。 10 | 11 | 还是通过浅显的示例来说明下为啥epoll比select厉害(这个例子在前面文章中应该提过,今儿再回放一遍)。 12 | 13 | 你要去继续练习大力金刚腿,阿梅还是要替你收双十一的10个快递。为了方便自己记忆这些快递,你把十个快递记录到了一个清单上给了阿梅。但这个时候阿梅显然不太清楚怎么应付这场景,于是每当收到X个快递,阿梅都是直接把快递清单抄写一份再拿给你并告诉你:“有快递来了!”,至于来了几个快递以及是分别是哪个镖局护送的,阿梅是不会告诉你的。于是只能是你自己,把单子上的10个快递逐次和收到的对比一遍,然后对比完毕后再把这个单子给了阿梅,然后阿梅继续等。 14 | 15 | 又是一年双十一,阿梅这次学聪明了,经历过那场球赛后,她已经得到了自我,实现了人生价值,今年的阿梅是一个全新的阿梅,一个剃了光头的阿梅。 16 | 17 | 你要去继续练习大力金刚腿,阿梅还是要替你收双十一的10个快递。为了方便自己记忆这些快递,你把十个快递记录到了一个清单上给了阿梅。但这个时候的阿梅显然已经得到了自我,是升华了的阿梅,于是每收到X个( X >= 1 )快递,阿梅都会在冲你喊一句:“顺丰镖局大师兄的铁头套,圆通镖局六师弟的鸡蛋到了!”,而你,不用再去依次对单子,阿梅会直接告诉你是哪个镖局护送的哪个快递,然后她还会按照你提前告诉她的“如果收到鸡蛋就给六师弟,收到铁头套就给大师兄”。哪怕你买了10000个快递,阿梅照样四两拨千斤,太极功夫收快递,而你,只需要安静的练习大力金刚腿。 18 | 19 | ![](https://static.ti-node.com/6396284198600048640) 20 | 21 | 剃光头前的阿梅,就是select,不敢正眼看老板娘一眼。 22 | 剃光头后的阿梅,就是epoll,可徒手接魔鬼队的死亡之球。 23 | 24 | 快递就相当于是socket fd,包括监听socket和连接socket;那个清单就是fd的集合;阿梅就是select或者epoll;你就是当前的一个进程;某个快递到了,就相当于是某个fd已经可读或可写。 25 | 26 | select虽然一定程度上解决了一个进程可以读写多个fd的问题,但是select有如下致命缺点: 27 | - 默认情况下,select可管理的fd的数量是1024个(阿梅最多帮你收1024个快递) 28 | - select每次检测到fd集合中有可读写的fd时,它会把整个fd全部复制一遍给你,然后你自己再去逐个轮询究竟是哪个fd可读写 29 | - 正如以上所说,它会把整个fd全部复制给你(她把整个清单抄了一份给你),从术语上讲,这个过程是将fd从内核态复制一遍给用户态的调用进程 30 | - 正如以上所说,你自己逐个轮询所有fd才能知道究竟是哪个可读写(反正就是有快递来了,来了几个都是谁你自己个儿对着清单查去) 31 | - 你自己个轮询的过程是线性的,如果有个n个fd,那么时间复杂度一定是O(n) 32 | 33 | 而epoll则拥有更加专业的高端大气上档次的技能指标: 34 | - 理论上可以搞定无上限的fd(可以收无数个快递的阿梅) 35 | - 只挑出可读写(其实严格意义上还有异常)的活跃的fd,其余的fd不理会 36 | - 使用MMAP加速内核态数据拷贝 37 | 38 | 除此之外,需要特殊指出的是,epoll本身的两种模式: 39 | - 水平触发。这种方式下,如果监听到了有X个事件发生,那么内核态会将这些事件拷贝到用户态,但是可惜的是,如果用户只处理了其中一件,剩余的X-1件出于某种原因并没有理会,那么下次的时候,这些未处理完的X-1个事件依然会从内核态拷贝到用户态。这样做是有阴阳两面的,阳面是事件安全的不会发生丢失,阴面是对于性能来说是一种浪费。其实这个时候的epoll颇有些类似于poll的工作方式。 40 | - 边缘触发。这种方式下,是鸡血版本的epoll,是释放自我的epoll,也是应该是正确的使用方式。这种情况下,如果发生了X个事件,然而你只处理了其中1个事件,那么剩余的X-1个事件就算“丢失”了。性能是上去了,与之俱来的就是可能的事件丢失。 41 | 42 | 那么,你以为是时候写代码演示epoll了,然而并不是,原因有两个: 43 | - 通过C语言可以直接操作epoll,但是,为了避免装逼失败,我决定不用C来演示(放到后面再深入的时候) 44 | - 如果说通过PHP来操作,我不得不提一件悲催的事情,***据我自己得到的经验告诉我*** 那就是PHP无法直接操控epoll,而是要通过操作libevent来搞定epoll。 45 | 46 | 那么,什么是Libevent呢?怎么听着好耳熟,不光耳熟,你看下下图,是不是还有点儿眼熟?没错,这的博客的前端页面就是抄的[Libevent官网](http://libevent.org/ "Libevent官网")的。 47 | ![](http://static.ti-node.com/6396306572812746753) 48 | 49 | 我先从Libevent官网抄袭一段话:“Currently, libevent supports /dev/poll, kqueue(2), event ports, POSIX select(2), Windows select(), poll(2), and epoll(4). ”,你就能大概知道Libevent是干啥的了。大概意思就是Libevent对/dev/poll、Mac中的kqueue、select、poll以及epoll的API进行了封装,屏蔽了这几个多路复用开发上的一些细节和不同点,对外提供统一的API的一个高性能网络事件库。 50 | 51 | 额外提醒一点,这个东西是用C语言编写的,几十年过去了,你大爷还是你大爷。 52 | 53 | 回到正路上来,就是“PHP中如何使用Libevent”。在pecl.php.net上,有两个扩展都可以使phper方便地操控libevent,一个就叫libevent,另一个叫做event,推荐大家用后者。前者不知道什么原因版本一直停留在0.10 Beta状态,开发日期则停留在了2013-05-22日,我没怎么试过,估计可能不支持php7,不过,还是要感谢开发者。event扩展就比较屌了,版本迭代不错,看起来开发者挺积极的,也支持php7,目前的稳定版本是2.3.0,所以推荐大家使用event扩展。 54 | 55 | 正好在此补充一下php扩展的安装方式,以event扩展为例。 56 | 57 | - 下载event 2.3.0的稳定版本,wget https://pecl.php.net/get/event-2.3.0.tgz 58 | -![](http://static.ti-node.com/6396312840944222209) 59 | 60 | - 解压tgz源码包,tar -zxvf event-2.3.0.tgz 61 | -![](http://static.ti-node.com/6396312844396134400) 62 | 63 | 64 | - cd event-2.3.0进入到主目录中,然后执行phpize,再执行./configure 65 | ![](http://static.ti-node.com/6396312847160180737) 66 | 67 | 68 | - 执行make 69 | 70 | ![](http://static.ti-node.com/6396312851614531584) 71 | 72 | 73 | - 执行make install安装 74 | ![](http://static.ti-node.com/6396312854005284864) 75 | 76 | 77 | - 配置php的cli环境配置文件,注意不是apache2,也不是fpm的,而是cli的php.ini,添加一句:extension = '/usr/lib/php/20151012/event.so',然后在终端中执行php -m看下,是不是有event呢? 78 | 79 | 好了,今天到这里正式收官,下一篇继续嗑php和他的event扩展二三事! 80 | -------- 81 | -------------------------------------------------------------------------------- /13. PHP socket初探 --- 硬着头皮继续libevent(二).md: -------------------------------------------------------------------------------- 1 | 实际上php.net上是有event扩展的使用说明手册,但是呢,对于初学者来说却并没有什么卵用,因为没有太多的强有力使用案例代码,也没有给力的User Contributed Notes,所以可能造成的结果就是:根本就看不懂。 2 | 3 | 这就是event文档,[点击这里](http://php.net/manual/en/book.event.php "点击这里"),你们可以感受一下。从文档上看,event扩展一共实现了如下图几个基础类,其中最常用重要的就是Event和EventBase以及EventConfig三个类了,所以,先围绕这三位开展一下工作。 4 | 5 | ![](https://static.ti-node.com/6396320853713223680) 6 | 7 | 考虑到你们、我、还有正在看这个文章的其他未知物种,大多数可能并不是搞C语言的老兵油子,所以我得用一些可能并不恰当的案例和比喻来尝试引入这些概念。 8 | 9 | libevent中有五个字母是event,实际上就是说“event才是王道”。 10 | 11 | Event类就是产生各种不同类型事件的产出器,比如定时器事件、读写事件等等,为了提升民族荣誉感,我们将这些各种事件比作各种战斗机:比如歼10、歼15和歼20。 12 | 13 | ![](http://static.ti-node.com/6397057860143939585) 14 | 15 | 16 | EventBase类就相对容易介入了,这玩意显然就是一个航空母舰了,为了提升民族荣誉感,我们就把EventBase类当作是辽宁舰。各种Event都必须依靠EventBase才能混口饭吃,这和战斗机有辽宁舰才有底气飞的更高更远是一个道理。一定是先有航母(EventBase),其次是战斗机(Event)挂在航母(EventBase)上。 17 | 18 | ![](http://static.ti-node.com/6397058595610951680) 19 | 20 | EventConfig则是一个配置类,实例化后的对象作为参数可以传递给EventBase类,这样在初始化EventBase类的时候会根据这个配置初始化出不同的EventBase实例。类比的话,这个类则有点儿类似于辽宁舰的舰岛,可以配置指挥整个辽宁舰。航空母舰的发展趋势是不需要舰岛的,同样,在实例化EventBase类时候同样也可以不传入EventConfig对象,直接进行实例化也是没有问题的。 21 | 22 | 下面我们从开始写一个php定时器来步入到代码的节奏中。定时器是大家常用的一个工具,一般phper一说定时器,脑海中第一个想起的绝逼是Linux中的crontab。难道phper们离开了crontab真的就没法混了吗?是的,真的好羞耻,现实告诉我们就是这样的,他们离开了crontab真的就没法混了。那么,是时候通过纯php来搞一波儿定时器实现了! 23 | 24 | 注意是真的纯php,连Event扩展都不用的那种。 25 | ```php 26 | add( $tick ); 63 | // eventBase进入loop状态(辽宁舰!走你!) 64 | $eventBase->loop(); 65 | ``` 66 | 将代码保存为tick.php,然后php tick.php执行一下,如下图所示: 67 | ![](http://static.ti-node.com/6397075325490036736) 68 | 69 | 这种定时器是持久的定时器(每隔X时间一定会执行一次),如果想要一次性的定时器(隔X时间后就会执行一次,执行过后再也不执行了),那么将上述代码中的“Event::TIMEOUT | Event::PERSIST”修改为“Event::TIMEOUT”即可。 70 | 71 | 如果你有一些自定义用户数据传递给回调函数,可以利用new Event()的第五个参数,这五个参数可以给回调函数用,如下所示: 72 | ```php 73 | 'woshishui', 79 | ) ); 80 | ``` 81 | 82 | 需要重点说明的是new Event()这行代码了,我把原型贴过来给大家看下: 83 | ```php 84 | public Event::__construct ( EventBase $base , mixed $fd , int $what , callable $cb [, mixed $arg = NULL ] ) 85 | ``` 86 | - 第一个参数是一个eventBase对象即可 87 | - 第二个参数是文件描述符,可以是一个监听socket、一个连接socket、一个fopen打开的文件或者stream流等。如果是时钟时间,则传入-1。如果是其他信号事件,用相应的信号常量即可,比如SIGHUP、SIGTERM等等 88 | - 第三个参数表示事件类型,依次是Event::READ、Event::WRITE、Event::SIGNAL、Event::TIMEOUT。其中,加上Event::PERSIST则表示是持久发生,而不是只发生一次就再也没反应了。比如Event::READ | Event::PERSIST就表示某个文件描述第一次可读的时候发生一次,后面如果又可读就绪了那么还会继续发生一次。 89 | - 第四个参数就熟悉的很了,就是事件回调了,意思就是当某个事件发生后那么应该具体做什么相应 90 | - 第五个参数是自定义数据,这个数据会传递给第四个参数的回调函数,回调函数中可以用这个数据。 91 | 92 | 通过以上的案例代码可以总结一下日常流程: 93 | 1. 创建EventConfig(非必需) 94 | 2. 创建EventBase 95 | 3. 创建Event 96 | 4. 将Event挂起,也就是执行了Event对象的add方法,不执行add方法那么这个event对象就无法挂起,也就不会执行 97 | 5. 将EventBase执行进入循环中,也就是loop方法 98 | 99 | 捋清楚了定时器代码,我们尝试来解决一个信号的问题。比如我们的进程是常驻内存的daemon,再接收到某个信号后就会作出相应的动作,比如收到term信号后进程就会退出、收到usr1信号就会执行reload等等。 100 | ```php 101 | add(); 112 | // 进入循环 113 | echo "进入循环".PHP_EOL; 114 | $eventBase->loop(); 115 | ``` 116 | 将代码保存成tick.php,然后执行php tick.php,代码已经进入循环了,然后我们打开另外一个终端,输入ps aux|grep tick查看一个php进程的pid进程号,对这个进程发送term信号,如下图所示: 117 | 118 | ![](http://static.ti-node.com/6397300737826619393) 119 | ![](http://static.ti-node.com/6397300801726840833) 120 | 121 | 奇怪啊,从第一张图看到确实收到term信号了,但是很奇怪为什么这个php进程退出了呢?是因为没有添加Event::PERSIST,修改如下代码如下: 122 | ```php 123 | getMethod().PHP_EOL; 156 | // 跑了许久龙套的config这次也得真的露露手脚了 157 | $eventConfig = new EventConfig; 158 | // 避免使用方法kqueue 159 | $eventConfig->avoidMethod('kqueue'); 160 | // 利用config初始化event base 161 | $eventBase = new EventBase( $eventConfig ); 162 | echo "当前event的方法是:".$eventBase->getMethod().PHP_EOL; 163 | ``` 164 | 将代码保存了,然后执行一下,可以看到结果如下图所示: 165 | 166 | ![](http://static.ti-node.com/6397310348063408129) 167 | 168 | 那么,还有一些更鸡贼的人继续发问,前面提到的边缘触发和水平触发,如何确认呢?既然都用上epoll或者kqueue了,就一定要用边缘触发。 169 | ```php 170 | getFeatures(); 174 | // 看不到这个判断条件的,请反思自己“位运算”相关欠缺 175 | if( $features & EventConfig::FEATURE_ET ){ 176 | echo "边缘触发".PHP_EOL; 177 | } 178 | if( $features & EventConfig::FEATURE_O1 ){ 179 | echo "O1添加删除事件".PHP_EOL; 180 | } 181 | if( $features & EventConfig::FEATURE_FDS ){ 182 | echo "任意文件描述符,不光socket".PHP_EOL; 183 | } 184 | ``` 185 | 运行结果如下图所示: 186 | ![](http://static.ti-node.com/6397315528683159552) 187 | 188 | 小小装个逼总结一下,今儿这些个内容就是讲述event的基础三大类,下个篇章依然是围绕这三个家伙和IO操作结合到一起。 189 | ------- 190 | -------------------------------------------------------------------------------- /14. PHP socket初探 --- 含着泪也要磕完libevent(三).md: -------------------------------------------------------------------------------- 1 | 这段时间相比大家也看到了,本人离职了,一是在家偷懒实在懒得动手,二是好不容易想写点儿时间全部砸到数据结构和算法那里了。 2 | 3 | 今儿回过头来,继续这里的文章。那句话是怎么说的: 4 | 5 | “**自己选择的课题,含着泪也得磕完!**”(图文无关,[详情点击这里](https://tieba.baidu.com/p/3504775033?red_tag=1379561293 "详情点击这里"))。 6 | 7 | ![](http://static.ti-node.com/6402086624192102400) 8 | 9 | 其实在上一篇libevent文章中([《PHP socket初探 --- 硬着头皮继续libevent(二)》]( "《PHP socket初探 --- 硬着头皮继续libevent(二)》")),如果你总结能力很好的话,可以观察出来我们尝试利用libevent做了至少两件事情: 10 | - 毫秒级别定时器 11 | - 信号监听工具 12 | 13 | 大家都是码php的,也喜欢把自己说的洋气点儿:“ 我是写服务器的 ”。所以,今天的第一个案例就是拿libevent来构建一个简单粗暴的http服务器: 14 | 15 | ```php 16 | 样做 32 | if( ( $connect_socket = socket_accept( $listen_socket ) ) != false){ 33 | echo "有新的客户端:".intval( $connect_socket ).PHP_EOL; 34 | $msg = "HTTP/1.0 200 OK\r\nContent-Length: 2\r\n\r\nHi"; 35 | socket_write( $connect_socket, $msg, strlen( $msg ) ); 36 | socket_close( $connect_socket ); 37 | } 38 | }, $listen_socket ); 39 | $event->add(); 40 | $event_base->loop(); 41 | 42 | ``` 43 | 将代码保存为test.php,然后php http.php运行起来。再开一个终端,使用curl的GET方式去请求服务器,效果如下: 44 | 45 | ![](http://static.ti-node.com/6402443744179650560) 46 | 47 | 这是一个非常非常简单地不能再简单的http demo了,对于一个完整的http服务器而言,他还差比较完整的http协议的实现、多核CPU的利用等等。这些,我们会放到后面继续深入的文章中开始细化丰富。 48 | 49 | 还记得我们使用select系统调用实现了一个粗暴的在线聊天室,select这种业余的都敢出来混个聊天室,专业的绝对不能怂。 50 | 51 | 无数个专业👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍送给libevent! 52 | 53 | ![](http://static.ti-node.com/6402446798421491713) 54 | 55 | ![](http://static.ti-node.com/6402446847486459905) 56 | 57 | ![](http://static.ti-node.com/6402446898828935169) 58 | 59 | 啦啦啦啦,开始码: 60 | ```php 61 | $conn_item ){ 92 | if( $conn != $conn_item ){ 93 | $msg = intval( $conn ).'说 : '.$buffer; 94 | socket_write( $conn_item, $msg, strlen( $msg ) ); 95 | } 96 | } 97 | }, $conn ); 98 | $event->add(); 99 | // 此处值得注意,我们需要将事件本身存储到全局数组中,如果不保存,连接会话会丢失,也就是说服务端和客户端将无法保持持久会话 100 | $event_arr[ intval( $conn ) ] = $event; 101 | } 102 | }, $fd ); 103 | $event->add(); 104 | $event_base->loop(); 105 | ``` 106 | 将代码保存为server.php,然后php server.php运行,再打开其他三个终端使用telnet连接上聊天室,运行效果如下所示: 107 | 108 | ![](http://static.ti-node.com/6402456852096876545) 109 | 110 | 尝试放一张动态图试试,看看行不行,自己制作的gif都特别大,不知道带宽够不够。 111 | 112 | ![](http://static.ti-node.com/6402456852096876545.gif) 113 | 114 | 截止到这篇为止,死磕Libevent系列的大体核心三把斧就算是抡完了,弄完这些,你在遇到这些代码的时候,就应该不会像下面这个样子了: 115 | 116 | ![](http://static.ti-node.com/6402456852096876549.gif) 117 | 118 | ---- 119 | -------------------------------------------------------------------------------- /15. PHP socket初探 --- 一些零碎细节的拾漏补缺(一).md: -------------------------------------------------------------------------------- 1 | 前面可以说是弄了一系列的php socket和多进程的一大坨内容,知识浅显、代码粗暴、风格简陋,总的说来,还是差了一些细节。今天,就一些漏掉的细节补充一下。 2 | 3 | 1. 一些有志青年可能最近手刃了Workerman源码,对于里面那一大坨stream_select()、stream_socket_server()表示疑惑,这个玩意和socket_create、socket_set_nonblock()有啥区别?其实,php官方手册里也提到过一嘴,socket系函数就是基于BSD Socket那一套玩意搞的,几乎就是将那些东西简单包装了一下直接抄过来用的,抄到甚至连名字都和C语言操控socket的函数一模一样,所以说socket系函数是一种比较低级(Low-Level,这里的低级是指软件工程中分层中层次的高低)socket操控方式,可以最大程度给你操作socket的自由以及细腻度。在php中,socket系本身是作为php扩展而体现的,这个你可以通过php -m来查看有没有socket,这件事情意味着有些php环境可能没有安装这个扩展,这个时候你就无法使用socket系的函数了。但stream则不同了,这货是内建于php中的,除了能处理socket网络IO外,还能操控普通文件的打开写入读取等,stream系将这些输入输出统一抽象成了流,通过流来对待一切。有人可能会问二者性能上差距,但是本人没有测试过,这个我就不敢轻易妄言了,但是从正常逻辑上推演的话,应该不会有什么太大差距之类的。 4 | 5 | 2. 一定要分清楚监听socket和连接socket,我们服务器监听的是监听socket,然后accept一个客户端连接后的叫做连接socket。 6 | 7 | 3. 关于“异步非阻塞”,这五个字到底体现在哪儿了。swoole我就不说了,我源码也才阅读了一小部分,我就说Workerman吧,它在github上称:“Workerman is an asynchronous event driven PHP framework with high performance for easily building fast, scalable network applications.”,看到其中有asynchronous(异步)的字样,打我脸的是我并没有看到有non-block(非阻塞)的字样,不过无妨,脸什么的不重要,重要的是我文章里那一坨又一坨的代码里哪里体现了非阻塞、哪里体现了异步。来吧,看代码吧。 8 | 9 | 看代码前,你要理解异步和非阻塞的区别是什么,因为这二者在表现结果上看起来是有点儿相似的,如果你没搞明白,那么一定要通过这个来理解一下[《PHP socket初探 --- 关于IO的一些枯燥理论》](https://t.ti-node.com/thread/6445811931549794305 "《PHP socket初探 --- 关于IO的一些枯燥理论》")。 10 | 11 | ```php 12 | Swoole不仅支持异步,还支持同步。什么情况下使用同步,什么情况下使用异步。这里说明一下。 8 | 我们不赞成用异步回调的方式去做功能开发,传统的PHP同步方式实现功能和逻辑是最简单的,也是最佳的方案。像node.js这样到处callback,只是牺牲可维护性和开发效率。 9 | 但有些时候很适合用异步,比如FTP、聊天服务器,smtp,代理服务器等等此类以通信和读写磁盘为主,功能和业务逻辑其次的服务器程序。 10 | 11 | 继续引用凑行数: 12 | 13 | >#### 异步的优势 14 | 高并发,同步阻塞IO模型的并发能力依赖于进程/线程数量,例如 php-fpm开启了200个进程,理论上最大支持的并发能力为200。如果每个请求平均需要100ms,那么应用程序就可以提供2000qps。异步非阻塞的并发能力几乎是无限的,可以发起或维持大量并发TCP连接 15 | 无IO等待,同步模型无法解决IOWait很高的场景,如上述例子每个请求平均要10s,那么应用程序就只能提供20qps了。而异步程序不存在IO等待,所以无论请求要花费多长时间,对整个程序的处理能力没有任何影响 16 | #### 同步的优势 17 | 编码简单,同步模式编写/调试程序更轻松 18 | 可控性好,同步模式的程序具有良好的过载保护机制,如在下面的情况异步程序就会出问题 19 | Accept保护,同步模式下一个TCP服务器最大能接受 进程数+Backlog 个TCP连接。一旦超过此数量,Server将无法再接受连接,客户端会连接失败。避免服务器Accept太多连接,导致请求堆积 20 | 21 | 最后的引用: 22 | 23 | >swoole_http_server继承自swoole_server,是一个完整的http服务器实现。swoole_http_server支持同步和异步2种模式。 24 | 无论是同步模式还是异步模式,swoole_http_server都可以维持大量TCP客户端连接。同步/异步仅仅体现在对请求的处理方式上。 25 | 示例: 26 | ```php 27 | on('request', function ($request, $response) { 30 | $response->end("

Hello Swoole. #".rand(1000, 9999)."

"); 31 | }); 32 | $http->start(); 33 | ``` 34 | #### 同步模式 35 | 这种模式等同于nginx+php-fpm/apache,它需要设置大量worker进程来完成并发请求处理。Worker进程内可以使用同步阻塞IO,编程方式与普通PHP Web程序完全一致。 36 | 与php-fpm/apache不同的是,客户端连接并不会独占进程,服务器依然可以应对大量并发连接。 37 | #### 异步模式 38 | 这种模式下整个服务器是异步非阻塞的,服务器可以应对大规模的并发连接和并发请求。但编程方式需要完全使用异步API,如MySQL、redis、http_client、file_get_contents、sleep等阻塞IO操作必须切换为异步的方式,如异步swoole_client,swoole_event_add,swoole_timer,swoole_get_mysqli_sock等API。 39 | 40 | 个人认为最后这段引用是非常具备价值的,仔细品读或许能够从中得到一些感悟。我在前面曾经写过一篇[swoole的进程模型 ](https://t.ti-node.com/thread/6445811931285553153 "swoole的进程模型 "),实际上你可以这么理解,就是master进程可以hold住上万个TCP连接是没有任何问题的,因为master进程内部异步非阻塞的,但是仅仅hold住上万个TCP连接本身是没有任何意义的,因为有数据传输的TCP连接才是有意义的。一旦有数据传输就意味着有业务逻辑产生了,那么master进程并不负责具体业务逻辑代码了,处理这个业务逻辑的活儿交给worker进程来干,然后干完后再由master进程返回给客户端。 41 | 42 | 同步阻塞模式下,如果说worker进程1秒钟完成1个客户端的业务逻辑,尽管master进程同时hold住了1W个TCP连接,但是1个worker进程只能服务于1个客户端,1W个客户端全部处理完毕,需要1W秒钟。所以,同步阻塞模式下,如果你想干活猛,就只能增加worker进程的数量,比如1000个甚至2000个。当然了,看到这里有为青年就会提出问题了,这样一味地增加进程数量岂不是意味着进程再多的话进程间切换都是极为耗费CPU的?是的,所以很简单,横向扩展加机器就是了![](http://static.ti-node.com/6345443000872599553)![](http://static.ti-node.com/6345443000872599553)![](http://static.ti-node.com/6345443000872599553)... ...或者,选择异步。 43 | 44 | 异步非阻塞模式下,这个时候除了master进程是异步非阻塞外,要求worker进程中的业务逻辑代码也得是异步非阻塞工作的方式。也就说worker进程在处理1个客户端业务逻辑的时候,如果没处理完毕就会立马开始处理第2个客户端的业务逻辑,然后继续第3个... ...持续...一旦某个客户端的业务逻辑处理完毕了就有回调通知,从此可以做到即便只有少量worker进程但依然可以维持高速高效地处理速度。所以,这种情况,对编写业务逻辑代码就有了很高的要求了。假如业务逻辑就是“插入1条评论,然后返回最新5条评论”,用伪代码演示如下: 45 | 46 | ```php 47 | on( 'connect', function( $async_mysql ){ 51 | echo '连接成功'.PHP_EOL; 52 | // 插入评论 53 | $sql = "insert into pinglun() values()"; 54 | $async_mysql->query( $sql, function( $async_mysql, $result ) { 55 | // 如果插入成功 56 | if( true == $result ){ 57 | // 获取5条最新评论 58 | $sql = "select * from pinglun limit 5"; 59 | $async_mysql->query( $sql, function( $async_mysql, $result ){ 60 | // 获取成功后拿数据 61 | if( true == $result ){ 62 | print_r( $result->fetchAll() ); 63 | } else { 64 | echo "获取失败".PHP_EOL; 65 | } 66 | } ); 67 | } 68 | // 如果插入失败 69 | else { 70 | echo "插入数据失败".PHP_EOL; 71 | } 72 | }); 73 | } ); 74 | ``` 75 | 76 | 这种代码里,将不可避免地产生大量的类似于on这种回调,如果再有一些条件依赖话,可能不得不层层回调。比如插入最新评论需要依赖connect,只有connect成功了才能执行插入操作,然后是查询最新5条评论功能依赖插入操作,只有插入操作成功才能继续查询5条最新评论。最重要的是,需要IO操作的这些函数等等都必须得是异步的才行,传统的pdo、mysqli是统统不可以用的。因为只要有一处是同步阻塞了,整个worker进程中的业务逻辑代码就算是彻底完蛋沦为同步阻塞了。所以说,如果你要在这种代码里用sleep( 100 ),你会死得惨烈。 77 | 78 | “没有这金刚钻,别拦这瓷器活”... 79 | 80 | 如果说我们用传统的同步阻塞代码的话,伪代码大概如下你们感受一下: 81 | 82 | ```php 83 | connect( $host, $port ); 87 | $pdo->query( "insert into pinglun() values()" ); 88 | $pdo->query( "select * from pinglun limit 5" ); 89 | } catch( Exception $e ) { 90 | throw new Exception('error.'); 91 | } 92 | ``` 93 | 94 | 爱不爱?喜不喜欢?高不高兴?而且我还能任意写sleep... ...![](http://static.ti-node.com/6345443000872599553) 95 | 96 | 当了这么多年的同步阻塞fpm(同步阻塞apache)的CURDer你跟我说你天生就爱异步?你猜我信么? 97 | 98 | ![](http://static.ti-node.com/6411754043550466049) 99 | 100 | 但是,异步带来的QPS上的提升实在是太明显了(注意,异步并不能提高性能,只是能提高QPS。性能就在那里躺着呢,该是多少就是多少,只不过异步可以更好的挖掘和压榨,并不能提高TA),但异步的代码实在是难写,辣么,有没有一种既可以用同步阻塞这种风格写的背后又是异步方式的方法呢?废话,当然有,不然我要这文章有何用?这种东西就是协程! 101 | 102 | 其实,有为青年在研究Golang的时候早就已经开眼见世界了,那是身经百战见的多了,但是像我这样的蠢货萌新自然是不知道的。一些人用php的yield来实现协程,不过,我认为swoole的协程要比这个yield好很多。简单说起来,协程这个东西就是用户态的异步IO,也就说不需要操作系统参与的,这点儿上和真正的异步IO的概念是不一样的。因为严格扣定义的话,异步IO是操作系统内核实现并参与的,现在协程并不需要系统参与,仅仅用户层就可以解决这些问题。 103 | 104 | 废话不多说,还是通过代码来感受一下,这坨代码大概意思就是开了一个http服务器,开了一个worker进程,worker进程中业务逻辑代码就是往数据库里添加一条记录,你们感受一下: 105 | 106 | #### 首先,注释掉同步阻塞传统代码,使用协程的写法;其次,注释掉协程写法,开启同步阻塞写法。然后分别使用ab进行简单测试 107 | 108 | - ab -n 5000 -c 100 -k http://127.0.0.1:9501/ 109 | - 只开了一个worker进程 110 | - 数据表你们自己建吧,我就不贴出来了 111 | 112 | ```php 113 | set( array( 116 | 'worker_num' => 1, 117 | ) ); 118 | $server->on('Request', function($request, $response) { 119 | // 数据库插入一条数据 120 | $sql = "insert into user(`name`) values('iwejf')"; 121 | 122 | // 下面这段是传统的同步阻塞写法 123 | /* 124 | $dbh = new PDO('mysql:host=localhost;dbname=meshbox', 'root', 'root'); 125 | $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 126 | $dbh->exec('set names utf8mb4'); 127 | $rs = $dbh->query($sql); 128 | */ 129 | 130 | // 下面这段是协程的写法 131 | $mysql = new Swoole\Coroutine\MySQL(); 132 | $res = $mysql->connect([ 133 | 'host' => '127.0.0.1', 134 | 'user' => 'root', 135 | 'password' => 'root', 136 | 'database' => 'meshbox', 137 | ]); 138 | $ret = $mysql->query( $sql ); 139 | 140 | // 回应客户端ok 141 | $response->end("ok"); 142 | }); 143 | $server->start(); 144 | ``` 145 | 146 | #### 这里是协程的测试结果: 147 | 148 | ![](http://static.ti-node.com/6411772116617658369) 149 | 150 | #### 这里是传统同步阻塞的测试结果: 151 | 152 | ![](http://static.ti-node.com/6411772168215986176) 153 | 154 | 测试结果我们就不分析了,你们应该能看懂。这中间巨大的QPS差距你们应该能感受到了。话说回来,由于我们知道想提高同步阻塞代码的QPS最有效的办法就是增加进程数量,因此我们将woker进程数量调整为8,再测试一把: 155 | 156 | ![](http://static.ti-node.com/6411773029109465088) 157 | 158 | 继续调整为16: 159 | 160 | ![](http://static.ti-node.com/6411773346500837376) 161 | 162 | 继续调整为32(接近协程的成绩,但依然差了1000QPS): 163 | 164 | ![](http://static.ti-node.com/6411774487712235521) 165 | 166 | 继续调整为64(终于超过单进程协程1600QPS了): 167 | 168 | ![](http://static.ti-node.com/6411774822082150401) 169 | 170 | 最终结果就是,我们用同步阻塞的模型开启了64个进程大概可以超越开启1个进程的协程方式将近1600QPS。 171 | 172 | 最后,部分有为青年可能想要了解swoole协程原理,我自己因为水准问题(其实我不懂)就不发表自己的看法了,直接盗链官网资料了:https://wiki.swoole.com/wiki/page/p-coroutine_realization.html 173 | -------------------------------------------------------------------------------- /17. PHP中的yield(上).md: -------------------------------------------------------------------------------- 1 | 其实,我并不是因为迭代或者生成器或者研究PHP手册才认识的yield,要不是协程,我到现在也不知道PHP中还有yield这么个鬼东西。人家这个东西是从PHP 5.5就开始引入了,官方名称叫做生成器。你要说为什么5.5年代的东西,现在才拿出来。我还想问你哟,PHP 5.3就有了的namespace为毛到最近这几年才开始正式投产。 2 | 3 | 那么,问题来了,这东西到底是有何用? 4 | 5 | 先来感受一个问题,给你100Kb的内存(是的,你没有看错,就是100Kb),然后让你迭代输出一个从1开始一直到10000的数组,步进为1。 6 | 7 | 愈先迭代数组,必先创造数组。 8 | 9 | 所以,脑门一拍,代码一坨如下: 10 | 11 | ```php 12 | valid() ){ 87 | echo $generator->current().PHP_EOL; 88 | $generator->next(); 89 | } 90 | ``` 91 | 92 | 运行结果如下所示: 93 | 94 | ![](http://static.ti-node.com/6426627373801668608) 95 | 96 | 重点来了:这个yield_range函数似乎能够记住它上一次运行到哪儿了,上一次运行的结果是什么,然后紧接着在下一次运行的时候继续从上次终止的地方继续开始。这不是普通的PHP函数可以做得到的! 97 | 98 | 我们知道,操作系统在调度进程的时候,会触发一个叫做“进程上下文切换”的概念。比如CPU从进程A调度给进程B了,那么当再次从进程B调度给进程A的时候,当初进程A运行到哪儿了、临时的数据结果是什么都是需要被还原的,不然,一切都要从头,那就要出大问题了。而,这个yield关键字,似乎在用户态(非系统内核级)就可以实现这个概念。所以说,用yield搞迭代,怕是真的很没出息的一件事,它能做的太多。 99 | 100 | 紧接着,我们需要认识一个生成器对象的一个方法,叫做send,简单看下下面这坨代码: 101 | 102 | ```php 103 | send( $generator->current() * 10 ); 113 | ``` 114 | 115 | 运行结果如图所示: 116 | 117 | ![](http://static.ti-node.com/6426631112352595969) 118 | 119 | send方法可以修改yield的返回值,但是,你也不能想当然,比如下面这坨代码,你们以为运行结果是什么样呢? 120 | 121 | ```php 122 | send( $generator->current() * 10 ); 133 | } 134 | ``` 135 | 136 | 本来以为运行结果是类似于这样的: 137 | 138 | ```php 139 | 除了装逼之外,甩锅也是有打脸风险的 10 | 11 | 那篇坑里,内容和你能在百毒上搜索到的大多数文章都是差不多的,不过我那篇坑标题起得好:《yield是个什么玩意(上)》,也就是暗示大家还有下篇,所以起标题也是需要一定技术含量的。 12 | 13 | 我坚信,在座的各位辣鸡在看完上篇坑文后最想说的注定是泰迪熊这句话(这是文化属性,不以各位的意志而转移): 14 | 15 | ![](http://static.ti-node.com/6510697997595049984) 16 | 17 | 回到今天主旨上来,强调几点: 18 | 19 | - 虽然文章标题中有“yield和协程”这样的关键字,但实际上yield并不是协程,看起来有不少人直接将yield和协程划了等号。yield的本质是生成器,英文名字叫做Generator。 20 | 21 | - yield只能用在function中,但用了yield就已经不是传统意义上的function了,同时如果你企图在function之外的其他地方用yield,你会被打脸。 22 | 23 | - yield的最重要作用就是:自己中断一坨代码的执行,然后主动让出CPU控制权给路人甲;然后又能通过一些方式从刚才中断的地方恢复运行。这个就比较屌了,假如你请求了一个费时10s的服务器API,此时是可以让出CPU给路人甲。粗暴地说上面的过程就算是协程的基本概念。 24 | 25 | 26 | 27 | 多线程和多进程都是操作系统参与的调度,而协程是用户自主实现的调度,协程的关键点实际上是“用户层实现自主调度”,大概有“翻身农奴把歌唱”的意思。 28 | 29 | 下面我通过一坨代码来体会一把“翻身农奴”,你们感受一下: 30 | ```php 31 | current(); 57 | // 这会儿我可以让task2介入进来了 58 | echo $task2->current(); 59 | // task1恢复中断 60 | $task1->next(); 61 | // task2恢复中断 62 | $task2->next(); 63 | } 64 | ``` 65 | 上面代码执行结果如下图: 66 | 67 | ![](http://static.ti-node.com/6510698099164315649) 68 | 69 | 70 | 虽然我话都说到这里了,但是肯定还是有人get不到“所以,到底发生了什么?”。你要知道,如果function gen1和function gen2中没有yield,而是普通函数,你是无法中断其中的for循环的,诸如下面这样的代码: 71 | 72 | ```php 73 | 我似乎已然精通了yield 95 | 96 | 写到这里后我也开始蹩了,和以往的憋了三天蹦不出来个屁有所不同,我这次蹩出了一个比较典型的应用场景:curl。下面我们基于上面那坨辣鸡代码将gen1修改为一个耗时curl网络请求,gen2将向一个文本文件中写内容,我们的目的就是在耗时的curl开始后主动让出CPU,让gen2去写文件,以实现CPU的最大化利用。 97 | 98 | ```php 99 | 0 ); 112 | $ret = curl_multi_getcontent( $ch1 ); 113 | echo $ret.PHP_EOL; 114 | return false; 115 | } 116 | function gen2() { 117 | for ( $i = 1; $i <= 10; $i++ ) { 118 | echo "gen2 : {$i}".PHP_EOL; 119 | file_put_contents( "./yield.log", "gen2".$i, FILE_APPEND ); 120 | yield; 121 | } 122 | } 123 | $gen1 = gen1( $mh, $ch1 ); 124 | $gen2 = gen2(); 125 | while( true ) { 126 | echo $gen1->current(); 127 | echo $gen2->current(); 128 | $gen1->next(); 129 | $gen2->next(); 130 | } 131 | ``` 132 | 133 | 上面的代码,运行以后,我们再等待curl发起请求的5秒钟内,同时可以完成文件写入功能,如果换做平时的PHP程序,就只能是先阻塞等待curl拿到结果后才能完成文件写入。 134 | 135 | 文章太长,就像“老太太的裹脚布一样,又臭又长”,所以,最后再对代码做个极小幅度的改动就收尾不写了! 136 | 137 | ```php 138 | 0 ); 152 | $ret = curl_multi_getcontent( $ch1 ); 153 | echo $ret.PHP_EOL; 154 | return false; 155 | } 156 | function gen2() { 157 | for ( $i = 1; $i <= 10; $i++ ) { 158 | echo "gen2 : {$i}".PHP_EOL; 159 | file_put_contents( "./yield.log", "gen2".$i, FILE_APPEND ); 160 | $rs = yield; 161 | echo "外部发送数据{$rs}".PHP_EOL; 162 | } 163 | } 164 | $gen1 = gen1( $mh, $ch1 ); 165 | $gen2 = gen2(); 166 | while( true ) { 167 | echo $gen1->current(); 168 | echo $gen2->current(); 169 | $gen1->send("gen1"); 170 | $gen2->send("gen2"); 171 | } 172 | ``` 173 | 174 | 我们修改了内容: 175 | 176 | 将$gen1->next()修改成了$gen1->send("gen1") 177 | 178 | 在function gen1中yield有了返回值,并且将返回值打印出来 179 | 180 | 181 | 182 | 这件事情告诉我们:yield和send,是可以双向通信的,同时告诉我们send可以用来恢复原来中断的代码,而且在恢复中断的同时可以携带信息回去。写到这里,你是不是觉得这玩意的可利用价值是不是比原来高点儿了? 183 | 184 | 我知道,有人肯定叨叨了:“老李,你代码特么写的真是辣鸡啊!你之前保证过了的 --- 只在公司生产环境写辣鸡代码的。可你看看你这辣鸡光环到笼罩都到demo里了,你连demo都不放过了!你怎么说?!”。兄dei,“又不是不能用”。而且我告诉你,上面这点儿curl demo来讲明白yield还是不够的,后面还有两三篇yield呢,照样是烂代码恶心死你,爱看不看。我劝你心放宽,你想想你这么烂的代码都经历了,还有什么不能经历的? 185 | 186 | 文章最后补个小故事:其实yield是PHP 5.5就已经添加进来了,这个模块的作者叫做Nikita Popov,网络上的名称是Nikic。我们知道PHP7这一代主力是惠新宸,下一代PHP主力就是Nikic了。早在2012年,Nikic就发表了一篇关于PHP yield多任务的文章,链接我贴出来大家共赏一下 --- http://nikic.github.io/2012/12/22/Cooperative-multitasking-using-coroutines-in-PHP.html 187 | 188 | -------------------------------------------------------------------------------- /2. php多进程初探---开篇.md: -------------------------------------------------------------------------------- 1 | 实际上PHP是有多线程的,只是很多人不常用。使用PHP的多线程首先需要下载安装一个线程安全版本(ZTS版本)的PHP,然后再安装pecl的[pthread扩展](http://pecl.php.net/package/pthreads "pthread扩展")。 2 | 3 | 实际上PHP是有多进程的,有一些人再用,总体来说php的多进程还算凑合,只需要在安装PHP的时候开启pcntl模块(是不是跟UNIX中的fcntl有点儿.... ....)即可。在*NIX下,在终端命令行下使用php -m就可以看到是否开启了pcntl模块。 4 | 5 | 所以我们只说php的多进程,至于php多线程就暂时放到一边儿。 6 | 7 | **注意:不要在apache或者fpm环境下使用php多进程,这将会产生不可预估的后果。** 8 | 9 | **进程是程序执行的实例**,举个例子有个程序叫做 “ 病毒.exe ”,这个程序平时是以文件形式存储在硬盘上,当你双击运行后,就会形成一个该程序的进程。系统会给每一个进程分配一个唯一的非负整数用来标记进程,这个数字称作进程ID。当该进程被杀死或终止后,其进程ID就会被系统回收,然后分配给新的其余的进程。 10 | 11 | 说了这么多,这鬼东西有什么用吗?我平时用CI、YII写个CURD跟这个也没啥关联啊。实际上,如果你了解APACHE PHP MOD或者FPM就知道这些东西就是多进程实现的。以FPM为例,一般都是nginx作为http服务器挡在最前面,静态文件请求则nginx自行处理,遇到php动态请求则转发给php-fpm进程来处理。如果你的php-fpm配置只开了5个进程,如果处理任意一个用户的请求都需要1秒钟,那么5个fpm进程1秒中就最多只能处5个用户的请求。所以结论就是:如果要单位时间内干活更快更多,就需要更多的进程,总之一句话就是多进程可以加快任务处理速度。 12 | 13 | 在php中我们使用pcntl_fork()来创建多进程(在*NIX系统的C语言编程中,已有进程通过调用fork函数来产生新的进程)。fork出来新进程则成为子进程,原进程则成为父进程,子进程拥有父进程的副本。这里要注意: 14 | - 子进程与父进程共享程序正文段 15 | - 子进程拥有父进程的数据空间和堆、栈的副本,注意是副本,不是共享 16 | - 父进程和子进程将继续执行fork之后的程序代码 17 | - fork之后,是父进程先执行还是子进程先执行无法确认,取决于系统调度(取决于信仰) 18 | 19 | 这里说子进程拥有父进程数据空间以及堆、栈的副本,实际上,在大多数的实现中也并不是真正的完全副本。更多是采用了COW(Copy On Write)即写时复制的技术来节约存储空间。简单来说,如果父进程和子进程都不修改这些 数据、堆、栈 的话,那么父进程和子进程则是暂时共享同一份 数据、堆、栈。只有当父进程或者子进程试图对 数据、堆、栈 进行修改的时候,才会产生复制操作,这就叫做写时复制。 20 | 21 | 在调用完pcntl_fork()后,该函数会返回两个值。在父进程中返回子进程的进程ID,在子进程内部本身返回数字0。由于多进程在apache或者fpm环境下无法正常运行,所以大家一定要在php cli环境下执行下面php代码。 22 | 23 | 第一段代码,我们来说明在程序从pcntl_fork()后父进程和子进程将各自继续往下执行代码: 24 | ```php 25 | 0 ){ 28 | echo "我是父亲".PHP_EOL; 29 | } else if( 0 == $pid ) { 30 | echo "我是儿子".PHP_EOL; 31 | } else { 32 | echo "fork失败".PHP_EOL; 33 | } 34 | ``` 35 | 将文件保存为test.php,然后在使用cli执行,结果如下图所示: 36 | ![](https://static.ti-node.com/6374508376738496512) 37 | 38 | 第二段代码,用来说明子进程拥有父进程的数据副本,而并不是共享: 39 | ```php 40 | 0 ){ 45 | $number += 1; 46 | echo "我是父亲,number+1 : { $number }".PHP_EOL; 47 | } else if( 0 == $pid ) { 48 | $number += 2; 49 | echo "我是儿子,number+2 : { $number }".PHP_EOL; 50 | } else { 51 | echo "fork失败".PHP_EOL; 52 | } 53 | ``` 54 | ![](https://static.ti-node.com/6374520918680535040) 55 | 56 | 第三段代码,比较容易让人思维混乱,pcntl_fork()配合for循环来做些东西,问题来了:会显示几次 “ 儿子 ”? 57 | ```php 58 | 0 ){ 62 | // do nothing ... 63 | } else if( 0 == $pid ){ 64 | echo "儿子".PHP_EOL; 65 | } 66 | } 67 | ``` 68 | 上面代码执行结果如下: 69 | ![](https://static.ti-node.com/6374530342694420480) 70 | 71 | 仔细数数,竟然是显示了7次 “ 儿子 ”。好奇怪,难道不是3次吗?... ... 72 | 下面我修改一下代码,结合下面的代码,再思考一下为什么会产生7次而不是3次。 73 | ```php 74 | 0 ){ 78 | // do nothing ... 79 | } else if( 0 == $pid ){ 80 | echo "儿子".PHP_EOL; 81 | exit; 82 | } 83 | } 84 | ``` 85 | 执行结果如下图所示: 86 | ![](https://static.ti-node.com/6374530960842555392) 87 | 88 | 前面强调过:**父进程和子进程将继续执行fork之后的程序代码(包含pcntl_fork函数)**。这里就不解释,实在想不明白的,可以动手自己画画思考一下。 89 | 90 | ___ 91 | 这里应该还是要解释一波的 92 | * i=1的时候父进程的pid不为0 这时候fork了一个pid=0的子进程a, 子进程数量1 93 | * i=2的时候父进程fork了一个子进程b, 子进程a又fork了一个子进程c, 子进程数量1+2 94 | * i=3的时候父进程fork了一个子进程d, a子进程fork了e, b子进程fork了f, c子进程fork了g子进程数量1+2+4=7 95 | 96 | * 至于在fork子进程退出的时候 i=1 =2 =3的时候都只有一个父进程fork一个子进程 所以只有三个儿子 97 | ___ 98 | 99 | 为了避免写成臭尾理论文儿,这里强行断篇分割一下,下一章说僵尸进程和孤儿进程的一些恩怨情仇。 100 | 101 | ----- 102 | 感谢workerman1群成员“小菜鸟”(不是我叫TA小菜鸟,是TA昵称就是小菜鸟)指出错误~ 103 | -------------------------------------------------------------------------------- /3. php多进程初探---孤儿和僵尸.md: -------------------------------------------------------------------------------- 1 | 2 | ## 孤儿进程和僵尸进程 3 | 上篇我整篇尬聊的都是pcntl_fork(),只管fork生产,不管产后护理,实际上这样并不符合主流价值观,而且,操作系统本身资源有限,这样无限生产不顾护理,操作系统也会吃不消的。 4 | 5 | 孤儿进程是指父进程在fork出子进程后,自己先完了。这个问题很尴尬,因为子进程从此变得无依无靠、无家可归,变成了孤儿。用术语来表达就是,父进程在子进程结束之前提前退出,这些子进程将由init(进程ID为1)进程收养并完成对其各种数据状态的收集。init进程是Linux系统下的奇怪进程,这个进程是以普通用户权限运行但却具备超级权限的进程,简单地说,这个进程在Linux系统启动的时候做初始化工作,比如运行getty、比如会根据/etc/inittab中设置的运行等级初始化系统等等,当然了,还有一个作用就是如上所说的:收养孤儿进程。 6 | 7 | 僵尸进程是指父进程在fork出子进程,而后子进程在结束后,父进程并没有调用wait或者waitpid等完成对其清理善后工作,导致该子进程进程ID、文件描述符等依然保留在系统中,极大浪费了系统资源。所以,僵尸进程是对系统有危害的,而孤儿进程则相对来说没那么严重。在Linux系统中,我们可以通过ps -aux来查看进程,如果有[Z+]标记就是僵尸进程。 8 | 9 | 在PHP中,父进程对子进程的状态收集等是通过pcntl_wait()和pcntl_waitpid()等完成的。依然还是要通过代码还演示说明: 10 | 11 | 演示并说明孤儿进程的出现,并演示孤儿进程被init进程收养: 12 | ```php 13 | 0 ){ 16 | // 显示父进程的进程ID,这个函数可以是getmypid(),也可以用posix_getpid() 17 | echo "Father PID:".getmypid().PHP_EOL; 18 | // 让父进程停止两秒钟,在这两秒内,子进程的父进程ID还是这个父进程 19 | sleep( 2 ); 20 | } else if( 0 == $pid ) { 21 | // 让子进程循环10次,每次睡眠1s,然后每秒钟获取一次子进程的父进程进程ID 22 | for( $i = 1; $i <= 10; $i++ ){ 23 | sleep( 1 ); 24 | // posix_getppid()函数的作用就是获取当前进程的父进程进程ID 25 | echo posix_getppid().PHP_EOL; 26 | } 27 | } else { 28 | echo "fork error.".PHP_EOL; 29 | } 30 | ``` 31 | 运行结果如下图: 32 |
33 | ![](https://static.ti-node.com/6375549819984805889) 34 | 35 | 可以看到,前两秒内,子进程的父进程进程ID为4129,但是从第三秒开始,由于父进程已经提前退出了,子进程变成孤儿进程,所以init进程收养了子进程,所以子进程的父进程进程ID变成了1。 36 | 37 | 演示并说明僵尸进程的出现,并演示僵尸进程的危害: 38 | ```php 39 | 0 ){ 42 | // 下面这个函数可以更改php进程的名称 43 | cli_set_process_title('php father process'); 44 | // 让主进程休息60秒钟 45 | sleep(60); 46 | } else if( 0 == $pid ) { 47 | cli_set_process_title('php child process'); 48 | // 让子进程休息10秒钟,但是进程结束后,父进程不对子进程做任何处理工作,这样这个子进程就会变成僵尸进程 49 | sleep(10); 50 | } else { 51 | exit('fork error.'.PHP_EOL); 52 | } 53 | ``` 54 | 55 | 运行结果如下图: 56 |
57 | ![](https://static.ti-node.com/6375554233759956993) 58 | 59 | 通过执行ps -aux命令可以看到,当程序在前十秒内运行的时候,php child process的状态列为[S+],然而在十秒钟过后,这个状态变成了[Z+],也就是变成了危害系统的僵尸进程。 60 | 61 | 那么,问题来了?如何避免僵尸进程呢?PHP通过pcntl_wait()和pcntl_waitpid()两个函数来帮我们解决这个问题。了解Linux系统编程的应该知道,看名字就知道这其实就是PHP把C语言中的wait()和waitpid()包装了一下。 62 | 63 | 通过代码演示pcntl_wait()来避免僵尸进程,在开始之前先简单普及一下pcntl_wait()的相关内容:这个函数的作用就是 “ 等待或者返回子进程的状态 ”,当父进程执行了该函数后,就会阻塞挂起等待子进程的状态一直等到子进程已经由于某种原因退出或者终止。换句话说就是如果子进程还没结束,那么父进程就会一直等等等,如果子进程已经结束,那么父进程就会立刻得到子进程状态。这个函数返回退出的子进程的进程ID或者失败返回-1。 64 | 65 | 我们将第二个案例中代码修改一下: 66 | ```php 67 | 0 ){ 70 | // 下面这个函数可以更改php进程的名称 71 | cli_set_process_title('php father process'); 72 | 73 | // 返回$wait_result,就是子进程的进程号,如果子进程已经是僵尸进程则为0 74 | // 子进程状态则保存在了$status参数中,可以通过pcntl_wexitstatus()等一系列函数来查看$status的状态信息是什么 75 | $wait_result = pcntl_wait( $status ); 76 | print_r( $wait_result ); 77 | print_r( $status ); 78 | 79 | // 让主进程休息60秒钟 80 | sleep(60); 81 | } else if( 0 == $pid ) { 82 | cli_set_process_title('php child process'); 83 | // 让子进程休息10秒钟,但是进程结束后,父进程不对子进程做任何处理工作,这样这个子进程就会变成僵尸进程 84 | sleep(10); 85 | } else { 86 | exit('fork error.'.PHP_EOL); 87 | } 88 | ``` 89 | 90 | 将文件保存为wait.php,然后php wait.php,在另外一个终端中通过ps -aux查看,可以看到在前十秒内,php child process是[S+]状态,然后十秒钟过后进程消失了,也就是被父进程回收了,没有变成僵尸进程。 91 | 92 |
93 | 94 | ![](https://static.ti-node.com/6375564405479833601) 95 | 96 | 但是,pcntl_wait()有个很大的问题,就是阻塞。父进程只能挂起等待子进程结束或终止,在此期间父进程什么都不能做,这并不符合多快好省原则,所以pcntl_waitpid()闪亮登场。pcntl_waitpid( $pid, &$status, $option = 0 )的第三个参数如果设置为WNOHANG,那么父进程不会阻塞一直等待到有子进程退出或终止,否则将会**和pcntl_wait()的表现类似。** 97 | 98 | 修改第三个案例的代码,但是,我们并不添加WNOHANG,演示说明pcntl_waitpid()功能: 99 | ```php 100 | 0 ){ 103 | // 下面这个函数可以更改php进程的名称 104 | cli_set_process_title('php father process'); 105 | 106 | // 返回值保存在$wait_result中 107 | // $pid参数表示 子进程的进程ID 108 | // 子进程状态则保存在了参数$status中 109 | // 将第三个option参数设置为常量WNOHANG,则可以避免主进程阻塞挂起,此处父进程将立即返回继续往下执行剩下的代码 110 | $wait_result = pcntl_waitpid( $pid, $status ); 111 | var_dump( $wait_result ); 112 | var_dump( $status ); 113 | 114 | // 让主进程休息60秒钟 115 | sleep(60); 116 | 117 | } else if( 0 == $pid ) { 118 | cli_set_process_title('php child process'); 119 | // 让子进程休息10秒钟,但是进程结束后,父进程不对子进程做任何处理工作,这样这个子进程就会变成僵尸进程 120 | sleep(10); 121 | } else { 122 | exit('fork error.'.PHP_EOL); 123 | } 124 | ``` 125 | 126 | 下面是运行结果,一个执行php程序的终端窗口,另一个是ps -aux终端窗口。实际上可以看到主进程是被阻塞的,一直到第十秒子进程退出了,父进程不再阻塞: 127 |
128 | ![](https://static.ti-node.com/6375667961356615681) 129 |
130 | ![](https://static.ti-node.com/6375668057506840577) 131 | 132 | 那么我们修改第四段代码,添加第三个参数WNOHANG,代码如下: 133 | ```php 134 | 0 ){ 137 | // 下面这个函数可以更改php进程的名称 138 | cli_set_process_title('php father process'); 139 | 140 | // 返回值保存在$wait_result中 141 | // $pid参数表示 子进程的进程ID 142 | // 子进程状态则保存在了参数$status中 143 | // 将第三个option参数设置为常量WNOHANG,则可以避免主进程阻塞挂起,此处父进程将立即返回继续往下执行剩下的代码 144 | $wait_result = pcntl_waitpid( $pid, $status, WNOHANG ); 145 | 146 | //$wait_result大于0代表子进程已退出,返回的是子进程的pid,非阻塞时0代表没取到退出子进程,为什么会没有取到子进程,因为当时子进程没有退出,在休眠sleep 147 | 148 | var_dump( $wait_result ); 149 | var_dump( $status ); 150 | echo "不阻塞,运行到这里".PHP_EOL; 151 | 152 | // 让主进程休息60秒钟 153 | sleep(60); 154 | 155 | } else if( 0 == $pid ) { 156 | cli_set_process_title('php child process'); 157 | // 让子进程休息10秒钟,但是进程结束后,父进程不对子进程做任何处理工作,这样这个子进程就会变成僵尸进程 158 | sleep(10); 159 | } else { 160 | exit('fork error.'.PHP_EOL); 161 | } 162 | ``` 163 | 164 | 下面是运行结果,一个执行php程序的终端窗口,另一个是ps -aux终端窗口。可以看到主进程不再阻塞: 165 |
166 | ![](https://static.ti-node.com/6375670669899726848) 167 |
168 | ![](https://static.ti-node.com/6375670752070336513) 169 | 170 | 问题出现了,竟然php child process进程状态竟然变成了[Z+],这是怎么搞得?回头分析一下代码: 171 | 172 | 我们看到子进程是睡眠了十秒钟,而父进程在执行pcntl_waitpid()之前没有任何睡眠且本身不再阻塞,所以,主进程自己先执行下去了,而子进程在足足十秒钟后才结束,进程状态自然无法得到回收。如果我们将代码修改一下,就是在主进程的pcntl_waitpid()前睡眠15秒钟,这样就可以回收子进程了。但是即便这样修改,细心想的话还是会有个问题,那就是在子进程结束后,在父进程执行pcntl_waitpid()回收前,有五秒钟的时间差,在这个时间差内,php child process也将会是僵尸进程。那么,pcntl_waitpid()如何正确使用啊?这样用,看起来毕竟不太科学。 173 | 174 | 那么,是时候引入信号学了! 175 | -------------------------------------------------------------------------------- /4. php多进程初探---信号.md: -------------------------------------------------------------------------------- 1 | 上一篇尬聊了通篇的pcntl_wait()和pcntl_waitpid(),就是为了解决僵尸进程的问题,但最后看起来还是有一些遗留问题,而且因为嘴欠在上篇文章的结尾出也给了解决方案:信号。 2 | 3 | 信号是一种软件中断,也是一种非常典型的异步事件处理方式。在 *nix 系统诞生的混沌之初,信号的定义是比较混乱的,而且最关键是不可靠,这是一个很严重的问题。所以在后来的POSIX标准中,对信号做了标准化同时也各个发行版的 *nix 也都提供大量可靠的信号。每种信号都有自己的名字,大概如SIGTERM、SIGHUP、SIGCHLD等等,在 *nix 中,这些信号本质上都是整形数字(游有心情的可以参观一下signal.h系列头文件)。 4 | 5 | 信号的产生是有多种方式的,下面是常见的几种: 6 | - 键盘上按某些组合键,比如Ctrl+C或者Ctrl+D等,会产生SIGINT信号。 7 | - 使用posix kill调用,可以向某个进程发送指定的信号。 8 | - 远程ssh终端情况下,如果你在服务器上执行了一个阻塞的脚本,正在阻塞过程中你关闭了终端,可能就会产生SIGHUP信号。 9 | - 硬件也会产生信号,比如OOM了或者遇到除0这种情况,硬件也会向进程发送特定信号。 10 | 11 | 而进程在收到信号后,可以有如下三种响应: 12 | - 直接忽略,不做任何反映。就是俗称的完全不鸟。但是有两种信号,永远不会被忽略,一个是SIGSTOP,另一个是SIGKILL,因为这两个进程提供了向内核最后的可靠的结束进程的办法。 13 | - 捕捉信号并作出相应的一些反应,具体响应什么可以由用户自己通过程序自定义。 14 | - 系统默认响应。大多数进程在遇到信号后,如果用户也没有自定义响应,那么就会采取系统默认响应,大多数的系统默认响应就是终止进程。 15 | 16 | 用人话来表达,就是说假如你是一个进程,你正在干活,突然施工队的喇叭里冲你嚷了一句:“吃饭了!”,于是你就放下手里的活儿去吃饭。你正在干活,突然施工队的喇叭里冲你嚷了一句:“发工资了!”,于是你就放下手里的活儿去领工资。你正在干活,突然施工队的喇叭里冲你嚷了一句:“有人找你!”,于是你就放下手里的活儿去看看是谁找你什么事情。当然了,你很任性,那是完全可以不鸟喇叭里喊什么内容,也就是忽略信号。也可以更任性,当喇叭里冲你嚷“吃饭”的时候,你去就不去吃饭,你去睡觉,这些都可以由你来。而你在干活过程中,从来不会因为要等某个信号就不干活了一直等信号,而是信号随时随地都可能会来,而你只需要在这个时候作出相应的回应即可,所以说,信号是一种软件中断,也是一种异步的处理事件的方式。 17 | 18 | 回到上文所说的问题,就是子进程在结束前,父进程就已经先调用了pcntl_waitpid(),导致子进程在结束后依然变成了僵尸进程。实际上在父进程不断while循环调用pcntl_waitpid()是个解决办法,大概代码如下: 19 | ```php 20 | $pid = pcntl_fork(); 21 | if( 0 > $pid ){ 22 | exit('fork error.'.PHP_EOL); 23 | } else if( 0 < $pid ) { 24 | // 在父进程中 25 | cli_set_process_title('php father process'); 26 | // 父进程不断while循环,去反复执行pcntl_waitpid(),从而试图解决已经退出的子进程 27 | while( true ){ 28 | sleep( 1 ); 29 | $wait_result=pcntl_waitpid( $pid, $status, WNOHANG ); 30 | 31 | //会输出20个0,第21个是子进程退出后返回的子进程号,第22个开始输出-1,那为何第22个开始一直是-1,因为当找不到子进程时,或者错误时是返回-1的,而0只代表当前子进程没有退出 32 | echo $wait_result.PHP_EOL; 33 | } 34 | } else if( 0 == $pid ) { 35 | // 在子进程中 36 | // 子进程休眠20秒钟后直接退出 37 | cli_set_process_title('php child process'); 38 | sleep( 20 ); 39 | exit; 40 | } 41 | ``` 42 | 下图是运行结果: 43 |
44 | 45 | ![](http://static.ti-node.com/6379568984051679233) 46 | 47 | 解析一下这个结果,我先后三次执行了ps -aux | grep php去查看这两个php进程。 48 | - 第一次:子进程正在休眠中,父进程依旧在循环中。 49 | - 第二次:子进程已经退出了,父进程依旧在循环中,但是代码还没有执行到pcntl_waitpid(),所以在子进程退出后到父进程执行回收前这段空隙内子进程变成了僵尸进程。 50 | - 第三次:此时父进程已经执行了pcntl_waitpid(),将已经退出的子进程回收,释放了pid等资源。 51 | 52 | 但是这样的代码有一个缺陷,实际上就是子进程已经退出的情况下,主进程还在不断while pcntl_waitpid()去回收子进程,这是一件很奇怪的事情,并不符合社会主义主流价值观,不低碳不节能,代码也不优雅,不好看。所以,应该考虑用更好的方式来实现。那么,我们篇头提了许久的信号终于概要出场了。 53 | 54 | 现在让我们考虑一下,为何信号可以解决“不低碳不节能,代码也不优雅,不好看”的问题。子进程在退出的时候,会向父进程发送一个信号,叫做SIGCHLD,那么父进程一旦收到了这个信号,就可以作出相应的回收动作,也就是执行pcntl_waitpid(),从而解决掉僵尸进程,而且还显得我们代码优雅好看节能环保。 55 | 56 | 梳理一下流程,子进程向父进程发送SIGCHLD信号是对人们来说是透明的,也就是说我们无须关心。但是,我们需要给父进程安装一个响应SIGCHLD信号的处理器,除此之外,还需要让这些信号处理器运行起来,安装上了不运行是一件尴尬的事情。那么,在php里给进程安装信号处理器使用的函数是pcntl_signal(),让信号处理器跑起来的函数是pcntl_signal_dispatch()。 57 | - pcntl_signal(),安装一个信号处理器,具体说明是pcntl_signal ( int $signo , callback $handler [, bool $restart_syscalls = true ] ),参数signo就是信号,callback则是响应该信号的代码段,返回bool值。 58 | - pcntl_signal_dispatch(),调用每个等待信号通过pcntl_signal() 安装的处理器,参数为void,返回bool值。 59 | 60 | 下面结合新引入的两个函数来解决一下楼上的丑陋代码: 61 | ```php 62 | $pid = pcntl_fork(); 63 | if( 0 > $pid ){ 64 | exit('fork error.'.PHP_EOL); 65 | } else if( 0 < $pid ) { 66 | // 在父进程中 67 | // 给父进程安装一个SIGCHLD信号处理器 68 | pcntl_signal( SIGCHLD, function() use( $pid ) { 69 | echo "收到子进程退出".PHP_EOL; 70 | pcntl_waitpid( $pid, $status, WNOHANG ); 71 | } ); 72 | cli_set_process_title('php father process'); 73 | // 父进程不断while循环,去反复执行pcntl_waitpid(),从而试图解决已经退出的子进程 74 | while( true ){ 75 | sleep( 1 ); 76 | // 注释掉原来老掉牙的代码,转而使用pcntl_signal_dispatch() 77 | //pcntl_waitpid( $pid, $status, WNOHANG ); 78 | pcntl_signal_dispatch(); 79 | } 80 | } else if( 0 == $pid ) { 81 | // 在子进程中 82 | // 子进程休眠20秒钟后直接退出 83 | cli_set_process_title('php child process'); 84 | sleep( 20 ); 85 | exit; 86 | } 87 | ``` 88 | 运行结果如下: 89 | 90 | 91 | ![](http://static.ti-node.com/6379579752918810624) 92 |
93 | 94 | ![](http://static.ti-node.com/6379579817108439041) 95 |
96 | 97 | -------------------------------------------------------------------------------- /5. swoole的进程模型.md: -------------------------------------------------------------------------------- 1 | ##### 很多phper一直停留在php web开发的mvc CURD中,也听到很多人对自己陷入这种困境中多有不满,但又不知道如何提高自己,摆脱困境。活脱脱就像一直趴在玻璃上的苍蝇,前途一片光明,就是飞不出去,可悲可叹。 2 | ##### 话说回来,实际上做到一名合格的CURDer也并不是一件容易的事情,万万不可眼高手低。 3 | ##### 如果想提高自己,也不一定非要通过工作,我认为一个人的提高更多是在非工作环境中。php的开发人员开始通过尝试接触并使用swoole或者workerman(0_0 或者等我的php socket框架,啦啦啦 0_0)来提高自己的认知水准,这就像2017年那部挺火的电视剧中那样,鼓励大家学英语,我认为挺好的。 4 | ##### 学习swoole前,我建议大家有最好有如下知识储备或者有准备学习如下知识的准备: 5 | - 多进程多线程知识储备,多进程进程间通信,多进程多线程锁相关,进程管理等 6 | - 服务器IO模型相关知识储备 7 | - 熟练的*NIX系统操作以及良好的使用习惯 8 | - TCP协议以及socket相关知识储备 9 | 10 | ##### 考虑到本PO主原来叨叨了一坨与进程相关的知识,所以这篇就来一坨与swoole进程相关的内容,这也是要学习并好好利用swoole的第一课,打好基础,会对你使用有着更大的帮助。 11 | ##### 先贴一张图,是从官方wiki上偷过来的,不得不说,如果你去swoole官方wiki上找,都不一定能找到这个图,我的言下之意就是老韩写的文档组织方式以及内容确实比较混乱,没有对比就没有伤害,比如人家workerman的文档的,我贴出来,你们感受一下: 12 | - https://wiki.swoole.com/ 13 | - http://doc3.workerman.net/ 14 | 15 | ##### 啦啦啦,盗图狗要贴图了: 16 | ![](http://static.ti-node.com/6379894692196122625) 17 | 18 | --- 19 | 20 | ![](http://static.ti-node.com/6379897089064697857) 21 | 22 | ##### 结合上图开始简单描述一下swoole中进程角色们: 23 | - master进程 24 | 这个进程比较复杂,也是我认为最核心的进程,这是一个包含多线程的进程,分别是一个主线程和n个reactor线程(数量可以配置)。其中,主线程用于accept新的连接,然后评估一下每个reactor线程负责维护的连接数,然后分配给数量最少的那个reactor线程,最大程度保证每个reactor线程的负载量是均衡的。本质上讲,一旦一个socket可读或者可写了,就由reactor线程发送给worker进程或者发送会客户端。除此之外,主线程还负责对所有信号的接管,避免reactor线程收到信号的打扰中断。说的洋气点儿就是:master进程负责了连接的accept、托管、socket的可读可写(数据的发送和接受),本质上讲,master进程负责了IO。还需要注意一点儿的是reactor线程是彻底的全异步非阻塞工作方式。 25 | 26 | - manager进程 27 | manager进程是worker进程和taskworker进程的妈,说的洋气点儿就是manager进程fork出来了worker进程和taskworker进程,生出来了就得管,所以,manager进程得负责对worker进程和taskworker进程的抚养义务,具体包括监控它们的状态、当它们意外挂了后重新拉起一个新的进程(避免了僵尸进程)、平滑重启(就是传说中的reload)。 28 | 29 | - worker进程 30 | worker进程是manager进程fork出来的,这个进程说白了就是搬砖干活(官方文档中屡次提到的业务代码),其实就是平时码的那些curd业务逻辑代码,懂了吧?只不过worker进程比较diao的是,这个进程可以用异步方式去工作,也可以用同步方式去工作。如果听不懂什么意思,那就先背过,先混个脸熟再说。 31 | 32 | - taskworker进程 33 | taskworker进程(后文中称tasker进程)实际本质上也是worker进程,只不过是一种特殊的worker进程。如果你的worker进程中存在一些耗时耗力的操作,那么可以先抛给tasker进程,自己先去干别的,等tasker干完了,再由worker进程取回,非常diao。但是tasker进程只能工作在同步方式下,并不能使用异步。这就是为什么tasker进程不可以使用定时器,而worker进程可以使用定时器的原因。 34 | 35 | ##### 简单总结混在一起说下这几种进程之间是怎么搭配起来干活的。见说来说,就是master进程就是接活儿的销售,但是具体干活则由worker进程来做,如果worker进程感觉到某些流程太繁忙复杂就可以让tasker进程来做。而manager进程就是后勤worker进程和takser进程的人力资源保障部,负责他们的生死存亡和吃喝拉撒。 36 | 37 | --- 38 | -------------------------------------------------------------------------------- /6. php多进程初探---利用多进程开发点儿东西.md: -------------------------------------------------------------------------------- 1 | ##### 干巴巴地叨逼叨了这么久,时候表演真正的技术了! 2 | ##### 做个高端点儿的玩意吧,假如我们要做一个任务系统,这个系统可以在后台帮我们完成一大波(注意是一大波)数据的处理,那么我们自然想到,多开几个进程分开处理这些数据,同时我们不能执行了php task.php后终端挂起,万一一不小心关闭了终端都会导致任务失败,所以我们还要实现程序的daemon化。好啦,开始了! 3 | ##### 首先,我们第一步就得将程序daemon化了! 4 | ```php 5 | // 设置umask为0,这样,当前进程创建的文件权限则为777 6 | umask( 0 ); 7 | $pid = pcntl_fork(); 8 | if( $pid < 0 ){ 9 | exit('fork error.'); 10 | } else if( $pid > 0 ) { 11 | // 主进程退出 12 | exit(); 13 | } 14 | // 子进程继续执行 15 | 16 | // 最关键的一步来了,执行setsid函数! 17 | /* 18 | http://linux.die.net/man/2/setsid 19 | [setsid详解][1] 主要目的脱离终端控制,自立门户。 20 | 创建一个新的会话,而且让这个pid统治这个会话,他既是会话组长,也是进程组长。 21 | 而且谁也没法控制这个会话,除了这个pid。当然关机除外。。 22 | 这时可以成做pid为这个无终端的会话组长。 23 | 注意这个函数需要当前进程不是父进程,或者说不是会话组长。 24 | 在这里当然不是,因为父进程已经被kill 25 | 26 | 换句话来说就是 : 调用进程必须是非当前进程组组长,调用后,产生一个新的会话期,且该会话期中只有一个进程组,且该进程组组长为调用进程,没有控制终端,新产生的group ID 和 session ID 被设置成调用进程的PID 27 | */ 28 | if( !posix_setsid() ){ 29 | exit('setsid error.'); 30 | } 31 | 32 | // 理论上一次fork就可以了 33 | // 但是,二次fork,这里的历史渊源是这样的:在基于system V的系统中,通过再次fork,父进程退出,子进程继续 34 | // 保证形成的daemon进程绝对不会成为会话首进程,不会拥有控制终端。 35 | $pid = pcntl_fork(); 36 | if( $pid < 0 ){ 37 | exit('fork error'); 38 | } else if( $pid > 0 ) { 39 | // 主进程退出 40 | exit; 41 | } 42 | // 子进程继续执行 43 | // 给进程重新起个名字 44 | cli_set_process_title('php master process'); 45 | 46 | ``` 47 | 48 | ##### 加入我们fork出5个子进程就可以搞定这些任务,那么fork出5个子进程,同时父进程要负责这5个子进程的状态等。 49 | ```php 50 | // 由于*NIX好像并没有(如果有,请告知)可以获取父进程fork出所有的子进程的ID们的功能,所以这个需要我们自己来保存 51 | $child_pid = []; 52 | 53 | // 父进程安装SIGCHLD信号处理器并分发 54 | pcntl_signal( SIGCHLD, function(){ 55 | // 这里注意要使用global将child_pid全局化,不然读到去数组将为空,具体原因可以自己思考下 56 | global $child_pid; 57 | // 如果子进程的数量大于0,也就说如果还有子进程存活未 退出,那么执行回收 58 | $child_pid_num = count( $child_pid ); 59 | if( $child_pid_num > 0 ){ 60 | // 循环子进程数组 61 | foreach( $child_pid as $pid_key => $pid_item ){ 62 | $wait_result = pcntl_waitpid( $pid_item, $status, WNOHANG ); 63 | // 如果子进程被成功回收了,那么一定要将其进程ID从child_pid中移除掉 64 | /* 65 | 可能有朋友疑惑为什么要判断$wait_result == $pid_ite,也不知道这时候程序运行到哪里了, 66 | 大家是否还记得第四章php多进程初探---信号中提到循环while等待子进程被回收,出现20个0,第21个输出子进程号,所以这里foreach判断是否等于子进程号,-1 == $wait_result就不用多讲,也提到,子进程找不到了 67 | */ 68 | if( $wait_result == $pid_item || -1 == $wait_result ){ 69 | unset( $child_pid[ $pid_key ] ); 70 | } 71 | } 72 | } 73 | } ); 74 | 75 | // fork出5个子进程出来,并给每个子进程重命名 76 | for( $i = 1; $i <= 5; $i++ ){ 77 | $_pid = pcntl_fork(); 78 | if( $_pid < 0 ){ 79 | exit(); 80 | } else if( 0 == $_pid ) { 81 | // 重命名子进程 82 | cli_set_process_title('php worker process'); 83 | 84 | // 啦啦啦啦啦啦啦啦啦啦,请在此处编写你的业务代码 85 | // do something ... 86 | // 啦啦啦啦啦啦啦啦啦啦,请在此处编写你的业务代码 87 | 88 | // 子进程退出执行,一定要exit,不然就不会fork出5个而是多于5个任务进程了 89 | exit(); 90 | 91 | } else if( $_pid > 0 ) { 92 | // 将fork出的任务进程的进程ID保存到数组中 93 | $child_pid[] = $_pid; 94 | } 95 | } 96 | 97 | // 主进程继续循环不断派遣信号 98 | while( true ){ 99 | pcntl_signal_dispatch(); 100 | // 每派遣一次休眠一秒钟 101 | sleep( 1 ); 102 | } 103 | ``` 104 | -------------------------------------------------------------------------------- /7. php多进程初探---再次谈daemon进程.md: -------------------------------------------------------------------------------- 1 | ##### 其实前面是谈过一次daemon进程的,但是并涉及过多原理,但是并不影响使用。今天打算说说关于daemon进程更多的二三事,本质上说,如果你仅仅是简单实现利用一下daemon进程,这个不看也是可以的。 2 | ##### 杠真,*NIX真是波大精深,越是深入看越是发现它的diao。原理往往都是枯燥的,大家都不爱看,但这并不影响我坚持写自己对这些东西的理解。 3 | ##### 三个概念,理(bei)解(song)一下: 4 | - 进程组。一坨相关的进程可以组成一个进程组,每个进程组都会有一个组ID(正整数),每个进程组都会有一个组长进程,组长进程的ID等于进程组ID。组长进程可以创建新的进程组以及该进程组中的其他进程。一个进程组的是有生命周期的,即便是组长进程挂了,只有组里还有其他的活口,那就就算该进程组依然存活,只有到组里最后一个活口也挂了,那真的就是彻底没了。 5 | - 会话。一坨相关的进程组组成了一个会话。在*NIX下,是通过setsid()创建一个新的会话。但是值得注意的是,组长进程不能创建会话,简单理解就是在组长进程中,执行setsid函数会报错,这点很重要。所以一般都是组长进程执行fork,然后主进程退出,因为子进程的进程ID是新分配的,而子进程的进程组ID是继承父进程的,所以子进程就注定不可能是组长进程,从而可以确保子进程中一定可以执行setsid函数。在执行setsid函数时候,一般会发生下面三个比较重要的事情: 6 | - 该进程会创建一个新的进程组,该进程为进程组组长(或者你可以认为这是一种提升) 7 | - 该进程会创建一个会话组并成为该会话的会话首进程(会话首进程就是创建该会话的进程) 8 | - 该进程会失去控制终端。如果该进程本来就没有控制终端,则罢了(liao)。如果有,那么该进程也将脱离该控制终端,与之失去联系。 9 | - 控制终端。每个会话可能会拥有一个控制终端(看着比较玄学,你可以暂时理解为就一个那种黑乎乎的命令行窗口),建立与控制终端连接的会话首进程叫做控制进程。 10 | 11 | ##### 结合Linux命令ps来查看一下上述几个概念的恩怨情仇,我们看下我们常用的 ps -o pid,ppid,pgid,sid,comm | less 执行结果: 12 | ![](http://static.ti-node.com/6379944793014796288) 13 | ##### 第一行分别是PID,PPID,PGID,SID,COMMAND,依次分别是进程ID,该进程父进程ID,进程组ID,会话ID,命令。 14 | ##### 通过最后一列,我们知道第二行就是bash也就是bash shell进程,其进程ID为15793,其父进程为13291,进程组ID为15793,会话ID也会15793,结合前面的概念,我们可以知道bash shell就是该进程组组长。 15 | ##### 第三行则是ps命令的进程,其进程ID为15816,他是由于bash进程fork出来的,所以他的父进程ID为15793,然后是他所属的组ID为15816,所属的会话ID依然是15793。 16 | ##### 最后一行是less命令的进程,其进程ID为15817,他也是由bash进程fork出来的,所以他的父进程ID也为15793,然后是他所属的组ID为15816,所属的会话ID依然是15793。 17 | ##### 简单总结一下: 18 | - 上述三个进程一共形成了两个进程组,bash自己为一组,组ID为15793,组长进程为bash自己 ; ps和less为一组,组ID为15816,组长进程为ps进程 19 | - 上述三个进程属于同一个会话,会话ID为15793,会话首进程为bash进程(待定) 20 | - 控制终端则为打开的terminal窗口,与之关联的控制进程则为bash进程 21 | 22 | ##### 通过这么一顿分析,是不是感觉可以接受点儿了?然后是,叨逼叨了半天这个,跟daemon进程有啥子关系? 23 | ##### 啦啦啦,下面通过引入代码直接分析: 24 | ```php 25 | $pid = pcntl_fork(); 26 | if( $pid < 0 ){ 27 | exit('fork error.'); 28 | } else if( $pid > 0 ) { 29 | // 主进程退出 30 | exit(); 31 | } 32 | // 子进程继续执行 33 | 34 | // 最关键的一步来了,执行setsid函数! 35 | if( !posix_setsid() ){ 36 | exit('setsid error.'); 37 | } 38 | 39 | // 理论上一次fork就可以了 40 | // 但是,二次fork,这里的历史渊源是这样的:在基于system V的系统中,通过再次fork,父进程退出,子进程继续,保证形成的daemon进程绝对不会成为会话首进程,不会拥有控制终端。 41 | 42 | $pid = pcntl_fork(); 43 | if( $pid < 0 ){ 44 | exit('fork error'); 45 | } else if( $pid > 0 ) { 46 | // 主进程退出 47 | exit; 48 | } 49 | 50 | // 子进程继续执行 51 | 52 | // 啦啦啦,啦啦啦,啦啦啦,已经变成daemon啦,开心 53 | cli_set_process_title('testtesttest'); 54 | // 睡眠1000000,防止进程执行完毕挂了 55 | sleep( 1000000 ); 56 | 57 | ``` 58 | ##### 将上述文件保存为daemon.php,然后php daemon.php执行,使用 ps -aux | grep testte ,如果没有什么大问题你应该就可以看到这个进程在后台跑了。 59 | 60 | ##### 所以为什么第一步要先fork呢?因为调用setsid的进程不可以是组长进程(篇头的枯燥知识需要了吧?),所以必须fork一次,然后将主进程直接退出,保留子进程。因为子进程一定不会是组长进程,所以子进程可以调用setsid。调用setsid则会产生三个现象:创建一个新会话并成为会话首进程,创建一个进程组并成为组长进程,脱离控制终端。 61 | ##### 啦啦啦,明白为啥篇头那一坨枯燥的知识是为了什么吧? 62 | ##### 然而,实际上,上述代码仅仅完成了一个标准daemon的80%,还有20%需要我们进一步完善。那么,需要完善什么呢?我们修改一下上述代码,让程序在最终的代码段中执行一些文本输出: 63 | ```php 64 | $pid = pcntl_fork(); 65 | if( $pid < 0 ){ 66 | exit('fork error.'); 67 | } else if( $pid > 0 ) { 68 | // 主进程退出 69 | exit(); 70 | } 71 | // 子进程继续执行 72 | 73 | // 最关键的一步来了,执行setsid函数! 74 | if( !posix_setsid() ){ 75 | exit('setsid error.'); 76 | } 77 | 78 | // 理论上一次fork就可以了 79 | // 但是,二次fork,这里的历史渊源是这样的:在基于system V的系统中,通过再次fork,父进程退出,子进程继续,保证形成的daemon进程绝对不会成为会话首进程,不会拥有控制终端。 80 | 81 | $pid = pcntl_fork(); 82 | if( $pid < 0 ){ 83 | exit('fork error'); 84 | } else if( $pid > 0 ) { 85 | // 主进程退出 86 | exit; 87 | } 88 | 89 | // 子进程继续执行 90 | 91 | // 啦啦啦,啦啦啦,啦啦啦,已经变成daemon啦,开心 92 | cli_set_process_title('testtesttest'); 93 | // 循环1000次,每次睡眠1s,输出一个字符test 94 | for( $i = 1; $i <= 1000; $i++ ){ 95 | sleep( 1 ); 96 | echo "test".PHP_EOL; 97 | } 98 | ``` 99 | ##### 将文件保存为daemon.php,然后php daemon.php执行文件,嗯,是不是有怪怪的现象,大概类似于下图: 100 | ![](http://static.ti-node.com/6379961708575719424) 101 | ##### 即便你按Ctrl+C都没用,终端在不断输出test,唯一办法就是关闭当前终端窗口然后重新开一个,然而,这并不符合社会主义主流价值观。所以,我们还要解决标准输出和错误输出,我们的daemon程序不可以再将终端窗口当作默认的标准输出了。 102 | ##### 其次是将当前工作目录修改更改为根目录。不然可能就会出现下面这样一个问题,就是如果父进程是的工作目录是一个挂载的目录,那么子进程会继承父进程的工作目录,当子进程已经daemon化后就会出现一个悲剧:那就是虽然原来挂载的目录已经不用了,但是却无法用umount卸载,非常悲剧。 103 | ##### 最后一个问题是,要在第一次fork后设置umask(0),避免权限上的一些问题。所以较为完整的代码如下: 104 | ```php 105 | // 设置umask为0,这样,当前进程创建的文件权限则为777 106 | umask( 0 ); 107 | 108 | $pid = pcntl_fork(); 109 | if( $pid < 0 ){ 110 | exit('fork error.'); 111 | } else if( $pid > 0 ) { 112 | // 主进程退出 113 | exit(); 114 | } 115 | // 子进程继续执行 116 | 117 | // 最关键的一步来了,执行setsid函数! 118 | if( !posix_setsid() ){ 119 | exit('setsid error.'); 120 | } 121 | 122 | // 理论上一次fork就可以了 123 | // 但是,二次fork,这里的历史渊源是这样的:在基于system V的系统中,通过再次fork,父进程退出,子进程继续,保证形成的daemon进程绝对不会成为会话首进程,不会拥有控制终端。 124 | 125 | $pid = pcntl_fork(); 126 | if( $pid < 0 ){ 127 | exit('fork error'); 128 | } else if( $pid > 0 ) { 129 | // 主进程退出 130 | exit; 131 | } 132 | 133 | // 子进程继续执行 134 | 135 | // 啦啦啦,啦啦啦,啦啦啦,已经变成daemon啦,开心 136 | cli_set_process_title('testtesttest'); 137 | // 一般服务器软件都有写配置项,比如以debug模式运行还是以daemon模式运行。如果以debug模式运行,那么标准输出和错误输出大多数都是直接输出到当前终端上,如果是daemon形式运行,那么错误输出和标准输出可能会被分别输出到两个不同的配置文件中去 138 | // 连工作目录都是一个配置项目,通过php函数chdir可以修改当前工作目录 139 | chdir( $dir ); 140 | 141 | ``` 142 | 143 | --- 144 | -------------------------------------------------------------------------------- /8. php多进程初探---进程间通信二三事.md: -------------------------------------------------------------------------------- 1 | ##### 往往开启多进程的目的是为了一起干活加速效率,前面说了不同进程之间的内存空间都是相互隔离的,也就说进程A是无法读或写进程B中的任何数据内容的,反之亦然。但是,有些时候,多个进程之间必须要有相互通知的机制,用职场上的话来说就叫“及时沟通”。大家都在一起做同一件事情的不同部分,彼此之间“及时沟通”是很重要的。 2 | ##### 于是进程间通信就诞生了,英文缩写IPC,全称InterProcess Communication。 3 | ##### 常见的进程间通信方式有:管道(分无名和有名两种)、消息队列、信号量、共享内存和socket,最后一种方式今天不提,放到后面的php socket编程中去说,重点说前四种方式。 4 | ##### 管道是*NIX上常见的一个东西,大家平时使用linux的时候也都在用,简单理解就是|,比如ps -aux|grep php这就是管道,大概意思类似于ps进程和grep进程两个进程之间用|完成了通信。管道是一种半双工(现在也有系统已经支持全双工的管道)的工作方式,也就是说数据只能沿着管道的一个方向进行传递,不可以在同一个管道上反向传数据。管道分为两种,一种叫做未命名的管道,另一种叫做命名管道,未命名管道只能在拥有公共祖先的两个进程之间使用,简单理解就是只能用于父进程和和其子进程之间的通信,但是命名管道则可以用于任何两个毫无关连的进程之间的通信(一会儿将要演示的将是这种命名管道)。 5 | ##### 需要特殊指出的是消息队列、信号量和共享内存这三种IPC同属于XSI IPC(XSI可以认为是POSIX标准的超集,简单粗暴理解为C++之于C)。这三种IPC在*NIX中一般都有两个“名字”来为其命名,一个叫做标志符,一个叫做键(key)。标志符是一个非负整数,每当一个IPC结构被创建然后又被销毁后,标志符便会+1一直加到整数的最大整数数值,而后又从0开始重新计算。既然是为了多进程通信使用,那么多进程在使用XSI IPC的时候就需要使用一个名字来找到相应的IPC,然后才能对其进行读写(术语叫做多个进程在同一个IPC结构上汇聚),所以POSIX建议是无论何时创建一个IPC结构,都应指定一个键(key)与之关联。一句话总结就是:标志符是XSI IPC的内部名称,键(key)是XSI IPC的外部名称。 6 | ##### 使多个进程在XSI IPC上汇聚的方法大概有如下三种: 7 | - 使用指定键IPC_PRIVATE来创建一个IPC结构,然后将返回的标志符保存到一个文件中,然后进程之间通过读取这个文件中的标志符进行通信。使用公共的头文件。这么做的缺点是多了IO操作。 8 | - 将共同认同的键写入到公共头文件中。这么做的缺点这个键可能已经与一个IPCi结构关联,这样在使用这个键创建结构的时候就可能会出错,然后必须删除已有的IPC结构再重新创建。 9 | - 认同一个文件路径名和项目ID,然后使用ftok将这两个参数转换成一个键。这将是我们使用的方式。 10 | 11 | ##### XSI IPC结构有一个与之对应的权限结构,叫做ipc_perm,这个结构中定义了IPC结构的创建者、拥有者等。 12 | 13 | ##### 多进程通信之一:命名管道。 在php中,创建一个管道的函数叫做posix_mkfifo(),管道创建完成后其实就是一个文件,然后就可以用任何与读写文件相关的函数对其进行操作了,代码大概演示一下: 14 | ```php 15 | 0 ) { 35 | // 在父进程中 36 | // 打开命名管道,然后读取文本 37 | $file = fopen( $pipe_file, "r" ); 38 | // 注意此处fread会被阻塞 39 | $content = fread( $file, 1024 ); 40 | echo $content.PHP_EOL; 41 | // 注意此处再次阻塞,等待回收子进程,避免僵尸进程 42 | pcntl_wait( $status ); 43 | } 44 | ``` 45 | ##### 运行结果如下: 46 | ![](http://static.ti-node.com/6381869827836870657) 47 | 48 | ##### 多进程通信之二:消息队列。这个怕是很多人都听过,不过印象往往停留在kafka、rabbitmq之类的用于服务器解耦网络消息队列软件上。消息队列是消息的链接表(一种常见的数据结构),但是这种消息队列存储于系统内核中(不是用户态),一般我们外部程序使用一个key来对消息队列进行读写操作。在PHP中,是通过msg_*系列函数完成消息队列操作的。 49 | ```php 50 | 0 ) { 68 | // 在父进程中 69 | // 使用msg_receive()函数获取消息 70 | msg_receive( $queue, 0, $msgtype, 1024, $message ); 71 | echo $message.PHP_EOL; 72 | // 用完了记得清理删除消息队列 73 | msg_remove_queue( $queue ); 74 | pcntl_wait( $status ); 75 | } else if( 0 == $pid ) { 76 | // 在子进程中 77 | // 向消息队列中写入消息 78 | // 使用msg_send()向消息队列中写入消息,具体可以参考文档内容 79 | msg_send( $queue, 1, "helloword" ); 80 | exit; 81 | } 82 | ``` 83 | ##### 运行结果如下: 84 | ![](http://static.ti-node.com/6382072383888424961) 85 | ##### 但是,值得大家继续深入研究的是msg_send()和msg_receive()两个函数,这两个的每一个参数都是非常值得深入研究和尝试的。篇幅问题,这里就不再详细描述。 86 | 87 | ##### 这里还需要提示一下ftok函数,不要认为第一个参数的目录被删除后,重新再建立一个同名的目录,这时候生成的key是不同了,所以确保ftok()的文件不被删除 88 | 89 | ##### 多进程通信之三:信号量与共享内存。共享内存是最快是进程间通信方式,因为n个进程之间并不需要数据复制,而是直接操控同一份数据。实际上信号量和共享内存是分不开的,要用也是搭配着用。*NIX的一些书籍中甚至不建议新手轻易使用这种进程间通信的方式,因为这是一种极易产生死锁的解决方案。共享内存顾名思义,就是一坨内存中的区域,可以让多个进程进行读写。这里最大的问题就在于数据同步的问题,比如一个在更改数据的时候,另一个进程不可以读,不然就会产生问题。所以为了解决这个问题才引入了信号量,信号量是一个计数器,是配合共享内存使用的,一般情况下流程如下: 90 | - 当前进程获取将使用的共享内存的信号量 91 | - 如果信号量大于0,那么就表示这块儿共享资源可以使用,然后进程将信号量减1 92 | - 如果信号量为0,则进程进入休眠状态一直到信号量大于0,进程唤醒开始从1 93 | 94 | ##### 一个进程不再使用当前共享资源情况下,就会将信号量减1。这个地方,信号量的检测并且减1是原子性的,也就说两个操作必须一起成功,这是由系统内核来实现的。 95 | ##### 在php中,信号量和共享内存先后一共也就这几个函数: 96 | ![](http://static.ti-node.com/6382081950860967937) 97 | ##### 其中,sem_*是信号量相关函数,shm_*是共享内存相关函数。 98 | ```php 99 | 0 ) { 138 | $child_pid[] = $pid; 139 | } 140 | } 141 | while( !empty( $child_pid ) ){ 142 | foreach( $child_pid as $pid_key => $pid_item ){ 143 | $wait_result=pcntl_waitpid( $pid_item, $status, WNOHANG ); 144 | //必须判断子进程回收的状态,如果不加判断,第一次两个子进程返回都是0,直接unset后会无法进入while,导致僵尸进程 145 | if($wait_result == -1 || $wait_result > 0) 146 | unset( $child_pid[ $pid_key ] ); 147 | } 148 | } 149 | // 休眠2秒钟,2个子进程都执行完毕了 150 | sleep( 2 ); 151 | echo '最终结果'.shm_get_var( $shm_id, SHM_VAR ).PHP_EOL; 152 | // 记得删除共享内存数据,删除共享内存是有顺序的,先remove后detach,顺序反过来php可能会报错 153 | shm_remove( $shm_id ); 154 | shm_detach( $shm_id ); 155 | ``` 156 | ##### 运行结果如下: 157 | ![](http://static.ti-node.com/6382157565437935617) 158 | ##### 确切说,如果不用sem的话,上述的运行结果在一定概率下就会产生1而不是2。但是只要加入sem,那就一定保证100%是2,绝对不会出现其他数值。 159 | --- 160 | -------------------------------------------------------------------------------- /9. PHP Socket初探---先从一个简单的socket服务器开始.md: -------------------------------------------------------------------------------- 1 | ##### socket的中文名字叫做套接字,这种东西就是对TCP/IP的“封装”。现实中的网络实际上只有四层而已,从上至下分别是应用层、传输层、网络层、数据链路层。最常用的http协议则是属于应用层的协议,而socket,可以简单粗暴的理解为是传输层的一种东西。如果还是很难理解,那再粗暴地点儿tcp://218.221.11.23:9999,看到没?这就是一个tcp socket。 2 | ##### socket赋予了我们操控传输层和网络层的能力,从而得到更强的性能和更高的效率,socket编程是解决高并发网络服务器的最常用解决和成熟的解决方案。任何一名服务器程序员都应当掌握socket编程相关技能。 3 | ##### 在php中,可以操控socket的函数一共有两套,一套是socket_*系列的函数,另一套是stream_*系列的函数。socket_*是php直接将C语言中的socket抄了过来得到的实现,而stream_*系则是php使用流的概念将其进行了一层封装。下面用socket_*系函数简单为这一系列文章开个篇。 4 | ##### 先来做个最简单socket服务器: 5 | ```php 6 |