使用php的fork進行父子進程代碼編寫,你至少需要對linux fork有這幾點基礎的理解。--- 記一次組內同學的fork問題排查

来源:https://www.cnblogs.com/zhaoyixing/archive/2019/05/10/10847300.html
-Advertisement-
Play Games

昨天組內同學在使用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版本


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一 對象的記憶體佈局: 在HotSpot虛擬機中,對象在記憶體中存儲的佈局可以分為3塊區域:對象頭(Header),實例數據(Instance Data)和對齊填充(Padding)。 HotSpot的對象頭包括兩部分信息,一部分存儲對象運轉時自身信息,例如hashCode,GC分代年齡,鎖狀態標誌,線 ...
  • info.m ff.m 測試代碼 ...
  • 緣起 本項目是基於之前學習的一個Dubbo+SSM分散式項目進行升級,基於此項目對前後端分離項目、微服務項目進一步深入學習。之前學習了vue、springBoot、springCloud後,沒有進行更多實戰練習,藉助此機會,整合之前所學知識,搭建一套微服務電商系統。本項目純屬個人學習總結,如有錯誤之 ...
  • 在c語言中實現全排列,對於剛接觸c語言,還沒學習演算法的人來說,比較困難了吧。估計大佬也不會看這種基礎的東西,全排列實現的辦法很多,在c++中有一個專門的函數可以使用,但是在c中實現就有點困難了。如果你想出用一個迴圈使一個數字每一位都不相同,那麼你就走進了死衚衕,這種辦法運算量巨大,往往到了高位就會超 ...
  • 轉自:PHP在無限分類時註意的一些問題(http://lxiaoke.cn) (註意:代碼使用的是原生PHP,旨在提供解決思路)1 無限分類的查找(獲取所有節點) 代碼: /** * 無限分類查詢,預設 pid 為 0 * @param $pid * @return array $res */ pr ...
  • JAVA流程語句有幾下幾種: 一、if語句: 1.if語句:如果滿足條件語句,則執行執行語句; if(條件語句){ 執行語句; ....; } 2.if....else語句:如果滿足判斷語句,則執行執行語句1,否則執行執行語句2; if(判斷語句{ 執行語句1; .....; }else{ 執行語句 ...
  • 大家好,忙裡抽空更新一下自己的博客,算是自己的一個進步,C語言視頻啟蒙我早就看完啦,只是覺得這個視頻真不錯,所以給大家分享一下,同時自己還有很多沒有理解透徹,寫寫博客算是一個筆記更是對自己所學的知識的吸收,廢話不多直接開始今天的主題,"C語言的選擇結構" 關係運算符 小於:< 大於:> 等於:= = ...
  • 描述Object wait()/notify()跟Condition await()/signal()的基本用法,三連問:解釋為什麼wait() 要放在while裡面?為什麼wait()方法放在Object對象中?為什麼wait()必須在同步方法/代碼塊中調用?以及這兩種通知/等待機制的區別 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...