PHP子进程使用以及常见问题

/ 0评 / 0

一、子进程使用

1)基本使用

$pid = pcntl_fork();
if ($pid == -1) {
    die('could not fork');
} else if ($pid) {
    // we are the parent
    pcntl_wait($status); //Protect against Zombie children
} else {
    // we are the child
}

2)多子进程使用

//最大进程数
define("MAXPROCESS", 50);
//计数器
$execute++;

for($i = 0; $i < 50 ; $i++){
    $pid = pcntl_fork();
    if ($pid == -1) {
        //处理fork失败的情况,一般不需要处理
    }elseif($pid){
        if ($execute>=MAXPROCESS){
            pcntl_wait($status);
            $execute--;
        }
    }else{
        //子进程逻辑
    }
}

这是子进程创建的基本方式,如果只需要怎么创建子进程上述已经满足你的需求。

然而上面代码有些问题。。。

二、僵尸进程 & 孤儿进程

pcntl_fork直接调用linux系统接口函数fork。所以讨论php的子进程可以直接参照系统的fork函数。更多关于fork的描述参照维基百科 https://zh.wikipedia.org/wiki/Fork_(%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8) ,php实现代码如下:

/* {{{ proto int pcntl_fork(void)
       Forks the currently running process following the same behavior as the UNIX fork() system call*/
    PHP_FUNCTION(pcntl_fork)
    {
    pid_t id;

    id = fork();
    if (id == -1) {
    PCNTL_G(last_error) = errno;
    php_error_docref(NULL, E_WARNING, "Error %d", errno);
    }

    RETURN_LONG((zend_long) id);
    }

子进程与主进程随机交互执行,默认情况(下文解释一般处理方案)下两个进程会先后执行完, 就会产生下面两种情况:

主进程先与子进程执行完:子进程会托孤给init(pid = 0),就是上文提到的进程必须存在父进程。

子进程先于主进程执行完:主进程需要捕获子进程的退出状态,否则子进程就会成为僵尸进程。(zombie),linux 进程状态为Z、z。

** 孤儿进程:** 
Linux 进程管理有一个基本规则,除了PID为1的init进程外,所有的进程必须存在父进程,也就是说所有的进程都是被其他进程fork出来的。孤儿进程只是描述进程的一种状态,与僵尸进程不同,它没有任何问题。

僵尸进程:
释放所有资源,不可被调度,僵尸进程会存在进程列表中。详细描述参见  LINUX 的僵尸(ZOMBIE)进程https://coolshell.cn/articles/656.html。

僵尸进程的防止有多重方案,最常用的就是就是pcntl_wait。关于此函数PHP手册描述如下:

wait函数刮起当前进程的执行直到一个子进程退出或接收到一个信号要求中断当前进程或调用一个信号处理函数。 如果一个子进程在调用此函数时已经退出(俗称僵尸进程),此函数立刻返回。子进程使用的所有系统资源将 被释放。关于wait在您系统上工作的详细规范请查看您系统的wait(2)手册。

文章开头第一种fork方式就是采用wait防止僵尸状态出现,但是这种方案会产生新的问题:

阻塞主进程:在子进程发出结束信号之前主进程没办法再次执行其他任务。第二方式极限情况下子进程进入后直接退出或者执行时间较短也会成为zombie。为了解决这个问题可以在主进程中安装信号量signal(SIGCHLD, SIG_IGN),通知内核忽略子进程状态。

三、子进程可以完整使用主进程资源?

答案是否定的,下面两段代码可以给出验证过程。

#### 1、mysqlclient 子进程不可使用。

$mysqliClient = new mysqli("localhost","root","123456","assets");
$result = $mysqliClient->query("select 1");
echo sprintf("fork前主进程访问数据库: %s\n\n",

print_r($mysqliClient->error,true));

sleep(5);

$pid = pcntl_fork();
if ($pid == -1) {
    die("fork error");
} elseif ($pid) {
    echo "已经fork子进程 " . $pid . "\n";
    $result = $mysqliClient->query("select 1");
    echo sprintf("fork后 主进程访问数据库, ErrNo %d , Err: %s\n\n",
    $mysqliClient->errno, $mysqliClient->error);
    sleep(10);
} else {
    sleep(20);
    $result = $mysqliClient->query("select 1");
    echo sprintf("fork后 子进程访问数据库, ErrNo %d , Err: %s\n\n", 
    $mysqliClient->errno, $mysqliClient->error);
}

2、tcp客户端子进程可用

$msg = "i am parent";
$len = strlen($msg);

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
$handler = socket_connect($socket, '127.0.0.1', 22);

echo sprintf("fork前, 主进程 socket 连接状态 %s \n",  
socket_send($socket, $msg, $len, 1));


$pid = pcntl_fork();
if ($pid == -1) {
    die("fork error");
} elseif($pid){
    sleep(5);
    echo sprintf("主进程 socket 发送字节 %d, 当前错误 %s \n",  
    socket_send($socket, $msg, $len, 1), socket_last_error($socket));
    sleep(100);
}else{
    sleep(10);
    echo sprintf("子进程 socket 发送字节 %d, 当前错误 %s \n",  
    socket_send($socket, $msg, $len, 1), socket_last_error($socket));
    sleep(100);
}

发表评论

电子邮件地址不会被公开。 必填项已用*标注