實(shí)際上PHP是有多線程的屎鳍,只是很多人不常用。使用PHP的多線程首先需要下載安裝一個(gè)線程安全版本(ZTS版本)的PHP问裕,然后再安裝pecl的?pthread?擴(kuò)展逮壁。
實(shí)際上PHP是有多進(jìn)程的,有一些人再用粮宛,總體來(lái)說(shuō)php的多進(jìn)程還算湊合窥淆,只需要在安裝PHP的時(shí)候開(kāi)啟pcntl模塊(是不是跟UNIX中的fcntl有點(diǎn)兒…. ….)即可。在*NIX下巍杈,在終端命令行下使用php -m就可以看到是否開(kāi)啟了pcntl模塊忧饭。
所以我們只說(shuō)php的多進(jìn)程,至于php多線程就暫時(shí)放到一邊兒筷畦。
注意:不要在apache或者fpm環(huán)境下使用php多進(jìn)程词裤,這將會(huì)產(chǎn)生不可預(yù)估的后果。
PHP多進(jìn)程初探
進(jìn)程是程序執(zhí)行的實(shí)例鳖宾,舉個(gè)例子有個(gè)程序叫做 “ 病毒.exe ”吼砂,這個(gè)程序平時(shí)是以文件形式存儲(chǔ)在硬盤(pán)上,當(dāng)你雙擊運(yùn)行后鼎文,就會(huì)形成一個(gè)該程序的進(jìn)程渔肩。系統(tǒng)會(huì)給每一個(gè)進(jìn)程分配一個(gè)唯一的非負(fù)整數(shù)用來(lái)標(biāo)記進(jìn)程,這個(gè)數(shù)字稱作進(jìn)程ID拇惋。當(dāng)該進(jìn)程被殺死或終止后周偎,其進(jìn)程ID就會(huì)被系統(tǒng)回收,然后分配給新的其余的進(jìn)程撑帖。
說(shuō)了這么多蓉坎,這鬼東西有什么用嗎?我平時(shí)用CI胡嘿、YII寫(xiě)個(gè)CURD跟這個(gè)也沒(méi)啥關(guān)聯(lián)啊袍嬉。實(shí)際上,如果你了解APACHE PHP MOD或者FPM就知道這些東西就是多進(jìn)程實(shí)現(xiàn)的灶平。以FPM為例,一般都是nginx作為http服務(wù)器擋在最前面箍土,靜態(tài)文件請(qǐng)求則nginx自行處理逢享,遇到php動(dòng)態(tài)請(qǐng)求則轉(zhuǎn)發(fā)給php-fpm進(jìn)程來(lái)處理。如果你的php-fpm配置只開(kāi)了5個(gè)進(jìn)程吴藻,如果處理任意一個(gè)用戶的請(qǐng)求都需要1秒鐘瞒爬,那么5個(gè)fpm進(jìn)程1秒中就最多只能處5個(gè)用戶的請(qǐng)求。所以結(jié)論就是:如果要單位時(shí)間內(nèi)干活更快更多,就需要更多的進(jìn)程侧但,總之一句話就是多進(jìn)程可以加快任務(wù)處理速度矢空。
在php中我們使用pcntl_fork()來(lái)創(chuàng)建多進(jìn)程(在*NIX系統(tǒng)的C語(yǔ)言編程中,已有進(jìn)程通過(guò)調(diào)用fork函數(shù)來(lái)產(chǎn)生新的進(jìn)程)禀横。fork出來(lái)新進(jìn)程則成為子進(jìn)程屁药,原進(jìn)程則成為父進(jìn)程,子進(jìn)程擁有父進(jìn)程的副本柏锄。這里要注意:
1.子進(jìn)程與父進(jìn)程共享程序正文段
2.子進(jìn)程擁有父進(jìn)程的數(shù)據(jù)空間和堆酿箭、棧的副本,注意是副本趾娃,不是共享
3.父進(jìn)程和子進(jìn)程將繼續(xù)執(zhí)行fork之后的程序代碼
4.fork之后缭嫡,是父進(jìn)程先執(zhí)行還是子進(jìn)程先執(zhí)行無(wú)法確認(rèn),取決于系統(tǒng)調(diào)度(取決于信仰)
這里說(shuō)子進(jìn)程擁有父進(jìn)程數(shù)據(jù)空間以及堆抬闷、棧的副本妇蛀,實(shí)際上,在大多數(shù)的實(shí)現(xiàn)中也并不是真正的完全副本笤成。更多是采用了COW(Copy On Write)即寫(xiě)時(shí)復(fù)制的技術(shù)來(lái)節(jié)約存儲(chǔ)空間评架。簡(jiǎn)單來(lái)說(shuō),如果父進(jìn)程和子進(jìn)程都不修改這些 數(shù)據(jù)疹启、堆古程、棧 的話,那么父進(jìn)程和子進(jìn)程則是暫時(shí)共享同一份 數(shù)據(jù)喊崖、堆挣磨、棧。只有當(dāng)父進(jìn)程或者子進(jìn)程試圖對(duì) 數(shù)據(jù)荤懂、堆嗓蘑、棧 進(jìn)行修改的時(shí)候,才會(huì)產(chǎn)生復(fù)制操作尝胆,這就叫做寫(xiě)時(shí)復(fù)制暇韧。
在調(diào)用完pcntl_fork()后,該函數(shù)會(huì)返回兩個(gè)值廊宪。在父進(jìn)程中返回子進(jìn)程的進(jìn)程ID矾瘾,在子進(jìn)程內(nèi)部本身返回?cái)?shù)字0。由于多進(jìn)程在apache或者fpm環(huán)境下無(wú)法正常運(yùn)行箭启,所以大家一定要在php cli環(huán)境下執(zhí)行下面php代碼壕翩。
第一段代碼,我們來(lái)說(shuō)明在程序從pcntl_fork()后父進(jìn)程和子進(jìn)程將各自繼續(xù)往下執(zhí)行代碼:
$pid = pcntl_fork(); //fork出子進(jìn)程
if( $pid ==-1 ){//創(chuàng)建錯(cuò)誤返回 -1 //錯(cuò)誤處理:創(chuàng)建子進(jìn)程失敗時(shí)返回-1.
die('進(jìn)程fork失敗');
} else if($pid >0) {// $pid > 0, 如果fork成功傅寡,返回子進(jìn)程id
echo "我是父進(jìn)程".PHP_EOL;
} else {// $pid = 0
echo "我是子進(jìn)程".PHP_EOL;
}
將文件保存為test.php放妈,然后在使用cli執(zhí)行北救,結(jié)果如下圖所示:
第二段代碼,用來(lái)說(shuō)明子進(jìn)程擁有父進(jìn)程的數(shù)據(jù)副本芜抒,而并不是共享:
//初始化一個(gè) number變量 數(shù)值為1
$number = 1;
$pid = pcntl_fork();
if( $pid > 0 ){
? ? $number += 1;
? echo "我是父進(jìn)程珍策,number+1 : { $number }".PHP_EOL;
} else if( 0 == $pid ) {
? ? $number += 2;
? echo "我是子進(jìn)程,number+2 : { $number }".PHP_EOL;
} else {
? ? die('進(jìn)程fork失敗');
}
第三段代碼宅倒,比較容易讓人思維混亂攘宙,pcntl_fork()配合for循環(huán)來(lái)做些東西,問(wèn)題來(lái)了:會(huì)顯示幾次 “ 兒子 ”唉堪?
for($i=1; $i<=3;$i++){
? ? echo "當(dāng)前進(jìn)程:" . getmypid() . PHP_EOL;
? ? $pid = pcntl_fork();
? ? if( $pid > 0 ){
? ? ? ? echo "我是父進(jìn)程".PHP_EOL;
? ? } else if( 0 == $pid ) {
? ? ? ? echo "我是子進(jìn)程".PHP_EOL;
? ? } else {
? ? ? ? echo "進(jìn)程fork失敗".PHP_EOL;;
????}
}
上面代碼執(zhí)行結(jié)果如下:
仔細(xì)數(shù)數(shù)模聋,竟然是顯示了7次 “ 兒子 ”。好奇怪唠亚,難道不是3次嗎链方?… …
下面我修改一下代碼,結(jié)合下面的代碼灶搜,再思考一下為什么會(huì)產(chǎn)生7次而不是3次祟蚀。
for($i=1; $i<=3;$i++){
? ? echo "當(dāng)前進(jìn)程:" . getmypid() . PHP_EOL;
? ? $pid = pcntl_fork();
? ? if( $pid > 0 ){
? ? ? ? ? ? echo "我是父進(jìn)程".PHP_EOL;
? ? } else if( 0 == $pid ) {
? ? ? ? echo "我是子進(jìn)程".PHP_EOL;
? ? ? ? exit;
? ? } else {
? ? ? ? echo "進(jìn)程fork失敗".PHP_EOL;;
????}
}
執(zhí)行結(jié)果如下圖所示:
前面強(qiáng)調(diào)過(guò):父進(jìn)程和子進(jìn)程將繼續(xù)執(zhí)行fork之后的程序代碼。這里就不解釋割卖,實(shí)在想不明白的前酿,可以動(dòng)手自己畫(huà)畫(huà)思考一下。
孤兒與僵尸進(jìn)程
實(shí)際上鹏溯,你們一定要記装瘴:PHP的多進(jìn)程是非常值得應(yīng)用于生產(chǎn)環(huán)境具備高價(jià)值的生產(chǎn)力工具。
但我認(rèn)為在正式開(kāi)始吹牛之前還是要說(shuō)兩個(gè)基本概念:孤兒進(jìn)程丙挽、僵尸進(jìn)程肺孵。
上文我整篇尬聊的都是pcntl_fork(),只管fork生產(chǎn)颜阐,不管產(chǎn)后護(hù)理平窘,實(shí)際上這樣并不符合主流價(jià)值觀,而且凳怨,操作系統(tǒng)本身資源有限瑰艘,這樣無(wú)限生產(chǎn)不顧護(hù)理,操作系統(tǒng)也會(huì)吃不消的肤舞。
孤兒進(jìn)程是指父進(jìn)程在fork出子進(jìn)程后紫新,自己先完了。這個(gè)問(wèn)題很尷尬李剖,因?yàn)樽舆M(jìn)程從此變得無(wú)依無(wú)靠弊琴、無(wú)家可歸,變成了孤兒杖爽。用術(shù)語(yǔ)來(lái)表達(dá)就是敲董,父進(jìn)程在子進(jìn)程結(jié)束之前提前退出,這些子進(jìn)程將由init(進(jìn)程ID為1)進(jìn)程收養(yǎng)并完成對(duì)其各種數(shù)據(jù)狀態(tài)的收集慰安。init進(jìn)程是Linux系統(tǒng)下的奇怪進(jìn)程腋寨,這個(gè)進(jìn)程是以普通用戶權(quán)限運(yùn)行但卻具備超級(jí)權(quán)限的進(jìn)程,簡(jiǎn)單地說(shuō)化焕,這個(gè)進(jìn)程在Linux系統(tǒng)啟動(dòng)的時(shí)候做初始化工作萄窜,比如運(yùn)行g(shù)etty、比如會(huì)根據(jù)/etc/inittab中設(shè)置的運(yùn)行等級(jí)初始化系統(tǒng)等等撒桨,當(dāng)然了查刻,還有一個(gè)作用就是如上所說(shuō)的:收養(yǎng)孤兒進(jìn)程。
僵尸進(jìn)程是指父進(jìn)程在fork出子進(jìn)程凤类,而后子進(jìn)程在結(jié)束后穗泵,父進(jìn)程并沒(méi)有調(diào)用wait或者waitpid等完成對(duì)其清理善后工作,導(dǎo)致改子進(jìn)程進(jìn)程ID谜疤、文件描述符等依然保留在系統(tǒng)中佃延,極大浪費(fèi)了系統(tǒng)資源。所以夷磕,僵尸進(jìn)程是對(duì)系統(tǒng)有危害的履肃,而孤兒進(jìn)程則相對(duì)來(lái)說(shuō)沒(méi)那么嚴(yán)重。在Linux系統(tǒng)中坐桩,我們可以通過(guò)ps -aux來(lái)查看進(jìn)程尺棋,如果有[Z+]標(biāo)記就是僵尸進(jìn)程。
在PHP中绵跷,父進(jìn)程對(duì)子進(jìn)程的狀態(tài)收集等是通過(guò)pcntl_wait()和pcntl_waitpid()等完成的膘螟。依然還是要通過(guò)代碼還演示說(shuō)明:
演示并說(shuō)明孤兒進(jìn)程的出現(xiàn),并演示孤兒進(jìn)程被init進(jìn)程收養(yǎng):
$pid= pcntl_fork();
if( $pid > 0 ){
? ? // 顯示父進(jìn)程的進(jìn)程ID抖坪,這個(gè)函數(shù)可以是getmypid()萍鲸,也可以用posix_getpid()
? ? echo "父進(jìn)程 PID:".getmypid().PHP_EOL;
? ? // 讓父進(jìn)程停止兩秒鐘,在這兩秒內(nèi)擦俐,子進(jìn)程的父進(jìn)程ID還是這個(gè)父進(jìn)程
? ? sleep( 2 );
} else if( 0 == $pid ) {
? ? // 讓子進(jìn)程循環(huán)10次脊阴,每次睡眠1s,然后每秒鐘獲取一次子進(jìn)程的父進(jìn)程進(jìn)程ID
? ? for( $i = 1; $i <= 10; $i++ ){
? ? ? ? sleep( 1 );
? ? ? ? // posix_getppid()函數(shù)的作用就是獲取當(dāng)前進(jìn)程的父進(jìn)程進(jìn)程ID
? ? ? ? echo posix_getppid().PHP_EOL;
}
} else {
? ? echo "fork error.".PHP_EOL;
}
運(yùn)行結(jié)果如下圖:
可以看到蚯瞧,前兩秒內(nèi)嘿期,子進(jìn)程的父進(jìn)程進(jìn)程ID為4129,但是從第三秒開(kāi)始埋合,由于父進(jìn)程已經(jīng)提前退出了备徐,子進(jìn)程變成孤兒進(jìn)程,所以init進(jìn)程收養(yǎng)了子進(jìn)程甚颂,所以子進(jìn)程的父進(jìn)程進(jìn)程ID變成了1蜜猾。
演示并說(shuō)明僵尸進(jìn)程的出現(xiàn)秀菱,并演示僵尸進(jìn)程的危害:
$pid = pcntl_fork();
if( $pid > 0 ){
? ? // 下面這個(gè)函數(shù)可以更改php進(jìn)程的名稱
? ? cli_set_process_title('php father process');
? ? // 讓主進(jìn)程休息60秒鐘
? ? sleep(60);
} else if( 0 == $pid ) {
? ? cli_set_process_title('php child process');
? ? // 讓子進(jìn)程休息10秒鐘,但是進(jìn)程結(jié)束后蹭睡,父進(jìn)程不對(duì)子進(jìn)程做任何處理工作衍菱,這樣這個(gè)子進(jìn)程就會(huì)變成僵尸進(jìn)程
? ? sleep(10);
} else {
? ? exit('fork error.'.PHP_EOL);
}
運(yùn)行結(jié)果如下圖:
通過(guò)執(zhí)行ps -aux命令可以看到,當(dāng)程序在前十秒內(nèi)運(yùn)行的時(shí)候肩豁,php child process的狀態(tài)列為[S+]脊串,然而在十秒鐘過(guò)后,這個(gè)狀態(tài)變成了[Z+]清钥,也就是變成了危害系統(tǒng)的僵尸進(jìn)程琼锋。
那么,問(wèn)題來(lái)了祟昭?如何避免僵尸進(jìn)程呢缕坎?PHP通過(guò)pcntl_wait()和pcntl_waitpid()兩個(gè)函數(shù)來(lái)幫我們解決這個(gè)問(wèn)題。了解Linux系統(tǒng)編程的應(yīng)該知道从橘,看名字就知道這其實(shí)就是PHP把C語(yǔ)言中的wait()和waitpid()包裝了一下念赶。
通過(guò)代碼演示pcntl_wait()來(lái)避免僵尸進(jìn)程,在開(kāi)始之前先簡(jiǎn)單普及一下pcntl_wait()的相關(guān)內(nèi)容:這個(gè)函數(shù)的作用就是 “ 等待或者返回子進(jìn)程的狀態(tài) ”恰力,當(dāng)父進(jìn)程執(zhí)行了該函數(shù)后叉谜,就會(huì)阻塞掛起等待子進(jìn)程的狀態(tài)一直等到子進(jìn)程已經(jīng)由于某種原因退出或者終止。換句話說(shuō)就是如果子進(jìn)程還沒(méi)結(jié)束踩萎,那么父進(jìn)程就會(huì)一直等等等停局,如果子進(jìn)程已經(jīng)結(jié)束,那么父進(jìn)程就會(huì)立刻得到子進(jìn)程狀態(tài)香府。這個(gè)函數(shù)返回退出的子進(jìn)程的進(jìn)程ID或者失敗返回-1董栽。
我們將第二個(gè)案例中代碼修改一下:
//修改部分代碼+pcntl_wait()
$pid = pcntl_fork();
if( $pid > 0 ){
? ? // 下面這個(gè)函數(shù)可以更改php進(jìn)程的名稱
? ? cli_set_process_title('php father process');
? ? // 返回$wait_result,就是子進(jìn)程的進(jìn)程號(hào)企孩,如果子進(jìn)程已經(jīng)是僵尸進(jìn)程則為0
? ? // 子進(jìn)程狀態(tài)則保存在了$status參數(shù)中锭碳,可以通過(guò)pcntl_wexitstatus()等一系列函數(shù)來(lái)查看$status的狀態(tài)信息是什么
? ? $wait_result = pcntl_wait( $status );
? ? print_r( $wait_result );
? ? print_r( $status );
? ? // 讓主進(jìn)程休息60秒鐘
? ? sleep(60);
} else if( 0 == $pid ) {
? ? cli_set_process_title('php child process');
? ? // 讓子進(jìn)程休息10秒鐘,但是進(jìn)程結(jié)束后勿璃,父進(jìn)程不對(duì)子進(jìn)程做任何處理工作擒抛,這樣這個(gè)子進(jìn)程就會(huì)變成僵尸進(jìn)程
? ? sleep(10);
} else {
? ? exit('fork error.'.PHP_EOL);
}
將文件保存為wait.php,然后php wait.php补疑,在另外一個(gè)終端中通過(guò)ps -aux查看歧沪,可以看到在前十秒內(nèi),php child process是[S+]狀態(tài)莲组,然后十秒鐘過(guò)后進(jìn)程消失了诊胞,也就是被父進(jìn)程回收了,沒(méi)有變成僵尸進(jìn)程锹杈。
但是撵孤,pcntl_wait()有個(gè)很大的問(wèn)題迈着,就是阻塞。父進(jìn)程只能掛起等待子進(jìn)程結(jié)束或終止邪码,在此期間父進(jìn)程什么都不能做寥假,這并不符合多快好省原則,所以pcntl_waitpid()閃亮登場(chǎng)霞扬。pcntl_waitpid( $pid, &$status, $option = 0 )的第三個(gè)參數(shù)如果設(shè)置為WNOHANG,那么父進(jìn)程不會(huì)阻塞一直等待到有子進(jìn)程退出或終止枫振,否則將會(huì)和pcntl_wait()的表現(xiàn)類似喻圃。
修改第三個(gè)案例的代碼,但是粪滤,我們并不添加WNOHANG斧拍,演示說(shuō)明pcntl_waitpid()功能:
$pid = pcntl_fork();
if( $pid > 0 ){
? ? // 下面這個(gè)函數(shù)可以更改php進(jìn)程的名稱
? ? cli_set_process_title('php father process');
? ? // 返回值保存在$wait_result中
? ? // $pid參數(shù)表示 子進(jìn)程的進(jìn)程ID
? ? // 子進(jìn)程狀態(tài)則保存在了參數(shù)$status中
? ? // 將第三個(gè)option參數(shù)設(shè)置為常量WNOHANG,則可以避免主進(jìn)程阻塞掛起杖小,此處父進(jìn)程將立即返回繼續(xù)往下執(zhí)行剩下的代碼
? ? $wait_result = pcntl_waitpid( $pid, $status );
? ? var_dump( $wait_result );
? ? var_dump( $status );
? ? // 讓主進(jìn)程休息60秒鐘
? ? sleep(60);
} else if( 0 == $pid ) {
? ? cli_set_process_title('php child process');
? ? // 讓子進(jìn)程休息10秒鐘肆汹,但是進(jìn)程結(jié)束后,父進(jìn)程不對(duì)子進(jìn)程做任何處理工作予权,這樣這個(gè)子進(jìn)程就會(huì)變成僵尸進(jìn)程
? ? sleep(10);
} else {
? ? exit('fork error.'.PHP_EOL);
}
下面是運(yùn)行結(jié)果昂勉,一個(gè)執(zhí)行php程序的終端窗口,另一個(gè)是ps -aux終端窗口扫腺。實(shí)際上可以看到主進(jìn)程是被阻塞的岗照,一直到第十秒子進(jìn)程退出了,父進(jìn)程不再阻塞:
那么我們修改第四段代碼笆环,添加第三個(gè)參數(shù)WNOHANG攒至,代碼如下:
$pid = pcntl_fork();
if( $pid > 0 ){
? ? // 下面這個(gè)函數(shù)可以更改php進(jìn)程的名稱
? ? cli_set_process_title('php father process');
? ? // 返回值保存在$wait_result中
? ? // $pid參數(shù)表示 子進(jìn)程的進(jìn)程ID
? ? // 子進(jìn)程狀態(tài)則保存在了參數(shù)$status中
? ? // 將第三個(gè)option參數(shù)設(shè)置為常量WNOHANG,則可以避免主進(jìn)程阻塞掛起躁劣,此處父進(jìn)程將立即返回繼續(xù)往下執(zhí)行剩下的代碼
? ? $wait_result = pcntl_waitpid( $pid, $status, WNOHANG );
? ? var_dump( $wait_result );
? ? var_dump( $status );
? ? echo "不阻塞迫吐,運(yùn)行到這里".PHP_EOL;
? ? // 讓主進(jìn)程休息60秒鐘
? ? sleep(60);
} else if( 0 == $pid ) {
? ? cli_set_process_title('php child process');
? ? // 讓子進(jìn)程休息10秒鐘,但是進(jìn)程結(jié)束后账忘,父進(jìn)程不對(duì)子進(jìn)程做任何處理工作志膀,這樣這個(gè)子進(jìn)程就會(huì)變成僵尸進(jìn)程
? ? sleep(10);
} else {
? ? exit('fork error.'.PHP_EOL);
}
下面是運(yùn)行結(jié)果,一個(gè)執(zhí)行php程序的終端窗口闪萄,另一個(gè)是ps -aux終端窗口梧却。實(shí)際上可以看到主進(jìn)程是被阻塞的,一直到第十秒子進(jìn)程退出了败去,父進(jìn)程不再阻塞:
問(wèn)題出現(xiàn)了放航,竟然php child process進(jìn)程狀態(tài)竟然變成了[Z+],這是怎么搞得圆裕?回頭分析一下代碼:
我們看到子進(jìn)程是睡眠了十秒鐘广鳍,而父進(jìn)程在執(zhí)行pcntl_waitpid()之前沒(méi)有任何睡眠且本身不再阻塞荆几,所以,主進(jìn)程自己先執(zhí)行下去了赊时,而子進(jìn)程在足足十秒鐘后才結(jié)束吨铸,進(jìn)程狀態(tài)自然無(wú)法得到回收。如果我們將代碼修改一下祖秒,就是在主進(jìn)程的pcntl_waitpid()前睡眠15秒鐘诞吱,這樣就可以回收子進(jìn)程了。但是即便這樣修改竭缝,細(xì)心想的話還是會(huì)有個(gè)問(wèn)題房维,那就是在子進(jìn)程結(jié)束后,在父進(jìn)程執(zhí)行pcntl_waitpid()回收前抬纸,有五秒鐘的時(shí)間差咙俩,在這個(gè)時(shí)間差內(nèi),php child process也將會(huì)是僵尸進(jìn)程湿故。那么阿趁,pcntl_waitpid()如何正確使用啊坛猪?這樣用脖阵,看起來(lái)畢竟不太科學(xué)。
那么砚哆,是時(shí)候引入信號(hào)量了独撇!
信號(hào)是一種軟件中斷,也是一種非常典型的異步事件處理方式躁锁。在NIX系統(tǒng)誕生的混沌之初纷铣,信號(hào)的定義是比較混亂的,而且最關(guān)鍵是不可靠战转,這是一個(gè)很?chē)?yán)重的問(wèn)題搜立。所以在后來(lái)的POSIX標(biāo)準(zhǔn)中,對(duì)信號(hào)做了標(biāo)準(zhǔn)化同時(shí)也各個(gè)發(fā)行版的NIX也都提供大量可靠的信號(hào)槐秧。每種信號(hào)都有自己的名字啄踊,大概如SIGTERM、SIGHUP刁标、SIGCHLD等等颠通,在*NIX中,這些信號(hào)本質(zhì)上都是整形數(shù)字(游有心情的可以參觀一下signal.h系列頭文件)膀懈。
信號(hào)的產(chǎn)生是有多種方式的顿锰,下面是常見(jiàn)的幾種:
鍵盤(pán)上按某些組合鍵,比如Ctrl+C或者Ctrl+D等,會(huì)產(chǎn)生SIGINT信號(hào)硼控。
使用posix kill調(diào)用刘陶,可以向某個(gè)進(jìn)程發(fā)送指定的信號(hào)。
遠(yuǎn)程ssh終端情況下牢撼,如果你在服務(wù)器上執(zhí)行了一個(gè)阻塞的腳本匙隔,正在阻塞過(guò)程中你關(guān)閉了終端,可能就會(huì)產(chǎn)生SIGHUP信號(hào)熏版。
硬件也會(huì)產(chǎn)生信號(hào)纷责,比如OOM了或者遇到除0這種情況,硬件也會(huì)向進(jìn)程發(fā)送特定信號(hào)撼短。
而進(jìn)程在收到信號(hào)后碰逸,可以有如下三種響應(yīng):
直接忽略,不做任何反映阔加。就是俗稱的完全不鳥(niǎo)。但是有兩種信號(hào)满钟,永遠(yuǎn)不會(huì)被忽略胜榔,一個(gè)是SIGSTOP,另一個(gè)是SIGKILL湃番,因?yàn)檫@兩個(gè)進(jìn)程提供了向內(nèi)核最后的可靠的結(jié)束進(jìn)程的辦法夭织。
捕捉信號(hào)并作出相應(yīng)的一些反應(yīng),具體響應(yīng)什么可以由用戶自己通過(guò)程序自定義吠撮。
系統(tǒng)默認(rèn)響應(yīng)尊惰。大多數(shù)進(jìn)程在遇到信號(hào)后,如果用戶也沒(méi)有自定義響應(yīng)泥兰,那么就會(huì)采取系統(tǒng)默認(rèn)響應(yīng)弄屡,大多數(shù)的系統(tǒng)默認(rèn)響應(yīng)就是終止進(jìn)程。
用人話來(lái)表達(dá)鞋诗,就是說(shuō)假如你是一個(gè)進(jìn)程膀捷,你正在干活,突然施工隊(duì)的喇叭里沖你嚷了一句:“吃飯了削彬!”全庸,于是你就放下手里的活兒去吃飯。你正在干活融痛,突然施工隊(duì)的喇叭里沖你嚷了一句:“發(fā)工資了壶笼!”,于是你就放下手里的活兒去領(lǐng)工資雁刷。你正在干活覆劈,突然施工隊(duì)的喇叭里沖你嚷了一句:“有人找你!”,于是你就放下手里的活兒去看看是誰(shuí)找你什么事情墩崩。當(dāng)然了氓英,你很任性,那是完全可以不鳥(niǎo)喇叭里喊什么內(nèi)容鹦筹,也就是忽略信號(hào)铝阐。也可以更任性,當(dāng)喇叭里沖你嚷“吃飯”的時(shí)候铐拐,你去就不去吃飯徘键,你去睡覺(jué),這些都可以由你來(lái)遍蟋。而你在干活過(guò)程中吹害,從來(lái)不會(huì)因?yàn)橐饶硞€(gè)信號(hào)就不干活了一直等信號(hào),而是信號(hào)隨時(shí)隨地都可能會(huì)來(lái)虚青,而你只需要在這個(gè)時(shí)候作出相應(yīng)的回應(yīng)即可它呀,所以說(shuō),信號(hào)是一種軟件中斷棒厘,也是一種異步的處理事件的方式纵穿。
回到上文所說(shuō)的問(wèn)題,就是子進(jìn)程在結(jié)束前奢人,父進(jìn)程就已經(jīng)先調(diào)用了pcntl_waitpid()谓媒,導(dǎo)致子進(jìn)程在結(jié)束后依然變成了僵尸進(jìn)程。實(shí)際上在父進(jìn)程不斷while循環(huán)調(diào)用pcntl_waitpid()是個(gè)解決辦法何乎,大概代碼如下:
$pid = pcntl_fork();if(0> $pid) {exit('fork error.'. PHP_EOL);}else{if(0< $pid) {// 在父進(jìn)程中cli_set_process_title('php father process');// 父進(jìn)程不斷while循環(huán)句惯,去反復(fù)執(zhí)行pcntl_waitpid(),從而試圖解決已經(jīng)退出的子進(jìn)程while(true) {? ? ? ? ? ? sleep(1);? ? ? ? ? ? pcntl_waitpid($pid, &$status, WNOHANG);? ? ? ? }? ? }else{if(0== $pid) {// 在子進(jìn)程中// 子進(jìn)程休眠3秒鐘后直接退出cli_set_process_title('php child process');? ? ? ? ? ? sleep(20);exit;? ? ? ? }? ? }}
下圖是運(yùn)行結(jié)果:
解析一下這個(gè)結(jié)果支救,我先后三次執(zhí)行了ps -aux | grep php去查看這兩個(gè)php進(jìn)程抢野。
第一次:子進(jìn)程正在休眠中,父進(jìn)程依舊在循環(huán)中各墨。
第二次:子進(jìn)程已經(jīng)退出了蒙保,父進(jìn)程依舊在循環(huán)中,但是代碼還沒(méi)有執(zhí)行到pcntl_waitpid()欲主,所以在子進(jìn)程退出后到父進(jìn)程執(zhí)行回收前這段空隙內(nèi)子進(jìn)程變成了僵尸進(jìn)程邓厕。
第三次:此時(shí)父進(jìn)程已經(jīng)執(zhí)行了pcntl_waitpid(),將已經(jīng)退出的子進(jìn)程回收扁瓢,釋放了pid等資源详恼。
但是這樣的代碼有一個(gè)缺陷,實(shí)際上就是子進(jìn)程已經(jīng)退出的情況下引几,主進(jìn)程還在不斷while pcntl_waitpid()去回收子進(jìn)程昧互,這是一件很奇怪的事情挽铁,并不符合社會(huì)主義主流價(jià)值觀,不低碳不節(jié)能敞掘,代碼也不優(yōu)雅叽掘,不好看。所以玖雁,應(yīng)該考慮用更好的方式來(lái)實(shí)現(xiàn)更扁。那么,我們篇頭提了許久的信號(hào)終于概要出場(chǎng)了赫冬。
現(xiàn)在讓我們考慮一下浓镜,為何信號(hào)可以解決“不低碳不節(jié)能,代碼也不優(yōu)雅劲厌,不好看”的問(wèn)題膛薛。子進(jìn)程在退出的時(shí)候,會(huì)向父進(jìn)程發(fā)送一個(gè)信號(hào)补鼻,叫做SIGCHLD哄啄,那么父進(jìn)程一旦收到了這個(gè)信號(hào),就可以作出相應(yīng)的回收動(dòng)作风范,也就是執(zhí)行pcntl_waitpid()增淹,從而解決掉僵尸進(jìn)程,而且還顯得我們代碼優(yōu)雅好看節(jié)能環(huán)保乌企。
梳理一下流程,子進(jìn)程向父進(jìn)程發(fā)送SIGCHLD信號(hào)是對(duì)人們來(lái)說(shuō)是透明的成玫,也就是說(shuō)我們無(wú)須關(guān)心加酵。但是,我們需要給父進(jìn)程安裝一個(gè)響應(yīng)SIGCHLD信號(hào)的處理器哭当,除此之外猪腕,還需要讓這些信號(hào)處理器運(yùn)行起來(lái),安裝上了不運(yùn)行是一件尷尬的事情钦勘。那么陋葡,在php里給進(jìn)程安裝信號(hào)處理器使用的函數(shù)是pcntl_signal(),讓信號(hào)處理器跑起來(lái)的函數(shù)是pcntl_signal_dispatch()彻采。
pcntl_signal()腐缤,安裝一個(gè)信號(hào)處理器,具體說(shuō)明是pcntl_signal ( int $signo , callback $handler [, bool $restart_syscalls = true ] )肛响,參數(shù)signo就是信號(hào)岭粤,callback則是響應(yīng)該信號(hào)的代碼段,返回bool值特笋。
pcntl_signal_dispatch()剃浇,調(diào)用每個(gè)等待信號(hào)通過(guò)pcntl_signal()?安裝的處理器,參數(shù)為void,返回bool值虎囚。
下面結(jié)合新引入的兩個(gè)函數(shù)來(lái)解決一下樓上的丑陋代碼:
$pid = pcntl_fork();
if( 0 > $pid ){
? ? exit('fork error.'.PHP_EOL);
} else if( 0 < $pid ) {
? ? // 在父進(jìn)程中
? ? // 給父進(jìn)程安裝一個(gè)SIGCHLD信號(hào)處理器
? ? pcntl_signal( SIGCHLD, function() use( $pid ) {
? ? ? ? echo "收到子進(jìn)程退出".PHP_EOL;
? ? ? ? pcntl_waitpid( $pid, $status, WNOHANG );
} );
? ? cli_set_process_title('php father process');
? ? // 父進(jìn)程不斷while循環(huán)角塑,去反復(fù)執(zhí)行pcntl_waitpid(),從而試圖解決已經(jīng)退出的子進(jìn)程
? ? while( true ){
? ? ? ? sleep( 1 );
? ? ? ? // 注釋掉原來(lái)老掉牙的代碼淘讥,轉(zhuǎn)而使用pcntl_signal_dispatch()
//pcntl_waitpid( $pid, &$status, WNOHANG );
? ? ? ? pcntl_signal_dispatch();
}
} else if( 0 == $pid ) {
? ? // 在子進(jìn)程中
? ? // 子進(jìn)程休眠3秒鐘后直接退出
? ? cli_set_process_title('php child process');
? ? sleep( 20 );
? ? exit;
}
運(yùn)行結(jié)果如下:
PHP 預(yù)定義了一些信號(hào)量圃伶,可參看?http://php.net/manual/zh/pcntl.constants.php。
原文地址:
https://blog.ti-node.com/blog/6363989547574886401
https://blog.ti-node.com/blog/6375675957193211905