linux fork進程請謹慎多個進程/線程共享一個 socket連接,會出現多個進程響應串聯的情況。


昨天組內同學在使用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:      lordgnu@me.com
 *
 * 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版本

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM