当前位置: 首页 > 后端技术 > PHP

PHP复习多进程编程

时间:2023-03-30 00:40:34 PHP

转载请注明文章出处:https://tlanyan.me/php-review...PHP复习系列目录PH??P基础web请求cookieweb响应session数据库操作加密解密composer创建自己的ComposerPacket发送邮件IO流Socket编程为了更好的利用多核CPU,我们需要多进程或者多线程。但是在常规的web开发中,我们很少用到这两种并发技术(curl_multi等特殊功能除外)。如果脚本运行在CLI模式下,多进程多线程技术是提升多核CPU的利器。与多线程相比,多进程程序具有健壮性、无锁、更好地支持分布式等特点。这篇文章是学习PHP多进程编程的。多处理PHP中与(多)处理相关的两个重要扩展是PCNTL和POSIX。PCNTL主要用于创建、执行子进程和处理信号,POSIX扩展实现了POSIX标准中定义的接口。由于Windows不兼容POSIX,因此POSIX扩展在Windows平台上不可用。先看简单代码看多进程编程://fork.php$parentId=posix_getpid();fwrite(STDOUT,"mypid:$parentId\n");$childNum=10;foreach(range(1,$childNum)as$index){$pid=pcntl_fork();if($pid===-1){fwrite(STDERR,"分叉失败!\n");出口;}//父代码if($pid>0){fwrite(STDOUT,"forkthe{$index}thchild,pid:$pid\n");}else{$mypid=posix_getpid();$parentId=posix_getppid();fwrite(STDOUT,"我是第{$index}个孩子,我的pid:$mypid,parentId:$parentId\n");睡觉(5);出口;//注意这一行}}关键代码是pcntl_fork函数,它返回一个整数,小于0的值表示克隆失败。如果克隆成功,返回两个值:父进程获取子进程的进程号,子进程获取0。根据函数的返回值可以判断下一次执行环境是否是在父进程中还是在子进程中。fork调用允许系统创建一个与当前进程几乎完全相同的进程。除了进程号等少数信息外,进程的代码段、栈、数据段的值都是相同的。父进程打开一个文件,复制的子进程也享有这个句柄。这是以往多个进程可以监听同一个端口的原理;子进程根据父进程fork(代码段共享)时的环境继续执行,直到退出。去掉上面代码中else语句块的exit函数,会帮助你更好的理解上面这段话。程序的初衷是生成10个子流程。去掉子进程执行代码的exit后,子进程执行else块中的代码,然后继续执行foreach循环,最终生成55个子进程(为什么是55个?)!鉴于此,一个好的做法是总是在子进程的执行代码之后加上exit终止语句,除非你真的确定子进程会按预期执行。除了fork,另一个多进程技术就是exec。system、exec、proc_open等函数会生成一个新的进程来执行外部命令(并返回结果)。这些函数的本质是fork一个进程,然后调用shell执行命令,主进程等待其执行结束。在函数执行过程中,主进程除了等待之外无法处理其他任务,所以一般不认为是多进程编程。在实践中,可以结合fork来并发执行外部命令。孤儿进程和僵尸进程在多进程编程中需要考虑的一个问题是孤儿进程和僵尸进程。进程结束前,父进程已经退出,进程成为孤儿进程;进程退出后,父进程正在执行,子进程未被回收,进程成为僵尸进程。孤儿进程是还在执行的进程,而僵尸进程已经停止执行,只剩下一缕进程ID,仍然可以被外界感知。孤儿进程会被系统的根进程(init进程,进程号1)接管,运行后会被根进程回收。下面的代码演示了孤儿进程的父进程的变化://orphan.php$pid=pcntl_fork();if($pid===0){$myid=posix_getpid();$parentId=posix_getppid();fwrite(STDOUT,"我的pid:$myid,parentId:$parentId\n");睡觉(5);$myid=posix_getpid();$parentId=posix_getppid();fwrite(STDOUT,"mypid:$myid,parentId:$parentId\n");}else{fwrite(STDOUT,"parentexit\n");}执行脚本:phporphan.php,可以看到输出类似如下:parentexitmypid:14384,parentId:14383mypid:14384,parentId:1父进程退出后,子进程被收养到根进程1,根进程1负责回收子进程。然后查看僵尸进程。如果主进程长时间运行,不回收子进程,则僵尸进程会一直存在,直到主进程退出,成为孤儿进程,被根进程收养;如果主进程一直运行,僵尸进程就会一直存在。下面的代码演示了生成10个僵尸进程://zombie.phpforeach(range(1,10)as$i){$pid=pcntl_fork();if($pid===0){fwrite(STDOUT,"childexit\n");出口;}}睡眠(200);退出;打开一个终端并执行phpzombie.php,然后打开一个新终端并执行psaux|grepphp|grep-vgrep,一个可能的输出如下:vagrant143360.30.834460015144pts/1S+05:090:00phpzombie.phpvagrant143370.00.000pts/1Z+05:090:00[php]流浪者143380.00.000pts/1Z+05:090:00[php]流浪者143390.00.000pts/1Z+05:090:00[php]流浪者143400.00.000pts/1Z+05:090:00[php]流浪者143410.00.000pts/1Z+05:090:00[php]流浪者143420.00.000pts/1Z+05:090:00[php]流浪者143430.00.000pts/1Z+05:090:00[php]流浪者143440.00.000pts/1Z+05:090:00[php]流浪者143450.00.000pts/1Z+05:090:00[php]流浪者143460.00.000pts/1Z+05:090:00[php]最后一列为的进程是僵尸进程。这些进程的第八列被标记为“Z+”,也就是Zombie虽然除了进程号之外不能被回收,但是僵尸进程没有僵尸那么可怕,但是我们应该让子进程执行完后就可以安息了以避免僵尸进程。有两种方法可以回收子进程。一种是主进程调用pcntl_wait/pcntl_waitpid函数等待子进程结束;另一个是处理SIGCLD信号。先说使用wait函数回收子进程,信号处理放在后面的章节。PCNT扩展中用来回收子进程的两个函数是pcntl_wait和pcntl_waitpid,pcntl_waitpid可以指定等待进程。让我们看看如何使用这两个函数回收子进程://wait.php$pid=pcntl_fork();如果($pid===0){$myid=posix_getpid();fwrite(STDOUT,"child$myidexited\n");}else{sleep(5);$状态=0;$pid=pcntl_wait($status,WUNTRACED);如果($pid>0){fwrite(STDOUT,"child:$pidexited\n");}睡眠(5);fwrite(STDOUT,"parentexit\n");}执行脚本:phpwait.php,然后打开另一个终端执行:watch-n2'psaux|grepphp|grep-vgrep'.从watch的输出可以看出,子进程退出后的5秒内,就是一个僵尸进程。父进程被回收后,僵尸进程消失,最终父进程退出。如果有多个子进程,父进程需要循环调用wait函数,否则部分子进程执行后会变成僵尸进程。信号处理PCNTL扩展中的pcntl_signal函数用于安装信号函数,当进程收到信号时会执行回调函数中的代码。我们知道Ctrl+C可以中断程序的执行。原理是系统在按下组合键后向程序发送一个SIGINT信号。该信号的默认动作是退出程序,因此系统终止程序。SIGINT信号可以捕获信号。我们可以设置信号回调函数。系统收到信号后,执行回调函数,不退出程序://signal.phppcntl_signal(SIGINT,function(){fwrite(STDOUT,"receivesignal:SIGINT,donothing...\n");});while(true){pcntl_signal_dispatch();sleep(1);}执行脚本:phpsignal.php,然后按Ctrl+C,输出如下:[vagrant@localhost~]$phpsignal.php^Creceivesignal:SIGINT,donothing...^Creceivesignal:SIGINT,donothing...^Creceivesignal:SIGINT,donothing...^Creceivesignal:SIGINT,donothing...^Creceivesignal:SIGINT,donothing...安装信号功能后,Ctrl+C不再起作用,程序仍然被恶作剧地执行。要结束一个程序,可以向进程发送一个无法捕获的信号,例如SIGKILL。ps辅助|grepphp找到程序的进程号,然后用kill命令发送SIGKILL信号:kill-SIGKILL进程号。程序在收到信号后被操作系统强行中断。如果您在代码中捕捉到SIGKILL信号,会发生什么情况?将上面代码中的SIGINT改为SIGKILL,执行脚本会提示:PHPFatalerror:Errorinstallingsignalhandlerfor9in/home/vagrant/signal.phponline2.9是SIGKILL的值,错误提示代码无法捕获此信号。支持哪些信号以及默认操作是什么取决于系统。大多数*nix系统都支持SIGINT和SIGKILL等31种常见的异步信号,有些系统支持的信号更多。内核收到进程信号后,会检查进程是否注册了处理函数,如果没有注册,则执行默认操作;否则,当进程运行在用户态时,内核会回调信号处理函数,移除信号。PHP中接收到信号后触发信号回调函数的方式有3种:tick触发,比如每执行100条底层指令检查信号:declare(ticks=100);使用pcntl_signal_dispatch手动触发,用法见上面的signal.php;PHP7。1可以使用pcntl_async_signals异步智能触发。tick方法效率很低,不推荐使用;pcntl_signal_dispatch需要手动触发,可能会有较大的延迟。如果PHP版本不低于7.1,建议使用pcnt_async_signals自动分发信号消息。该功能比tick更高效,实时性优于手动触发。原理是检查程序从内核态切出或函数返回时是否有信号,有则执行回调。了解了信号之后,我们再看看如何利用信号来解决僵尸进程问题。子进程退出后,操作系统会向父进程发送SIGCLD信号,在信号回调函数中可以回收子进程。详情见如下代码://fork-signal.phppcntl_async_signals(true);pcntl_signal(SIGCLD,function(){$pid=pcntl_wait($status,WUNTRACED);fwrite(STDOUT,"child:$pidexited\n");});$pid=pcntl_fork();if($pid===0){fwrite(STDOUT,"childexit\n");}else{//mockbusyworksleep(1);}相比手动pcntl_wait/pcntl_waitpid方式,信号处理无疑是更简洁高效。信号也是进程内的一种通信方式。接下来简单说一下进程间通信。进程间通信fork出子进程后,两个进程的数据段和栈(理论上)就分开了。与多线程不同,全局变量不能在不同进程之间共享。进程之间要交换数据,必须通过进程间通信(Inter-ProcessCommunication)技术。上面提到的信号是过程中的一种通信技术。posix_kill函数可以向指定的进程发送信号,达到通信的目的。进程间通信技术主要包括:管道(pipe)、流管道(s_pipe)和著名管道(FIFO);信号(信号);消息队列(messagequeue);共享内存(共享内存);信号量(semaphore);套接字(套接字);关于这些通信技术的详细介绍,请参考文末链接,或其他文档,本文不再详述。守护进程通过phptest.php执行程序,关闭终端后程序退出。为了使程序长期运行,需要额外的方法。总结起来主要有3种:nohup;screen/tmux等工具;fork子进程后,父进程退出,子进程提升为session/processleader,继续跑离终端。screen/tmux程序实际上仍然保留在终端中,它只是在一个长寿命的终端中运行。nohup和fork方法是让程序脱离终端并实现物理提升(成为守护进程)的正确方法。以下代码通过fork使程序成为守护进程://daemon.php$pid=pcntl_fork();switch($pid){case-1:fwrite(STDOUT,"forkfailed!\n");退出(1);休息;case0:if(posix_setsid()===-1){fwrite(STDERR,"设置child为sessionleader失败!\n");出口;}file_put_contents("/tmp/daemon.out","phpdaemonexample\n",FILE_APPEND);while(true){睡眠(5);file_put_contents("/tmp/daemon.out","now:".date("Y-m-dH:i:s")."\n",FILE_APPEND);}break;default://parentexitexit;}fork之后最重要的操作是posix_setsid,设置当前进程为sessionleader(set进程当前不能为teamleader)。有些开源库会fork两次,以??防止第一次fork的进程不小心打开终端(非sessionleader无法打开终端)。执行程序:phpdaemon.php,然后关闭终端,或者重新登录,通过psaux|查看程序是否运行。grepdaemon.php。检测/tmp/daemon.out,有连续的内容输出,说明程序已经成为后台继续运行的守护进程。注意后台的多进程应该在进程离开终端后fork,即最后在后台工作的进程不能直接从脚本启动的进程fork,但至少应该是该进程的孙进程由脚本启动的进程。Application让我们来谈谈一个简单的多进程应用程序。在上一篇博文《PHP复习Socket编程》中,我们的服务器已经能够几乎实时地响应客户端的请求,但是客户端并没有实时收到服务器发送的消息。使用多进程,我们使用一个进程从服务器读取消息,另一个进程从终端收集用户输入并发送到服务器。下面是多进程的客户端代码://client.php"echo","args"=>$args,]);fwrite($socket,$message);}}}执行客户端:phpclient.php,你会发现终端输入和服务器消息都能及时响应,断线信号也能正确广播。总结本文简单介绍了多进程编程的几个方面,最后给出了一个应用实例,希望对学习多进程的同仁有所帮助。谢谢阅读!参考http://php.net/manual/en/book...http://php.net/manual/en/book...https://www.cnblogs.com/hicji...http://gityuan.com/2015/12/20...https://www.cnblogs.com/hoys/...http://www.cnblogs.com/taobat...https://www.jianshu.com/p/c10...https://blog.csdn.net/column/...https://segmentfault.com/a/11...