您好,登錄后才能下訂單哦!
PHP多進程、信號量及孤兒進程和僵尸進程是什么?這個問題可能是我們日常學習或工作經常見到的。希望通過這個問題能讓你收獲頗深。下面是小編給大家帶來的參考內容,讓我們一起來看看吧!
PHP多進程、信號量及孤兒進程和僵尸進程
實際上PHP是有多線程的,只是很多人不常用。使用PHP的多線程首先需要下載安裝一個線程安全版本(ZTS版本)的PHP,然后再安裝pecl的 pthread 擴展。
實際上PHP是有多進程的,有一些人再用,總體來說php的多進程還算湊合,只需要在安裝PHP的時候開啟pcntl模塊(是不是跟UNIX中的fcntl有點兒…. ….)即可。在*NIX下,在終端命令行下使用php -m就可以看到是否開啟了pcntl模塊。
所以我們只說php的多進程,至于php多線程就暫時放到一邊兒。
注意:不要在apache或者fpm環境下使用php多進程,這將會產生不可預估的后果。
PHP多進程初探
進程是程序執行的實例,舉個例子有個程序叫做 “ 病毒.exe ”,這個程序平時是以文件形式存儲在硬盤上,當你雙擊運行后,就會形成一個該程序的進程。系統會給每一個進程分配一個唯一的非負整數用來標記進程,這個數字稱作進程ID。當該進程被殺死或終止后,其進程ID就會被系統回收,然后分配給新的其余的進程。
說了這么多,這鬼東西有什么用嗎?我平時用CI、YII寫個CURD跟這個也沒啥關聯啊。實際上,如果你了解APACHE PHP MOD或者FPM就知道這些東西就是多進程實現的。以FPM為例,一般都是nginx作為http服務器擋在最前面,靜態文件請求則nginx自行處理,遇到php動態請求則轉發給php-fpm進程來處理。如果你的php-fpm配置只開了5個進程,如果處理任意一個用戶的請求都需要1秒鐘,那么5個fpm進程1秒中就最多只能處5個用戶的請求。所以結論就是:如果要單位時間內干活更快更多,就需要更多的進程,總之一句話就是多進程可以加快任務處理速度。
在php中我們使用pcntl_fork()來創建多進程(在*NIX系統的C語言編程中,已有進程通過調用fork函數來產生新的進程)。fork出來新進程則成為子進程,原進程則成為父進程,子進程擁有父進程的副本。這里要注意:
子進程與父進程共享程序正文段
子進程擁有父進程的數據空間和堆、棧的副本,注意是副本,不是共享
父進程和子進程將繼續執行fork之后的程序代碼
fork之后,是父進程先執行還是子進程先執行無法確認,取決于系統調度(取決于信仰)
這里說子進程擁有父進程數據空間以及堆、棧的副本,實際上,在大多數的實現中也并不是真正的完全副本。更多是采用了COW(Copy On Write)即寫時復制的技術來節約存儲空間。簡單來說,如果父進程和子進程都不修改這些 數據、堆、棧 的話,那么父進程和子進程則是暫時共享同一份 數據、堆、棧。只有當父進程或者子進程試圖對 數據、堆、棧 進行修改的時候,才會產生復制操作,這就叫做寫時復制。
在調用完pcntl_fork()后,該函數會返回兩個值。在父進程中返回子進程的進程ID,在子進程內部本身返回數字0。由于多進程在apache或者fpm環境下無法正常運行,所以大家一定要在php cli環境下執行下面php代碼。
第一段代碼,我們來說明在程序從pcntl_fork()后父進程和子進程將各自繼續往下執行代碼:
$pid = pcntl_fork(); if( $pid > 0 ){ echo "我是父親".PHP_EOL; } else if( 0 == $pid ) { echo "我是兒子".PHP_EOL; } else { echo "fork失敗".PHP_EOL; }
將文件保存為test.php,然后在使用cli執行,結果如下圖所示:
第二段代碼,用來說明子進程擁有父進程的數據副本,而并不是共享:
// 初始化一個 number變量 數值為1 $number = 1; $pid = pcntl_fork(); if( $pid > 0 ){ $number += 1; echo "我是父親,number+1 : { $number }".PHP_EOL; } else if( 0 == $pid ) { $number += 2; echo "我是父親,number+2 : { $number }".PHP_EOL; } else { echo "fork失敗".PHP_EOL; }
第三段代碼,比較容易讓人思維混亂,pcntl_fork()配合for循環來做些東西,問題來了:會顯示幾次 “ 兒子 ”?
for( $i = 1; $i <= 3 ; $i++ ){ $pid = pcntl_fork(); if( $pid > 0 ){ // do nothing ... } else if( 0 == $pid ){ echo "兒子".PHP_EOL; } }
上面代碼執行結果如下:
仔細數數,竟然是顯示了7次 “ 兒子 ”。好奇怪,難道不是3次嗎?… …
下面我修改一下代碼,結合下面的代碼,再思考一下為什么會產生7次而不是3次。
for( $i = 1; $i <= 3 ; $i++ ){ $pid = pcntl_fork(); if( $pid > 0 ){ // do nothing ... } else if( 0 == $pid ){ echo "兒子".PHP_EOL; exit; } }
執行結果如下圖所示:
前面強調過:父進程和子進程將繼續執行fork之后的程序代碼。這里就不解釋,實在想不明白的,可以動手自己畫畫思考一下。
孤兒與僵尸進程
實際上,你們一定要記住:PHP的多進程是非常值得應用于生產環境具備高價值的生產力工具。
但我認為在正式開始吹牛之前還是要說兩個基本概念:孤兒進程、僵尸進程。
上文我整篇尬聊的都是pcntl_fork(),只管fork生產,不管產后護理,實際上這樣并不符合主流價值觀,而且,操作系統本身資源有限,這樣無限生產不顧護理,操作系統也會吃不消的。
孤兒進程是指父進程在fork出子進程后,自己先完了。這個問題很尷尬,因為子進程從此變得無依無靠、無家可歸,變成了孤兒。用術語來表達就是,父進程在子進程結束之前提前退出,這些子進程將由init(進程ID為1)進程收養并完成對其各種數據狀態的收集。init進程是Linux系統下的奇怪進程,這個進程是以普通用戶權限運行但卻具備超級權限的進程,簡單地說,這個進程在Linux系統啟動的時候做初始化工作,比如運行getty、比如會根據/etc/inittab中設置的運行等級初始化系統等等,當然了,還有一個作用就是如上所說的:收養孤兒進程。
僵尸進程是指父進程在fork出子進程,而后子進程在結束后,父進程并沒有調用wait或者waitpid等完成對其清理善后工作,導致改子進程進程ID、文件描述符等依然保留在系統中,極大浪費了系統資源。所以,僵尸進程是對系統有危害的,而孤兒進程則相對來說沒那么嚴重。在Linux系統中,我們可以通過ps -aux來查看進程,如果有[Z+]標記就是僵尸進程。
在PHP中,父進程對子進程的狀態收集等是通過pcntl_wait()和pcntl_waitpid()等完成的。依然還是要通過代碼還演示說明:
演示并說明孤兒進程的出現,并演示孤兒進程被init進程收養:
$id = pcntl_fork();if( $pid > 0 ){ // 顯示父進程的進程ID,這個函數可以是getmypid(),也可以用posix_getpid() echo "Father PID:".getmypid().PHP_EOL; // 讓父進程停止兩秒鐘,在這兩秒內,子進程的父進程ID還是這個父進程 sleep( 2 ); } else if( 0 == $pid ) { // 讓子進程循環10次,每次睡眠1s,然后每秒鐘獲取一次子進程的父進程進程ID for( $i = 1; $i <= 10; $i++ ){ sleep( 1 ); // posix_getppid()函數的作用就是獲取當前進程的父進程進程ID echo posix_getppid().PHP_EOL; } } else { echo "fork error.".PHP_EOL; }
運行結果如下圖:
可以看到,前兩秒內,子進程的父進程進程ID為4129,但是從第三秒開始,由于父進程已經提前退出了,子進程變成孤兒進程,所以init進程收養了子進程,所以子進程的父進程進程ID變成了1。(php視頻教程)
演示并說明僵尸進程的出現,并演示僵尸進程的危害:
$pid = pcntl_fork(); if( $pid > 0 ){ // 下面這個函數可以更改php進程的名稱 cli_set_process_title('php father process'); // 讓主進程休息60秒鐘 sleep(60); } else if( 0 == $pid ) { cli_set_process_title('php child process'); // 讓子進程休息10秒鐘,但是進程結束后,父進程不對子進程做任何處理工作,這樣這個子進程就會變成僵尸進程 sleep(10); } else { exit('fork error.'.PHP_EOL); }
運行結果如下圖:
通過執行ps -aux命令可以看到,當程序在前十秒內運行的時候,php child process的狀態列為[S+],然而在十秒鐘過后,這個狀態變成了[Z+],也就是變成了危害系統的僵尸進程。
那么,問題來了?如何避免僵尸進程呢?PHP通過pcntl_wait()和pcntl_waitpid()兩個函數來幫我們解決這個問題。了解Linux系統編程的應該知道,看名字就知道這其實就是PHP把C語言中的wait()和waitpid()包裝了一下。
通過代碼演示pcntl_wait()來避免僵尸進程,在開始之前先簡單普及一下pcntl_wait()的相關內容:這個函數的作用就是 “ 等待或者返回子進程的狀態 ”,當父進程執行了該函數后,就會阻塞掛起等待子進程的狀態一直等到子進程已經由于某種原因退出或者終止。換句話說就是如果子進程還沒結束,那么父進程就會一直等等等,如果子進程已經結束,那么父進程就會立刻得到子進程狀態。這個函數返回退出的子進程的進程ID或者失敗返回-1。
我們將第二個案例中代碼修改一下:
$pid = pcntl_fork();if( $pid > 0 ){ // 下面這個函數可以更改php進程的名稱 cli_set_process_title('php father process'); // 返回$wait_result,就是子進程的進程號,如果子進程已經是僵尸進程則為0 // 子進程狀態則保存在了$status參數中,可以通過pcntl_wexitstatus()等一系列函數來查看$status的狀態信息是什么 $wait_result = pcntl_wait( $status ); print_r( $wait_result ); print_r( $status ); // 讓主進程休息60秒鐘 sleep(60); } else if( 0 == $pid ) { cli_set_process_title('php child process'); // 讓子進程休息10秒鐘,但是進程結束后,父進程不對子進程做任何處理工作,這樣這個子進程就會變成僵尸進程 sleep(10); } else { exit('fork error.'.PHP_EOL); }
將文件保存為wait.php,然后php wait.php,在另外一個終端中通過ps -aux查看,可以看到在前十秒內,php child process是[S+]狀態,然后十秒鐘過后進程消失了,也就是被父進程回收了,沒有變成僵尸進程。
但是,pcntl_wait()有個很大的問題,就是阻塞。父進程只能掛起等待子進程結束或終止,在此期間父進程什么都不能做,這并不符合多快好省原則,所以pcntl_waitpid()閃亮登場。pcntl_waitpid( $pid, &$status, $option = 0 )的第三個參數如果設置為WNOHANG,那么父進程不會阻塞一直等待到有子進程退出或終止,否則將會和pcntl_wait()的表現類似。
修改第三個案例的代碼,但是,我們并不添加WNOHANG,演示說明pcntl_waitpid()功能:
$pid = pcntl_fork(); if( $pid > 0 ){ // 下面這個函數可以更改php進程的名稱 cli_set_process_title('php father process'); // 返回值保存在$wait_result中 // $pid參數表示 子進程的進程ID // 子進程狀態則保存在了參數$status中 // 將第三個option參數設置為常量WNOHANG,則可以避免主進程阻塞掛起,此處父進程將立即返回繼續往下執行剩下的代碼 $wait_result = pcntl_waitpid( $pid, $status ); var_dump( $wait_result ); var_dump( $status ); // 讓主進程休息60秒鐘 sleep(60); } else if( 0 == $pid ) { cli_set_process_title('php child process'); // 讓子進程休息10秒鐘,但是進程結束后,父進程不對子進程做任何處理工作,這樣這個子進程就會變成僵尸進程 sleep(10); } else { exit('fork error.'.PHP_EOL); }
下面是運行結果,一個執行php程序的終端窗口,另一個是ps -aux終端窗口。實際上可以看到主進程是被阻塞的,一直到第十秒子進程退出了,父進程不再阻塞:
那么我們修改第四段代碼,添加第三個參數WNOHANG,代碼如下:
$pid = pcntl_fork(); if( $pid > 0 ){ // 下面這個函數可以更改php進程的名稱 cli_set_process_title('php father process'); // 返回值保存在$wait_result中 // $pid參數表示 子進程的進程ID // 子進程狀態則保存在了參數$status中 // 將第三個option參數設置為常量WNOHANG,則可以避免主進程阻塞掛起,此處父進程將立即返回繼續往下執行剩下的代碼 $wait_result = pcntl_waitpid( $pid, $status, WNOHANG ); var_dump( $wait_result ); var_dump( $status ); echo "不阻塞,運行到這里".PHP_EOL; // 讓主進程休息60秒鐘 sleep(60); } else if( 0 == $pid ) { cli_set_process_title('php child process'); // 讓子進程休息10秒鐘,但是進程結束后,父進程不對子進程做任何處理工作,這樣這個子進程就會變成僵尸進程 sleep(10); } else { exit('fork error.'.PHP_EOL); }
下面是運行結果,一個執行php程序的終端窗口,另一個是ps -aux終端窗口。實際上可以看到主進程是被阻塞的,一直到第十秒子進程退出了,父進程不再阻塞:
問題出現了,竟然php child process進程狀態竟然變成了[Z+],這是怎么搞得?回頭分析一下代碼:
我們看到子進程是睡眠了十秒鐘,而父進程在執行pcntl_waitpid()之前沒有任何睡眠且本身不再阻塞,所以,主進程自己先執行下去了,而子進程在足足十秒鐘后才結束,進程狀態自然無法得到回收。如果我們將代碼修改一下,就是在主進程的pcntl_waitpid()前睡眠15秒鐘,這樣就可以回收子進程了。但是即便這樣修改,細心想的話還是會有個問題,那就是在子進程結束后,在父進程執行pcntl_waitpid()回收前,有五秒鐘的時間差,在這個時間差內,php child process也將會是僵尸進程。那么,pcntl_waitpid()如何正確使用啊?這樣用,看起來畢竟不太科學。
那么,是時候引入信號量了!
PHP 信號量
信號是一種軟件中斷,也是一種非常典型的異步事件處理方式。在NIX系統誕生的混沌之初,信號的定義是比較混亂的,而且最關鍵是不可靠,這是一個很嚴重的問題。所以在后來的POSIX標準中,對信號做了標準化同時也各個發行版的NIX也都提供大量可靠的信號。每種信號都有自己的名字,大概如SIGTERM、SIGHUP、SIGCHLD等等,在*NIX中,這些信號本質上都是整形數字(游有心情的可以參觀一下signal.h系列頭文件)。
信號的產生是有多種方式的,下面是常見的幾種:
鍵盤上按某些組合鍵,比如Ctrl+C或者Ctrl+D等,會產生SIGINT信號。
使用posix kill調用,可以向某個進程發送指定的信號。
遠程ssh終端情況下,如果你在服務器上執行了一個阻塞的腳本,正在阻塞過程中你關閉了終端,可能就會產生SIGHUP信號。
硬件也會產生信號,比如OOM了或者遇到除0這種情況,硬件也會向進程發送特定信號。
而進程在收到信號后,可以有如下三種響應:
直接忽略,不做任何反映。就是俗稱的完全不鳥。但是有兩種信號,永遠不會被忽略,一個是SIGSTOP,另一個是SIGKILL,因為這兩個進程提供了向內核最后的可靠的結束進程的辦法。
捕捉信號并作出相應的一些反應,具體響應什么可以由用戶自己通過程序自定義。
系統默認響應。大多數進程在遇到信號后,如果用戶也沒有自定義響應,那么就會采取系統默認響應,大多數的系統默認響應就是終止進程。
用人話來表達,就是說假如你是一個進程,你正在干活,突然施工隊的喇叭里沖你嚷了一句:“吃飯了!”,于是你就放下手里的活兒去吃飯。你正在干活,突然施工隊的喇叭里沖你嚷了一句:“發工資了!”,于是你就放下手里的活兒去領工資。你正在干活,突然施工隊的喇叭里沖你嚷了一句:“有人找你!”,于是你就放下手里的活兒去看看是誰找你什么事情。當然了,你很任性,那是完全可以不鳥喇叭里喊什么內容,也就是忽略信號。也可以更任性,當喇叭里沖你嚷“吃飯”的時候,你去就不去吃飯,你去睡覺,這些都可以由你來。而你在干活過程中,從來不會因為要等某個信號就不干活了一直等信號,而是信號隨時隨地都可能會來,而你只需要在這個時候作出相應的回應即可,所以說,信號是一種軟件中斷,也是一種異步的處理事件的方式。
回到上文所說的問題,就是子進程在結束前,父進程就已經先調用了pcntl_waitpid(),導致子進程在結束后依然變成了僵尸進程。實際上在父進程不斷while循環調用pcntl_waitpid()是個解決辦法,大概代碼如下:
$pid = pcntl_fork();if (0 > $pid) { exit('fork error.' . PHP_EOL); } else { if (0 < $pid) { // 在父進程中 cli_set_process_title('php father process'); // 父進程不斷while循環,去反復執行pcntl_waitpid(),從而試圖解決已經退出的子進程 while (true) { sleep(1); pcntl_waitpid($pid, &$status, WNOHANG); } } else { if (0 == $pid) { // 在子進程中 // 子進程休眠3秒鐘后直接退出 cli_set_process_title('php child process'); sleep(20); exit; } } }
下圖是運行結果:
解析一下這個結果,我先后三次執行了ps -aux | grep php去查看這兩個php進程。
第一次:子進程正在休眠中,父進程依舊在循環中。
第二次:子進程已經退出了,父進程依舊在循環中,但是代碼還沒有執行到pcntl_waitpid(),所以在子進程退出后到父進程執行回收前這段空隙內子進程變成了僵尸進程。
第三次:此時父進程已經執行了pcntl_waitpid(),將已經退出的子進程回收,釋放了pid等資源。
但是這樣的代碼有一個缺陷,實際上就是子進程已經退出的情況下,主進程還在不斷while pcntl_waitpid()去回收子進程,這是一件很奇怪的事情,并不符合社會主義主流價值觀,不低碳不節能,代碼也不優雅,不好看。所以,應該考慮用更好的方式來實現。那么,我們篇頭提了許久的信號終于概要出場了。
現在讓我們考慮一下,為何信號可以解決“不低碳不節能,代碼也不優雅,不好看”的問題。子進程在退出的時候,會向父進程發送一個信號,叫做SIGCHLD,那么父進程一旦收到了這個信號,就可以作出相應的回收動作,也就是執行pcntl_waitpid(),從而解決掉僵尸進程,而且還顯得我們代碼優雅好看節能環保。
梳理一下流程,子進程向父進程發送SIGCHLD信號是對人們來說是透明的,也就是說我們無須關心。但是,我們需要給父進程安裝一個響應SIGCHLD信號的處理器,除此之外,還需要讓這些信號處理器運行起來,安裝上了不運行是一件尷尬的事情。那么,在php里給進程安裝信號處理器使用的函數是pcntl_signal(),讓信號處理器跑起來的函數是pcntl_signal_dispatch()。
pcntl_signal(),安裝一個信號處理器,具體說明是pcntl_signal ( int $signo , callback $handler [, bool $restart_syscalls = true ] ),參數signo就是信號,callback則是響應該信號的代碼段,返回bool值。
pcntl_signal_dispatch(),調用每個等待信號通過pcntl_signal() 安裝的處理器,參數為void,返回bool值。
下面結合新引入的兩個函數來解決一下樓上的丑陋代碼:
$pid = pcntl_fork();if( 0 > $pid ){ exit('fork error.'.PHP_EOL); } else if( 0 < $pid ) { // 在父進程中 // 給父進程安裝一個SIGCHLD信號處理器 pcntl_signal( SIGCHLD, function() use( $pid ) { echo "收到子進程退出".PHP_EOL; pcntl_waitpid( $pid, $status, WNOHANG ); } ); cli_set_process_title('php father process'); // 父進程不斷while循環,去反復執行pcntl_waitpid(),從而試圖解決已經退出的子進程 while( true ){ sleep( 1 ); // 注釋掉原來老掉牙的代碼,轉而使用pcntl_signal_dispatch() //pcntl_waitpid( $pid, &$status, WNOHANG ); pcntl_signal_dispatch(); } } else if( 0 == $pid ) { // 在子進程中 // 子進程休眠3秒鐘后直接退出 cli_set_process_title('php child process'); sleep( 20 ); exit; }
感謝各位的閱讀!看完上述內容,你們對PHP多進程、信號量及孤兒進程和僵尸進程是什么大概了解了嗎?希望文章內容對大家有所幫助。如果想了解更多相關文章內容,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。