摘要:多進(jìn)程中與多進(jìn)程相關(guān)的兩個(gè)重要拓展是和。函數(shù)執(zhí)行期間,主進(jìn)程除了等待無法處理其他任務(wù),所以一般不認(rèn)為這是多進(jìn)程編程?;厥兆舆M(jìn)程有兩種方式,一種是主進(jìn)程調(diào)用函數(shù)等待子進(jìn)程結(jié)束另外一種是處理信號。
轉(zhuǎn)載請注明文章出處: https://tlanyan.me/php-review...PHP回顧系列目錄
PHP基礎(chǔ)
web請求
cookie
web響應(yīng)
session
數(shù)據(jù)庫操作
加解密
Composer
創(chuàng)建自己的Composer包
發(fā)送郵件
IO
流
Socket編程
為了更好的利用多核CPU,我們需要多進(jìn)程或多線程。但在常規(guī)web開發(fā)中,我們極少用到這兩種并發(fā)技術(shù)(curl_multi等特殊函數(shù)除外)。如果腳本運(yùn)行在CLI模式下,多進(jìn)程和多線程技術(shù)是提高多核CPU的有力工具。
相對于多線程,多進(jìn)程的程序具有健壯、無鎖、對分布式支持更好等特點(diǎn)。本文來學(xué)習(xí)一下PHP的多進(jìn)程編程。
多進(jìn)程PHP中與(多)進(jìn)程相關(guān)的兩個(gè)重要拓展是PCNTL和POSIX。PCNTL主要用來創(chuàng)建、執(zhí)行子進(jìn)程和處理信號,POSIX拓展則實(shí)現(xiàn)了POSIX標(biāo)準(zhǔn)中定義的接口。由于Windows不是POSIX兼容的,所以POSIX拓展在Windows平臺上不可用。
先上簡單的代碼看多進(jìn)程編程:
// fork.php $parentId = posix_getpid(); fwrite(STDOUT, "my pid: $parentId "); $childNum = 10; foreach (range(1, $childNum) as $index) { $pid = pcntl_fork(); if ($pid === -1) { fwrite(STDERR, "failt to fork! "); exit; } // parent code if ($pid > 0) { fwrite(STDOUT, "fork the {$index}th child, pid: $pid "); } else { $mypid = posix_getpid(); $parentId = posix_getppid(); fwrite(STDOUT, "I"m the {$index}th child and my pid: $mypid, parentId: $parentId "); sleep(5); exit; // 注意這一行 } }
關(guān)鍵的代碼是pcntl_fork函數(shù),函數(shù)返回一個(gè)整數(shù),小于0表示克隆失敗??寺〕晒Φ那闆r下返回兩個(gè)值:父進(jìn)程拿到子進(jìn)程的進(jìn)程號,而子進(jìn)程則得到0??梢愿鶕?jù)函數(shù)的返回值判斷接下來的執(zhí)行環(huán)境在父進(jìn)程中還是子進(jìn)程中。
fork調(diào)用讓系統(tǒng)創(chuàng)建一個(gè)與當(dāng)前進(jìn)程幾乎完全一樣的進(jìn)程,除了進(jìn)程號等少數(shù)信息不一樣,進(jìn)程的代碼段、堆棧、數(shù)據(jù)段的值都一致。父進(jìn)程打開了一個(gè)文件,復(fù)制的子進(jìn)程同樣享有這個(gè)句柄,這是過去多進(jìn)程能監(jiān)聽同一個(gè)端口的原理;子進(jìn)程基于父進(jìn)程fork時(shí)的環(huán)境繼續(xù)執(zhí)行(代碼段共享)直到退出。
去掉上述代碼中else語句塊的exit能將幫助你更好地理解上面這段話。程序的本意是生成10個(gè)子進(jìn)程,去掉子進(jìn)程執(zhí)行代碼的exit后,子進(jìn)程執(zhí)行完else塊中代碼后繼續(xù)執(zhí)行foreach循環(huán),最終生成55個(gè)子進(jìn)程(為什么是55個(gè)?)!鑒于此,一個(gè)良好的實(shí)踐是在子進(jìn)程的執(zhí)行代碼后總是加上exit終止語句,除非你真的有把握子進(jìn)程會按照預(yù)期執(zhí)行。
除了fork,另外一種多進(jìn)程技術(shù)是exec。system、exec、proc_open等函數(shù)會生成一個(gè)新的進(jìn)程執(zhí)行外部命令(并返回結(jié)果)。這些函數(shù)的本質(zhì)是fork一個(gè)進(jìn)程,然后調(diào)用shell執(zhí)行命令,主進(jìn)程等待其執(zhí)行結(jié)束。函數(shù)執(zhí)行期間,主進(jìn)程除了等待無法處理其他任務(wù),所以一般不認(rèn)為這是多進(jìn)程編程。實(shí)踐中可以結(jié)合fork來并發(fā)執(zhí)行外部命令。
孤兒進(jìn)程與僵尸進(jìn)程多進(jìn)程編程需要考慮到的一個(gè)問題是孤兒進(jìn)程和僵尸進(jìn)程。進(jìn)程結(jié)束前父進(jìn)程已經(jīng)退出,進(jìn)程變成孤兒進(jìn)程;進(jìn)程退出后父進(jìn)程在執(zhí)行且未回收子進(jìn)程,那么進(jìn)程變成僵尸進(jìn)程。孤兒進(jìn)程是仍在執(zhí)行的進(jìn)程,僵尸進(jìn)程則已經(jīng)停止執(zhí)行,只剩下進(jìn)程號一縷孤魂仍能被外界感知。
孤兒進(jìn)程會被系統(tǒng)的根進(jìn)程(init進(jìn)程,進(jìn)程號為1)接管,運(yùn)行結(jié)束后由根進(jìn)程回收。下面代碼演示孤兒進(jìn)程的父進(jìn)程的變化:
// orphan.php $pid = pcntl_fork(); if ($pid === 0) { $myid = posix_getpid(); $parentId = posix_getppid(); fwrite(STDOUT, "my pid: $myid, parentId: $parentId "); sleep(5); $myid = posix_getpid(); $parentId = posix_getppid(); fwrite(STDOUT, "my pid: $myid, parentId: $parentId "); } else { fwrite(STDOUT, "parent exit "); }
執(zhí)行腳本:php orphan.php,可以看到類似如下輸出:
parent exit my pid: 14384, parentId: 14383 my pid: 14384, parentId: 1
父進(jìn)程退出后子進(jìn)程過繼給1號根進(jìn)程,并由其負(fù)責(zé)回收子進(jìn)程。
接著看僵尸進(jìn)程。主進(jìn)程長時(shí)間運(yùn)行且不回收子進(jìn)程,僵尸進(jìn)程會一直存在,直到主進(jìn)程退出后變成孤兒進(jìn)程過繼給根進(jìn)程;如果主進(jìn)程一直運(yùn)行,僵尸進(jìn)程將一直存在。
下面代碼演示生成10個(gè)僵尸進(jìn)程:
// zombie.php foreach (range(1, 10) as $i) { $pid = pcntl_fork(); if ($pid === 0) { fwrite(STDOUT, "child exit "); exit; } } sleep(200); exit;
打開終端執(zhí)行php zombie.php,然后新打開一個(gè)終端執(zhí)行ps aux | grep php | grep -v grep,一個(gè)可能的輸出如下:
vagrant 14336 0.3 0.8 344600 15144 pts/1 S+ 05:09 0:00 php zombie.php vagrant 14337 0.0 0.0 0 0 pts/1 Z+ 05:09 0:00 [php]vagrant 14338 0.0 0.0 0 0 pts/1 Z+ 05:09 0:00 [php] vagrant 14339 0.0 0.0 0 0 pts/1 Z+ 05:09 0:00 [php] vagrant 14340 0.0 0.0 0 0 pts/1 Z+ 05:09 0:00 [php] vagrant 14341 0.0 0.0 0 0 pts/1 Z+ 05:09 0:00 [php] vagrant 14342 0.0 0.0 0 0 pts/1 Z+ 05:09 0:00 [php] vagrant 14343 0.0 0.0 0 0 pts/1 Z+ 05:09 0:00 [php] vagrant 14344 0.0 0.0 0 0 pts/1 Z+ 05:09 0:00 [php] vagrant 14345 0.0 0.0 0 0 pts/1 Z+ 05:09 0:00 [php] vagrant 14346 0.0 0.0 0 0 pts/1 Z+ 05:09 0:00 [php]
最后一列為
回收子進(jìn)程有兩種方式,一種是主進(jìn)程調(diào)用pcntl_wait/pcntl_waitpid函數(shù)等待子進(jìn)程結(jié)束;另外一種是處理SIGCLD信號。我們先說使用wait函數(shù)回收子進(jìn)程,信號處理放在下面的章節(jié)。
PCNT拓展中用于回收子進(jìn)程的兩個(gè)函數(shù)是pcntl_wait和pcntl_waitpid,pcntl_waitpid可以指定等待的進(jìn)程。來看如何用這兩個(gè)函數(shù)回收子進(jìn)程:
// wait.php $pid = pcntl_fork(); if ($pid === 0) { $myid = posix_getpid(); fwrite(STDOUT, "child $myid exited "); } else { sleep(5); $status = 0; $pid = pcntl_wait($status, WUNTRACED); if ($pid > 0) { fwrite(STDOUT, "child: $pid exited "); } sleep(5); fwrite(STDOUT, "parent exit "); }
執(zhí)行腳本:php wait.php,然后打開另外一個(gè)終端執(zhí)行:watch -n2 "ps aux | grep php | grep -v grep"。從watch輸出可以看到子進(jìn)程退出后的5秒內(nèi)是僵尸進(jìn)程,父進(jìn)程回收后僵尸進(jìn)程消失,最后父進(jìn)程退出。
如果有多個(gè)子進(jìn)程,父進(jìn)程需要循環(huán)調(diào)用wait函數(shù),否則某些子進(jìn)程執(zhí)行完畢后也會變成僵尸進(jìn)程。
信號處理PCNTL拓展中的pcntl_signal函數(shù)用于安裝信號函數(shù),進(jìn)程收到信號時(shí)會執(zhí)行回調(diào)函數(shù)中的代碼。我們知道Ctrl + C可以中斷程序的執(zhí)行,原理是按下組合鍵后系統(tǒng)向程序發(fā)出SIGINT信號。這個(gè)信號的默認(rèn)操作是退出程序,所以系統(tǒng)終止了程序運(yùn)行。SIGINT信號可捕捉信號,我們可以設(shè)置信號回調(diào)函數(shù),收到信號后系統(tǒng)執(zhí)行回調(diào)函數(shù)而非退出程序:
// signal.php pcntl_signal(SIGINT, function () { fwrite(STDOUT, "receive signal: SIGINT, do nothing... "); }); while (true) { pcntl_signal_dispatch(); sleep(1); }
執(zhí)行腳本:php signal.php,然后按Ctrl + C,輸出如下:
[vagrant@localhost ~]$ php signal.php ^Creceive signal: SIGINT, do nothing... ^Creceive signal: SIGINT, do nothing... ^Creceive signal: SIGINT, do nothing... ^Creceive signal: SIGINT, do nothing... ^Creceive signal: SIGINT, do nothing...
安裝了信號函數(shù)后,Ctrl + C不再好使,程序依舊調(diào)皮的執(zhí)行。要結(jié)束程序,可以向進(jìn)程發(fā)送無法捕捉的信號,例如SIGKILL。ps aux | grep php找到程序的進(jìn)程號,然后用kill命令發(fā)送SIGKILL信號:kill -SIGKILL 進(jìn)程號。程序收到信號后被操作系統(tǒng)強(qiáng)制中斷執(zhí)行。
如果在代碼中捕捉SIGKILL信號會怎么樣?將上面代碼中的SIGINT改成SIGKILL,執(zhí)行腳本會提示:PHP Fatal error: Error installing signal handler for 9 in /home/vagrant/signal.php on line 2。9是SIGKILL的值,錯(cuò)誤表示代碼中不能捕捉這個(gè)信號。
支持哪些信號,默認(rèn)操作是什么,和系統(tǒng)相關(guān)。絕大部分*nix系統(tǒng)支持SIGINT、SIGKILL等31個(gè)常見異步信號,某些系統(tǒng)支持更多的信號。
內(nèi)核收到進(jìn)程信號后,會查看進(jìn)程是否注冊了處理函數(shù),如果未注冊則執(zhí)行默認(rèn)操作;否則當(dāng)進(jìn)程運(yùn)行在用戶態(tài)時(shí),內(nèi)核回調(diào)信號處理函數(shù)并移除信號。PHP中收到信號后觸發(fā)信號回調(diào)函數(shù)的方式有三種:
tick觸發(fā),例如每執(zhí)行100條低級指令檢查信號:declare(ticks=100);
使用pcntl_signal_dispatch手動觸發(fā),用法見上文signal.php;
PHP7.1起可以使用pcntl_async_signals異步智能觸發(fā)。
tick的方式十分低效,不建議使用;pcntl_signal_dispatch需要手動觸發(fā),可能存在較大延遲。如果PHP的版本不低于7.1,建議使用pcnt_async_signals自動分發(fā)信號消息。這個(gè)函數(shù)效率上比tick高,實(shí)時(shí)性上比手動觸發(fā)強(qiáng)。其原理是當(dāng)程序從內(nèi)核態(tài)切出、函數(shù)返回等時(shí)機(jī)檢查是否有信號,有則執(zhí)行回調(diào)。
理解了信號,再看看如何使用信號解決僵尸進(jìn)程問題。子進(jìn)程退出后,操作系統(tǒng)會發(fā)送SIGCLD信號到父進(jìn)程,在信號回調(diào)函數(shù)中回收子進(jìn)程即可,詳情見下面代碼:
// fork-signal.php pcntl_async_signals(true); pcntl_signal(SIGCLD, function () { $pid = pcntl_wait($status, WUNTRACED); fwrite(STDOUT, "child: $pid exited "); }); $pid = pcntl_fork(); if ($pid === 0) { fwrite(STDOUT, "child exit "); } else { // mock busy work sleep(1); }
相對于手動pcntl_wait/pcntl_waitpid方式,信號處理無疑更為簡潔高效。
信號也是進(jìn)程中通信的一種方式。接下來簡要說一下進(jìn)程間通信。
進(jìn)程間通信fork出子進(jìn)程后,兩個(gè)進(jìn)程的數(shù)據(jù)段和堆棧(理論上)均分開。與多線程不同,全局變量在不同進(jìn)程中無法共享。進(jìn)程間要進(jìn)行數(shù)據(jù)交換,必須通過進(jìn)程間通信(Inter-Process Communication)技術(shù)。上文提到的信號是進(jìn)程中通信技術(shù)的一種,posix_kill函數(shù)可以向指定進(jìn)程發(fā)送信號,達(dá)到通信的目的。
進(jìn)程間通信技術(shù)主要有:
管道(pipe),流管道(s_pipe)和有名管道(FIFO);
信號(signal);
消息隊(duì)列(message queue);
共享內(nèi)存(share memory);
信號量(semaphore);
套接字(socket);
這些通信技術(shù)的詳細(xì)內(nèi)容請參考文末的鏈接,或者其他文獻(xiàn),本文不再詳述。
守護(hù)進(jìn)程通過php test.php方式執(zhí)行程序,關(guān)閉終端后程序會退出。要讓程序能長期執(zhí)行,需要額外的手段。總結(jié)起來主要有三種:
nohup;
screen/tmux等工具;
fork子進(jìn)程后,父進(jìn)程退出,子進(jìn)程升為會話/進(jìn)程組長,脫離終端繼續(xù)運(yùn)行。
screen/tmux方式程序?qū)嶋H上仍停留在終端,只是運(yùn)行在一個(gè)長期存在的終端中。nohup和fork方式才是讓程序脫離(detach)終端,達(dá)到肉體飛升的正道(成為daemon)。
下面的代碼通過fork的方式讓程序成為守護(hù)進(jìn)程:
// daemon.php $pid = pcntl_fork(); switch ($pid) { case -1: fwrite(STDOUT, "fork failed! "); exit(1); break; case 0: if (posix_setsid() === -1) { fwrite(STDERR, "fail to set child as the session leader! "); exit; } file_put_contents("/tmp/daemon.out", "php daemon example ", FILE_APPEND); while (true) { sleep(5); file_put_contents("/tmp/daemon.out", "now: " . date("Y-m-d H:i:s") . " ", FILE_APPEND); } break; default: // parent exit exit; }
fork之后最重要的一個(gè)操作是posix_setsid,該函數(shù)把當(dāng)前進(jìn)程設(shè)置為會話組長(被設(shè)置的進(jìn)程當(dāng)前不能是組長)。某些開源庫中會fork兩次,防止第一次fork的進(jìn)程無意間打開終端(非會話組長無法打開終端)。
執(zhí)行程序:php daemon.php,然后關(guān)閉終端,或者重新登錄,通過ps aux | grep daemon.php查看程序均在執(zhí)行。檢測/tmp/daemon.out,不斷有內(nèi)容輸出,說明程序已經(jīng)成為在后臺持續(xù)運(yùn)行的守護(hù)進(jìn)程。
注意后臺的多進(jìn)程應(yīng)當(dāng)在進(jìn)程脫離終端后再fork,即最終在后臺干活的進(jìn)程不能直接從腳本啟動的進(jìn)程fork,而應(yīng)該至少是腳本啟動進(jìn)程的孫子進(jìn)程。
應(yīng)用下面來說一個(gè)多進(jìn)程的簡單應(yīng)用。在上一篇博文“PHP回顧之Socket編程”,我們的服務(wù)端已經(jīng)能做到幾乎實(shí)時(shí)響應(yīng)客戶端的請求,但是客戶端不是實(shí)時(shí)收到服務(wù)端下發(fā)的消息。利用多進(jìn)程,我們用一個(gè)進(jìn)程專門負(fù)責(zé)讀取服務(wù)端的消息,另一個(gè)進(jìn)程則負(fù)責(zé)收集用戶在終端的輸入,然后發(fā)送到服務(wù)端。下面是多進(jìn)程的客戶端代碼:
// client.php "echo", "args" => $args, ]); fwrite($socket, $message); } } }
執(zhí)行客戶端:php client.php,會發(fā)現(xiàn)終端輸入和服務(wù)端消息都能及時(shí)響應(yīng)。同時(shí),連接斷開的信號也被正確的廣播。
總結(jié)本文簡要介紹了多進(jìn)程編程的幾個(gè)方面,最后給出一個(gè)應(yīng)用的例子,希望對學(xué)習(xí)多進(jìn)程的同行有幫助。
感謝閱讀!
參考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...
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/28899.html
摘要:所以這次采用多進(jìn)程的方式來實(shí)現(xiàn)同時(shí)為多個(gè)客戶端提供服務(wù)。而多進(jìn)程則是通過創(chuàng)建多個(gè)進(jìn)程來共同完成一件事。如果是子進(jìn)程的執(zhí)行環(huán)境,則返回。正常情況下,子進(jìn)程是通過父進(jìn)程創(chuàng)建的。以上則是我們的多進(jìn)程回聲服務(wù)程序。 上次的回聲服務(wù)程序有個(gè)很大的缺點(diǎn),就是只能同時(shí)連接一個(gè)客戶端,這明顯是不合理的。 所以這次采用多進(jìn)程的方式來實(shí)現(xiàn)同時(shí)為多個(gè)客戶端提供服務(wù)。 以下是最終的效果:showImg(htt...
摘要:有研究過框架的同學(xué)就會發(fā)現(xiàn),其實(shí)最核心的,就是用了拓展加上拓展來實(shí)現(xiàn)其底層的網(wǎng)絡(luò)服務(wù)和多進(jìn)程調(diào)度。我們在模式下,測試起五個(gè)進(jìn)程主進(jìn)程要等待回收我們,這樣就很簡單的實(shí)現(xiàn)了一個(gè)多進(jìn)程的協(xié)程服務(wù)。 有研究過Workman框架的同學(xué)就會發(fā)現(xiàn),其實(shí)workman最核心的,就是用了php socket拓展加上pcntl拓展來實(shí)現(xiàn)其底層的網(wǎng)絡(luò)服務(wù)和多進(jìn)程調(diào)度。那我們今天就來探討如何使用Swoole的...
摘要:多線程技術(shù)是個(gè)很龐大的課題,編程思想這本書英文版,以下簡稱中也用了頁介紹的多線程體系。一個(gè)線程歸屬于唯一的進(jìn)程,線程無法脫離進(jìn)程而存在。五線程內(nèi)數(shù)據(jù)線程的私有數(shù)據(jù)僅歸屬于一個(gè)線程,不在線程之間共享,例如,,。 多線程技術(shù)是個(gè)很龐大的課題,《Java編程思想》這本書(英文版,以下簡稱TIJ)中也用了136頁介紹Java的多線程體系。的確,Java語言發(fā)展到今天,多線程機(jī)制相比其他的語言從...
摘要:本文先回顧生成器,然后過渡到協(xié)程編程。其作用主要體現(xiàn)在三個(gè)方面數(shù)據(jù)生成生產(chǎn)者,通過返回?cái)?shù)據(jù)數(shù)據(jù)消費(fèi)消費(fèi)者,消費(fèi)傳來的數(shù)據(jù)實(shí)現(xiàn)協(xié)程。解決回調(diào)地獄的方式主要有兩種和協(xié)程。重點(diǎn)應(yīng)當(dāng)關(guān)注控制權(quán)轉(zhuǎn)讓的時(shí)機(jī),以及協(xié)程的運(yùn)作方式。 轉(zhuǎn)載請注明文章出處: https://tlanyan.me/php-review... PHP回顧系列目錄 PHP基礎(chǔ) web請求 cookie web響應(yīng) sess...
閱讀 1882·2021-11-25 09:43
閱讀 3177·2021-11-15 11:38
閱讀 2718·2019-08-30 13:04
閱讀 494·2019-08-29 11:07
閱讀 1508·2019-08-26 18:37
閱讀 2743·2019-08-26 14:07
閱讀 594·2019-08-26 13:52
閱讀 2289·2019-08-26 12:09