昨天組內同學在使用php父子進程模式的時候遇到了一個比較詭異的問題 簡單說來就是:因為fork,父子進程共用了一個redis連接、然後父子進程在發送了各自的redis請求分別獲取到了對方的響應體。 復現示例代碼: testFork.php PowerSpawn.php 主要用戶進程fork管理工作 ...
昨天組內同學在使用php父子進程模式的時候遇到了一個比較詭異的問題
簡單說來就是:因為fork,父子進程共用了一個redis連接、然後父子進程在發送了各自的redis請求分別獲取到了對方的響應體。
復現示例代碼:
testFork.php
1 <?php 2 require_once("./PowerSpawn.php"); 3 4 $ps = new Forkutil_PowerSpawn(); 5 $ps->maxChildren = 10 ; 6 $ps->timeLimit = 86400; 7 8 $redisObj = new Redis(); 9 $redisObj->connect('127.0.0.1','6379'); 10 11 // 主進程 -- 查詢任務列表並新建子進程 12 while ($ps->runParentCode()) { 13 echo "parent:".$redisObj->get("parent")."\n" ; 14 // 產生一個子進程 15 if ($ps->spawnReady()) { 16 $ps->spawnChild(); 17 } else { 18 // 隊列已滿,等待 19 $ps->Tick(); 20 } 21 } 22 23 // 子進程 -- 處理具體的任務 24 if ($ps->runChildCode()) { 25 echo "chlidren:".$redisObj->get("children")."\n" ; 26 }
PowerSpawn.php 主要用戶進程fork管理工作
<?php /* * PowerSpawn * * Object wrapper for handling process forking within PHP * Depends on PCNTL package * Depends on POSIX package * * Author: Don Bauer * E-Mail: [email protected] * * Date: 2011-11-04 */ declare(ticks = 1); class Forkutil_PowerSpawn { private $myChildren; private $parentPID; private $shutdownCallback = null; private $killCallback = null; public $maxChildren = 10; // Max number of children allowed to Spawn public $timeLimit = 0; // Time limit in seconds (0 to disable) public $sleepCount = 100; // Number of uSeconds to sleep on Tick() public $childData; // Variable for storage of data to be passed to the next spawned child public $complete; public function __construct() { if (function_exists('pcntl_fork') && function_exists('posix_getpid')) { // Everything is good $this->parentPID = $this->myPID(); $this->myChildren = array(); $this->complete = false; // Install the signal handler pcntl_signal(SIGCHLD, array($this, 'sigHandler')); } else { die("You must have POSIX and PCNTL functions to use PowerSpawn\n"); } } public function __destruct() { } public function sigHandler($signo) { switch ($signo) { case SIGCHLD: $this->checkChildren(); break; } } public function getChildStatus($name = false) { if ($name === false) return false; if (isset($this->myChildren[$name])) { return $this->myChildren[$name]; } else { return false; } } public function checkChildren() { foreach ($this->myChildren as $i => $child) { // Check for time running and if still running if ($this->pidDead($child['pid']) != 0) { // Child is dead unset($this->myChildren[$i]); } elseif ($this->timeLimit > 0) { // Check the time limit if (time() - $child['time'] >= $this->timeLimit) { // Child had exceeded time limit $this->killChild($child['pid']); unset($this->myChildren[$i]); } } } } /** * 獲取當前進程pid * @return int */ public function myPID() { return posix_getpid(); } /** * 獲取父進程pid * @return int */ public function myParent() { return posix_getppid(); } /** * 創建子進程 並記錄到myChildren中 * @param bool $name */ public function spawnChild($name = false) { $time = time(); $pid = pcntl_fork(); if ($pid) { if ($name !== false) { $this->myChildren[$name] = array('time'=>$time,'pid'=>$pid); } else { $this->myChildren[] = array('time'=>$time,'pid'=>$pid); } } } /** * 殺死子進程 * @param int $pid */ public function killChild($pid = 0) { if ($pid > 0) { posix_kill($pid, SIGTERM); if ($this->killCallback !== null) call_user_func($this->killCallback); } } /** * 該進程是否主進程 是返回true 不是返回false * @return bool */ public function parentCheck() { if ($this->myPID() == $this->parentPID) { return true; } else { return false; } } public function pidDead($pid = 0) { if ($pid > 0) { return pcntl_waitpid($pid, $status, WUNTRACED OR WNOHANG); } else { return 0; } } public function setCallback($callback = null) { $this->shutdownCallback = $callback; } public function setKillCallback($callback = null) { $this->killCallback = $callback; } /** * 返回子進程個數 * @return int */ public function childCount() { return count($this->myChildren); } public function runParentCode() { if (!$this->complete) { return $this->parentCheck(); } else { if ($this->shutdownCallback !== null) call_user_func($this->shutdownCallback); return false; } } public function runChildCode() { return !$this->parentCheck(); } /** * 進程池是否已滿 * @return bool */ public function spawnReady() { if (count($this->myChildren) < $this->maxChildren) { return true; } else { return false; } } public function shutdown() { while($this->childCount()) { $this->checkChildren(); $this->tick(); } $this->complete = true; } public function tick() { usleep($this->sleepCount); } public function exec($proc, $args = null) { if ($args == null) { pcntl_exec($proc); } else { pcntl_exec($proc, $args); } } }View Code
解釋一下testFork.php做的事情:子進程從父進程fork出來之後,父子進程各自從redis中取數據,父進程取parent這個key的數據。子進程取child這個key的數據
終端的輸出結果是:
parent:parent parent:parent parent:children chlidren:parent
很顯然,在偶然的情況下:子進程讀到了父進程的結果、父進程讀到了子進程該讀的結果。
先說結論,再看原因。
linux fork進程請謹慎多個進程/線程共用一個 socket連接,會出現多個進程響應串聯的情況。
有經驗的朋友應該會想起unix網路編程中在寫併發server代碼的時候,fork子進程之後立馬關閉了子進程的listenfd,原因也是類似的。
昨天,寫這份代碼的同學,自己悶頭查了很長時間,其實還是對於fork沒有重分瞭解,匆忙的寫下這份代碼。
使用父子進程模式之前,得先問一下自己幾個問題:
1.你的代碼真的需要父子進程來做嗎?(當然這不是今天討論的話題,對於php業務場景而言、我覺得基本不需要)
2.fork產生的子進程到底與父進程有什麼關係?複製的變數相互間的更改是否受影響?
《UNIX系統編程》第24章進程的創建 中對上面的兩個問題給出了完美的回答、下麵我摘抄幾個知識點:
1.fork之後父子進程將共用代碼文本段,但是各自擁有不同的棧段、數據段及堆段拷貝。子進程的棧、數據從fork一瞬間開始是對於父進程的完全拷貝、每個進程可以更改自己的數據,而不要擔心相互影響!
2.fork之後父子進程同時開始從fork點向下執行代碼,具體fork之後CPU會調度到誰?不一定!
3.執行fork之後,子進程將拷貝父進程的文件描述符副本,指向同一個文件句柄(包含了當前文件讀寫的偏移量等信息)。對於socket而言,其實是復用了同一個socket,這也是文章開頭提到的問題所在。
那麼再回頭看開始提到的問題,當fork之後,父子進程同時共用同一條redis連接。
一條tcp連接唯一標識的辦法是那個四元組:clientip + clientport + serverip + serverport
那當兩個進程同時指向了一個socket,socket改把響應體給誰呢?我的理解是CPU片分到誰誰會去讀取,當然這個理解也可能是錯誤的,在評論區給出你的理解,謝謝
文章的最後談幾點我的想法:
1.php業務場景下需要使用多進程模式的並不多,如果你覺得真的需要使用fork來完成業務,可以先思考一下,真的需要嗎?
2.當遇到問題的時候,最先看的還應該是你所使用技術的到底做了啥,主動與身邊人溝通
3.《UNIX系統編程》是一本好書,英文名是《The Linux Programming Interface》,簡稱《TLPI》,我在這本書里找到了很多我想找到的答案。作為一個寫php的、讀C的程式員來說,簡單易懂。
比如進程的創建、IO相關主題、select&poll&信號驅動IO&epoll,特別是事件驅動這塊非常推薦閱讀,後面我也會在我弄明白網路請求到達網卡之後、linux內核做了啥?然後結合事件驅動再記一篇我的理解。
我把《UNIX系統編程》電子版書籍放到了我的公眾號,如果需要可以掃碼關註我的公眾號&回覆 "TLPI",即可下載 《UNIX系統編程》《The Linux Programming Interface》的pdf版本