1. 準備
在動手以前,請肯定你用的不是M$ Windows平臺(由於我沒有Windows)。Linux / BSD / Unix應該都是沒問題的。確認好了工做環境之後一塊兒來看看咱們須要的PHP模塊是否都有。打開終端輸入下面的命令:小程序
$ php -m
這個命令檢查並打印當前PHP全部開啓的擴展,看一下pcntl
和posix
是否在輸出的列表中。函數
1.1. pcntl
若是找不到pcntl
,八成是編譯的時候沒把這個擴展編譯進去。若是你和我同樣是編譯安裝的PHP,那麼須要從新編譯安裝PHP。在配置的時候記得加上--enable-pcntl
參數便可。動畫
$ cd /path/to/php_source_code_dir $ ./configure [some other options] --enable-pcntl $ make && make install
1.2. posix
這貨通常默認就會裝上,只要你編譯的時候沒有加上--disable-posix
。spa
2. 預備知識
在繼續以前,你還須要對Linux多進程有一點了解。多進程是咋回事呢?這裏可跟火影忍者裏的影分身稍微有點不一樣。首先,鳴人從小長到大,好比16歲,咳。有一天他發動了影分身,分出了5個他。顯然,這些分身也是16歲的鳴人而不是剛出生啥也不懂就會哭的嬰兒(那叫克隆)。而後,不同的地方來了:分身們變成了獨立的人各自去作各自的事,互相之間再也不知道其餘分身和原身都作了什麼(固然不會像動畫片裏同樣積累經驗給原身啦)。除非,他們互相之間有交流,否則,只有16歲以前的事情纔是他們共同的記憶。操作系統
有同窗說了,老大你這不坑爹呢麼?我又沒看過火影忍者!那你去看一遍好了……code
最後,預備知識完了,就是大體瞭解一下主進程開出來的子進程是怎麼回事。子進程的代碼和主進程是徹底同樣的,還有一部分同樣的東西就是直到發動影分身以前執行的全部內容。具體請參見《操做系統》課程。token
3. 影分身之術
因此呢,沒有點基礎知識怎麼能理解卷軸裏的內容呢?打開卷軸首先看到了一個單詞:fork。進程
3.1. fork
叉子?叉子是分岔的,一個變多個嘛!差很少就是這個意思。建立子進程就用這個命令。這裏須要用到pcntl_fork()
函數。(能夠先簡單看一下PHP手冊關於這個函數的介紹。)建立一個PHP腳本:
$pid = pcntl_fork(); // 一旦調用成功,事情就變得有些不一樣了 if ($pid == -1) { die('fork failed'); } else if ($pid == 0) { } else { }
pcntl_fork()
函數建立一個子進程,子進程和父進程惟一的區別就是PID(進程ID)和PPID(父進程ID)不一樣。在終端下查看進程用ps
命令(問問man看ps怎麼用:man ps
)。當函數返回值爲-1的時候,說明fork失敗了。試試在if
前面加一句:echo $pid . PHP_EOL;
。運行你的腳本,輸出可能像下面這樣(結果說明子進程和父進程的代碼是相同的):
67789 # 這個是父進程打印的 0 # 這個是子進程打印的
pcntl_fork()
函數調用成功後,在父進程中會返回子進程的PID,而在子進程中返回的是0。因此,下面直接用if
分支來控制父進程和子進程作不一樣的事。
3.2. 分配任務
而後咱們來講說鳴人16歲那次影分身的事兒,給原身和分身分配兩個簡單的輸出任務:
$parentPid = getmypid(); // 這就是傳說中16歲以前的記憶 $pid = pcntl_fork(); // 一旦調用成功,事情就變得有些不一樣了 if ($pid == -1) { die('fork failed'); } else if ($pid == 0) { $mypid = getmypid(); // 用getmypid()函數獲取當前進程的PID echo 'I am child process. My PID is ' . $mypid . ' and my father's PID is ' . $parentPid . PHP_EOL; } else { echo 'Oh my god! I am a father now! My child's PID is ' . $pid . ' and mine is ' . $parentPid . PHP_EOL; }
輸出的結果多是這樣:
Oh my god! I am a father now! My child's PID is 68066 and mine is 68065 I am child process. My PID is 68066 and my father's PID is 68065
再強調一下,pcntl_fork()
調用成功之後,一個程序變成了兩個程序:一個程序獲得的$pid
變量值是0,它是子進程;另外一個程序獲得的$pid
的值大於0,這個值是子進程的PID,它是父進程。在下面的分支語句中,因爲$pid
值的不一樣,運行了不一樣的代碼。再次強調一下:子進程的代碼和父進程的是同樣的。因此就要經過分支語句給他們分配不一樣的任務。
3.3. 子進程回收
剛剛有man ps
麼?通常我習慣用ps aux
加上grep
命令來查找運行着的後臺進程。其中有一列STAT
,標識了每一個進程的運行狀態。這裏,咱們關注狀態Z
:殭屍(Zombie)。當子進程比父進程先退出,而父進程沒對其作任何處理的時候,子進程將會變成殭屍進程。Oops,又跟火影裏的影分身不同了。鳴人的影分身被幹死了之後就自動消失了,可是這裏的子進程分身死了話還留着一個空殼在,直到父進程回收它。殭屍進程雖然不佔什麼內存,可是很礙眼,院子裏一堆躺着的殭屍怎麼都以爲怪怪的。(別忘了它們還佔用着PID)
通常來講,在父進程結束以前回收掛掉的子進程就能夠了。在pcntl
擴展裏面有一個pcntl_wait()
函數,它會將父進程掛起,直到有一個子進程退出爲止。若是有一個子進程變成了殭屍的話,它會當即返回。全部的子進程都要回收,因此多等等也不要緊啦!
3.4. 父進程先掛了
若是父進程先掛了怎麼辦?會發生什麼?什麼也不會發生,子進程依舊還在運行。可是這個時候,子進程會被交給1號進程,1號進程成爲了這些子進程的繼父。1號進程會很好地處理這些進程的資源,當它們結束時1號進程會自動回收資源。因此,另外一種處理殭屍進程的臨時辦法是關閉它們的父進程。
4. 信號
通常多進程的事兒講到上面就完了,但是信號在系統中確實是一個很是重要的東西。信號就是信號燈,點亮一個信號燈,程序就會作出反應。這個你必定用過,好比說在終端下運行某個程序,等了半天也沒什麼反應,可能你會按 Ctrl+C 來關閉這個程序。實際上,這裏就是經過鍵盤向程序發送了一箇中斷的信號:SIGINT。有時候進程失去響應了還會執行kill [PID]
命令,未加任何其餘參數的話,程序會接收到一個SIGTERM信號。程序收到上面兩個信號的時候,默認都會結束執行,那麼是否有可能改變這種默認行爲呢?必須能啊!
4.1. 註冊信號
人是活的程序也是活的,只不過程序須要遵循人制定的規則來運行。如今開始給信號從新設定規則,這裏用到的函數是pcntl_signal()
(繼續以前爲啥不先查查PHP手冊呢?)。下面這段程序將給SIGINT從新定義行爲,注意看好:
// 定義一個處理器,接收到SIGINT信號後只輸出一行信息 function signalHandler($signal) { if ($signal == SIGINT) { echo 'signal received' . PHP_EOL; } } // 信號註冊:當接收到SIGINT信號時,調用signalHandler()函數 pcntl_signal(SIGINT, 'signalHandler'); while (true) { sleep(1); // do something pcntl_signal_dispatch(); // 接收到信號時,調用註冊的signalHandler() }
執行一下,隨時按下 Ctrl+C 看看會發生什麼事。
4.2. 信號分發
說明一下:pcntl_signal()
函數僅僅是註冊信號和它的處理方法,真正接收到信號並調用其處理方法的是pcntl_signal_dispatch()
函數。試試把// do something
替換成下面這段代碼:
for ($i = 0; $i < 1000000; $i++) { echo $i . PHP_EOL; usleep(100000); }
在終端下執行這個腳本,當它不停輸出數字的時候嘗試按下 Ctrl+C 。看看程序有什麼響應?嗯……什麼都沒有,除了屏幕可能多了個^C
之外,程序一直在不停地輸出數字。由於程序一直沒有執行到pcntl_signal_dispatch()
,因此就並無調用signalHandler()
,因此就沒有輸出signal received
。
4.3. 版本問題
若是認真看了PHP文檔,會發現pcntl_signal_dispatch()
這個函數是PHP 5.3以上才支持的,若是你的PHP版本大於5.3,建議使用這個方法調用信號處理器。5.3如下的版本須要在註冊信號以前加一句:declare(ticks = 1);
表示每執行一條低級指令,就檢查一次信號,若是檢測到註冊的信號,就調用其信號處理器。想一想就挺不爽的,幹嗎一直都檢查?仍是在咱們指定的地方檢查一下就好。
4.4. 感覺殭屍進程
如今咱們回到子進程回收的問題上(差點忘了= =")。當你的一個子進程掛了(或者說是結束了),可是父進程還在運行中而且可能很長一段時間不會退出。一個殭屍進程今後站起來了!這時,保護傘公司(內核)發現它的地盤裏出現了一個殭屍,這個殭屍是誰兒子呢?看一下PPID就知道了。而後,內核給PPID這個進程(也就是殭屍進程的父進程)發送一個信號:SIGCHLD。而後,你知道怎麼在父進程中回收這個子進程了麼?提示一下,用pcntl_wait()
函數。
4.5. 發送信號
但願剛剛有認真man過kill
命令。它其實就是向進程發送信號,在PHP中也能夠調用posix_kill()
函數來達到相同的效果。有了它就能夠在父進程中控制其餘子進程的運行了。好比在父進程結束以前關閉全部子進程,那麼fork
的時候在父進程記錄全部子進程的PID,父進程結束以前依次給子進程發送結束信號便可。
5. 實踐
PHP的多進程跟C仍是挺像的,搞明白了之後用其餘語言寫的話也大同小異差很少都是這麼個狀況。若是有空的話,嘗試寫一個小程序,切身體會一下箇中滋味:
- 16歲的鳴人發送影分身,分出5個分身
- 每一個分身隨機生存10到30秒,每秒都輸出點什麼
- 保證原身能感覺到分身的結束,而後開動另外一個分身,保證最多有5個分身
- 不使用
nohup
,讓原身在終端關閉後依舊可以運行 - 把分身數量(5)寫進一個配置文件裏,當給原身發送信號(能夠考慮用SIGUSR1或SIGUSR2)時,原身讀取配置文件並更新容許的分身最大數量
- 若是分身多了,關閉幾個;若是少了,再分出來幾個
提示:
- 用
while
循環保證進程運行,注意sleep
以避免100%的CPU佔用 - 運行進程的終端被關閉時,程序會收到一個SIGHUP信號
- 能夠用
parse_ini_file()
函數解析INI配置文件