摘要:多進程中與多進程相關(guān)的兩個重要拓展是和。函數(shù)執(zhí)行期間,主進程除了等待無法處理其他任務(wù),所以一般不認為這是多進程編程。回收子進程有兩種方式,一種是主進程調(diào)用函數(shù)等待子進程結(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,我們需要多進程或多線程。但在常規(guī)web開發(fā)中,我們極少用到這兩種并發(fā)技術(shù)(curl_multi等特殊函數(shù)除外)。如果腳本運行在CLI模式下,多進程和多線程技術(shù)是提高多核CPU的有力工具。
相對于多線程,多進程的程序具有健壯、無鎖、對分布式支持更好等特點。本文來學(xué)習(xí)一下PHP的多進程編程。
多進程PHP中與(多)進程相關(guān)的兩個重要拓展是PCNTL和POSIX。PCNTL主要用來創(chuàng)建、執(zhí)行子進程和處理信號,POSIX拓展則實現(xiàn)了POSIX標準中定義的接口。由于Windows不是POSIX兼容的,所以POSIX拓展在Windows平臺上不可用。
先上簡單的代碼看多進程編程:
// 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ù)返回一個整數(shù),小于0表示克隆失敗。克隆成功的情況下返回兩個值:父進程拿到子進程的進程號,而子進程則得到0。可以根據(jù)函數(shù)的返回值判斷接下來的執(zhí)行環(huán)境在父進程中還是子進程中。
fork調(diào)用讓系統(tǒng)創(chuàng)建一個與當(dāng)前進程幾乎完全一樣的進程,除了進程號等少數(shù)信息不一樣,進程的代碼段、堆棧、數(shù)據(jù)段的值都一致。父進程打開了一個文件,復(fù)制的子進程同樣享有這個句柄,這是過去多進程能監(jiān)聽同一個端口的原理;子進程基于父進程fork時的環(huán)境繼續(xù)執(zhí)行(代碼段共享)直到退出。
去掉上述代碼中else語句塊的exit能將幫助你更好地理解上面這段話。程序的本意是生成10個子進程,去掉子進程執(zhí)行代碼的exit后,子進程執(zhí)行完else塊中代碼后繼續(xù)執(zhí)行foreach循環(huán),最終生成55個子進程(為什么是55個?)!鑒于此,一個良好的實踐是在子進程的執(zhí)行代碼后總是加上exit終止語句,除非你真的有把握子進程會按照預(yù)期執(zhí)行。
除了fork,另外一種多進程技術(shù)是exec。system、exec、proc_open等函數(shù)會生成一個新的進程執(zhí)行外部命令(并返回結(jié)果)。這些函數(shù)的本質(zhì)是fork一個進程,然后調(diào)用shell執(zhí)行命令,主進程等待其執(zhí)行結(jié)束。函數(shù)執(zhí)行期間,主進程除了等待無法處理其他任務(wù),所以一般不認為這是多進程編程。實踐中可以結(jié)合fork來并發(fā)執(zhí)行外部命令。
孤兒進程與僵尸進程多進程編程需要考慮到的一個問題是孤兒進程和僵尸進程。進程結(jié)束前父進程已經(jīng)退出,進程變成孤兒進程;進程退出后父進程在執(zhí)行且未回收子進程,那么進程變成僵尸進程。孤兒進程是仍在執(zhí)行的進程,僵尸進程則已經(jīng)停止執(zhí)行,只剩下進程號一縷孤魂仍能被外界感知。
孤兒進程會被系統(tǒng)的根進程(init進程,進程號為1)接管,運行結(jié)束后由根進程回收。下面代碼演示孤兒進程的父進程的變化:
// 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
父進程退出后子進程過繼給1號根進程,并由其負責(zé)回收子進程。
接著看僵尸進程。主進程長時間運行且不回收子進程,僵尸進程會一直存在,直到主進程退出后變成孤兒進程過繼給根進程;如果主進程一直運行,僵尸進程將一直存在。
下面代碼演示生成10個僵尸進程:
// 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,然后新打開一個終端執(zhí)行ps aux | grep php | grep -v grep,一個可能的輸出如下:
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]
最后一列為
回收子進程有兩種方式,一種是主進程調(diào)用pcntl_wait/pcntl_waitpid函數(shù)等待子進程結(jié)束;另外一種是處理SIGCLD信號。我們先說使用wait函數(shù)回收子進程,信號處理放在下面的章節(jié)。
PCNT拓展中用于回收子進程的兩個函數(shù)是pcntl_wait和pcntl_waitpid,pcntl_waitpid可以指定等待的進程。來看如何用這兩個函數(shù)回收子進程:
// 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,然后打開另外一個終端執(zhí)行:watch -n2 "ps aux | grep php | grep -v grep"。從watch輸出可以看到子進程退出后的5秒內(nèi)是僵尸進程,父進程回收后僵尸進程消失,最后父進程退出。
如果有多個子進程,父進程需要循環(huán)調(diào)用wait函數(shù),否則某些子進程執(zhí)行完畢后也會變成僵尸進程。
信號處理PCNTL拓展中的pcntl_signal函數(shù)用于安裝信號函數(shù),進程收到信號時會執(zhí)行回調(diào)函數(shù)中的代碼。我們知道Ctrl + C可以中斷程序的執(zhí)行,原理是按下組合鍵后系統(tǒng)向程序發(fā)出SIGINT信號。這個信號的默認操作是退出程序,所以系統(tǒng)終止了程序運行。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é)束程序,可以向進程發(fā)送無法捕捉的信號,例如SIGKILL。ps aux | grep php找到程序的進程號,然后用kill命令發(fā)送SIGKILL信號:kill -SIGKILL 進程號。程序收到信號后被操作系統(tǒ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的值,錯誤表示代碼中不能捕捉這個信號。
支持哪些信號,默認操作是什么,和系統(tǒng)相關(guān)。絕大部分*nix系統(tǒng)支持SIGINT、SIGKILL等31個常見異步信號,某些系統(tǒng)支持更多的信號。
內(nèi)核收到進程信號后,會查看進程是否注冊了處理函數(shù),如果未注冊則執(zhí)行默認操作;否則當(dāng)進程運行在用戶態(tài)時,內(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ā)信號消息。這個函數(shù)效率上比tick高,實時性上比手動觸發(fā)強。其原理是當(dāng)程序從內(nèi)核態(tài)切出、函數(shù)返回等時機檢查是否有信號,有則執(zhí)行回調(diào)。
理解了信號,再看看如何使用信號解決僵尸進程問題。子進程退出后,操作系統(tǒng)會發(fā)送SIGCLD信號到父進程,在信號回調(diào)函數(shù)中回收子進程即可,詳情見下面代碼:
// 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方式,信號處理無疑更為簡潔高效。
信號也是進程中通信的一種方式。接下來簡要說一下進程間通信。
進程間通信fork出子進程后,兩個進程的數(shù)據(jù)段和堆棧(理論上)均分開。與多線程不同,全局變量在不同進程中無法共享。進程間要進行數(shù)據(jù)交換,必須通過進程間通信(Inter-Process Communication)技術(shù)。上文提到的信號是進程中通信技術(shù)的一種,posix_kill函數(shù)可以向指定進程發(fā)送信號,達到通信的目的。
進程間通信技術(shù)主要有:
管道(pipe),流管道(s_pipe)和有名管道(FIFO);
信號(signal);
消息隊列(message queue);
共享內(nèi)存(share memory);
信號量(semaphore);
套接字(socket);
這些通信技術(shù)的詳細內(nèi)容請參考文末的鏈接,或者其他文獻,本文不再詳述。
守護進程通過php test.php方式執(zhí)行程序,關(guān)閉終端后程序會退出。要讓程序能長期執(zhí)行,需要額外的手段。總結(jié)起來主要有三種:
nohup;
screen/tmux等工具;
fork子進程后,父進程退出,子進程升為會話/進程組長,脫離終端繼續(xù)運行。
screen/tmux方式程序?qū)嶋H上仍停留在終端,只是運行在一個長期存在的終端中。nohup和fork方式才是讓程序脫離(detach)終端,達到肉體飛升的正道(成為daemon)。
下面的代碼通過fork的方式讓程序成為守護進程:
// 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之后最重要的一個操作是posix_setsid,該函數(shù)把當(dāng)前進程設(shè)置為會話組長(被設(shè)置的進程當(dāng)前不能是組長)。某些開源庫中會fork兩次,防止第一次fork的進程無意間打開終端(非會話組長無法打開終端)。
執(zhí)行程序:php daemon.php,然后關(guān)閉終端,或者重新登錄,通過ps aux | grep daemon.php查看程序均在執(zhí)行。檢測/tmp/daemon.out,不斷有內(nèi)容輸出,說明程序已經(jīng)成為在后臺持續(xù)運行的守護進程。
注意后臺的多進程應(yīng)當(dāng)在進程脫離終端后再fork,即最終在后臺干活的進程不能直接從腳本啟動的進程fork,而應(yīng)該至少是腳本啟動進程的孫子進程。
應(yīng)用下面來說一個多進程的簡單應(yīng)用。在上一篇博文“PHP回顧之Socket編程”,我們的服務(wù)端已經(jīng)能做到幾乎實時響應(yīng)客戶端的請求,但是客戶端不是實時收到服務(wù)端下發(fā)的消息。利用多進程,我們用一個進程專門負責(zé)讀取服務(wù)端的消息,另一個進程則負責(zé)收集用戶在終端的輸入,然后發(fā)送到服務(wù)端。下面是多進程的客戶端代碼:
// client.php "echo", "args" => $args, ]); fwrite($socket, $message); } } }
執(zhí)行客戶端:php client.php,會發(fā)現(xiàn)終端輸入和服務(wù)端消息都能及時響應(yīng)。同時,連接斷開的信號也被正確的廣播。
總結(jié)本文簡要介紹了多進程編程的幾個方面,最后給出一個應(yīng)用的例子,希望對學(xué)習(xí)多進程的同行有幫助。
感謝閱讀!
參考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://specialneedsforspecialkids.com/yun/28899.html
摘要:所以這次采用多進程的方式來實現(xiàn)同時為多個客戶端提供服務(wù)。而多進程則是通過創(chuàng)建多個進程來共同完成一件事。如果是子進程的執(zhí)行環(huán)境,則返回。正常情況下,子進程是通過父進程創(chuàng)建的。以上則是我們的多進程回聲服務(wù)程序。 上次的回聲服務(wù)程序有個很大的缺點,就是只能同時連接一個客戶端,這明顯是不合理的。 所以這次采用多進程的方式來實現(xiàn)同時為多個客戶端提供服務(wù)。 以下是最終的效果:showImg(htt...
摘要:有研究過框架的同學(xué)就會發(fā)現(xiàn),其實最核心的,就是用了拓展加上拓展來實現(xiàn)其底層的網(wǎng)絡(luò)服務(wù)和多進程調(diào)度。我們在模式下,測試起五個進程主進程要等待回收我們,這樣就很簡單的實現(xiàn)了一個多進程的協(xié)程服務(wù)。 有研究過Workman框架的同學(xué)就會發(fā)現(xiàn),其實workman最核心的,就是用了php socket拓展加上pcntl拓展來實現(xiàn)其底層的網(wǎng)絡(luò)服務(wù)和多進程調(diào)度。那我們今天就來探討如何使用Swoole的...
摘要:多線程技術(shù)是個很龐大的課題,編程思想這本書英文版,以下簡稱中也用了頁介紹的多線程體系。一個線程歸屬于唯一的進程,線程無法脫離進程而存在。五線程內(nèi)數(shù)據(jù)線程的私有數(shù)據(jù)僅歸屬于一個線程,不在線程之間共享,例如,,。 多線程技術(shù)是個很龐大的課題,《Java編程思想》這本書(英文版,以下簡稱TIJ)中也用了136頁介紹Java的多線程體系。的確,Java語言發(fā)展到今天,多線程機制相比其他的語言從...
摘要:本文先回顧生成器,然后過渡到協(xié)程編程。其作用主要體現(xiàn)在三個方面數(shù)據(jù)生成生產(chǎn)者,通過返回數(shù)據(jù)數(shù)據(jù)消費消費者,消費傳來的數(shù)據(jù)實現(xiàn)協(xié)程。解決回調(diào)地獄的方式主要有兩種和協(xié)程。重點應(yīng)當(dāng)關(guān)注控制權(quán)轉(zhuǎn)讓的時機,以及協(xié)程的運作方式。 轉(zhuǎn)載請注明文章出處: https://tlanyan.me/php-review... PHP回顧系列目錄 PHP基礎(chǔ) web請求 cookie web響應(yīng) sess...
閱讀 1870·2021-11-25 09:43
閱讀 3161·2021-11-15 11:38
閱讀 2708·2019-08-30 13:04
閱讀 483·2019-08-29 11:07
閱讀 1491·2019-08-26 18:37
閱讀 2697·2019-08-26 14:07
閱讀 582·2019-08-26 13:52
閱讀 2278·2019-08-26 12:09