欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

使用php的fork进行父子进程代码编写,你至少需要对linux fork有这几点基础的理解。--- 记一次组内同学的fork问题排查

程序员文章站 2022-12-23 17:13:38
昨天组内同学在使用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:      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);
        }
    }
}

 

解释一下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,这也是文章开头提到的问题所在。

使用php的fork进行父子进程代码编写,你至少需要对linux fork有这几点基础的理解。---   记一次组内同学的fork问题排查

 

 

那么再回头看开始提到的问题,当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版本

使用php的fork进行父子进程代码编写,你至少需要对linux fork有这几点基础的理解。---   记一次组内同学的fork问题排查