zoukankan      html  css  js  c++  java
  • 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")."
    " ;
    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")."
    " ;
    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
    ");
            }
        }
    
        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版本

    如果想和我我交流技术方向的心得及感受,可以扫码关注我的公众号,上面我绑定了微信号。
  • 相关阅读:
    java 下载图片并传输(java自带 BASE64工具进行图片和字符串转换)
    MySQL的日期格式
    eclipse下查看maven下载的源码中文乱码问题
    Linux----部署
    python----logging
    python----pymysql
    vmware15 激活秘钥
    vmware15 激活秘钥
    Ubuntu18.04安装
    msyql45讲 20--幻读是什么,幻读有什么问题?
  • 原文地址:https://www.cnblogs.com/zhaoyixing/p/10847300.html
Copyright © 2011-2022 走看看