PHP孤儿进程、僵尸进程的代码演示和方法处理

2022-03-01 19:38:56 955 技术小虫有点萌

基本概念

我们知道,在Unix和linux中,子进程是由父进程创建的,因为父进程不知道子进程什么时候结束,所以子进程的状态对于父进程来说是异步的。那么父进程如何知道子进程的状态呢?就需要调用wait() 或者waitpid()系统调用获取子进程的状态

pcntl_fork()

  • (PHP 4 >= 4.1.0, PHP 5, PHP 7) pcntl_fork — 在当前进程当前位置产生分支(子进程)。译注:fork是创建了一个子进程,父进程和子进程 都从fork的位置开始向下继续执行,不同的是父进程执行过程中,得到的fork返回值为子进程 号,而子进程得到的是0。 pcntl_fork()函数创建一个子进程,这个子进程仅PID(进程号) 和PPID(父进程号)与其父进程不同。fork怎样在您的系统工作的详细信息请查阅您的系统 的fork(2)手册。
  • 成功时,在父进程执行线程内返回产生的子进程的PID,在子进程执行线程内返回0。失败时,在 父进程上下文返回-1,不会创建子进程,并且会引发一个PHP错误。

init进程

  • Linux下有3个特殊的进程,idle进程(PID=0), init进程(PID=1)和kthreadd(PID=2)

  • idle进程由系统自动创建, 运行在内核态 idle进程其pid=0,其前身是系统创建的第一个进程,也是唯一一个没有通过fork或者kernel_thread产生的进程。完成加载系统后,演变为进程调度、交换

  • init进程由idle通过kernel_thread创建,在内核空间完成初始化后, 加载init程序,并存在于用户空间 由0进程创建,完成系统的初始化. 是系统中所有其它用户进程的祖先进程,Linux中的所有进程都是有init进程创建并运行的。首先Linux内核启动,然后在用户空间中启动init进程,再启动其他系统进程。在系统启动完成完成后,init将变为守护进程监视系统其他进程。也称为一号进程,它是内核启动的第一个用户级进程。 init有许多很重要的任务,比如像启动getty(用于用户登录)、实现运行级别、以及处理孤儿进程。

  • kthreadd进程由idle通过kernel_thread创建,并始终运行在内核空间, 负责所有内核线程的调度和管理 它的任务就是管理和调度其他内核线程kernel_thread, 会循环执行一个kthread的函数,该函数的作用就是运行kthread_create_list全局链表中维护的kthread, 当我们调用kernel_thread创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以kthreadd为父进程

孤儿进程

孤儿进程,简单来说,就是父进程退出了,子进程还在运行,这些子进程被称为孤儿进程,孤儿进程将被init进程(就是我们前面说的1号进程 )所收养,并完成孤儿进程的回收工作。

  • 我们来演示一下,在这个栗子中,父进程
<?php
$pid = pcntl_fork();

if ($pid > 0) {
 //前面我们说过,在父进程执行线程内,返回产生的子进程的PID 是大于0的
 // getmypid()获取当前 PHP 进程 ID
 //posix_getppid — 返回父进程ID
    echo "我是主进程,我的ppid是". posix_getppid() .",我的pid是".getmypid()."\n";
// 让父进程停止4秒钟,在这4秒内,子进程的父进程ID还是这个父进程
    sleep(4);

} else if (0 == $pid) {//在子进程执行线程内返回0
// 让子进程循环10次,每次睡眠1s,然后每秒钟获取一次子进程的父进程进程ID
    for ($i = 1; $i <= 10; $i++) {
        sleep(2);
// posix_getppid()函数的作用就是获取当前进程的父进程进程ID
        echo "我是子进程,我的ppid是". posix_getppid() .",我的pid是 ".getmypid()."\n";

    }

} else {//-1 不会创建子进程

    echo "创建进程失败了" . PHP_EOL;

}


  • 我们看到,在前4秒,父进程还在,子进程的父进程号是1148 ,父进程结束之后,子进程就变成了孤儿进程,而且父进程变成了init进程,由init进程负责孤儿进程的回收工作

僵尸进程

一个进程使用fork创建了一个子进程,但是父进程还没有来得及维护,即父进程还没有调用wait或者waitpid 获取子进程状态信息,子进程退出了。虽然子进程退出了,但是并没有马上小时,子进程的进程描述符还保留在系统中。所以这会造成一个问题,就是 进程没有完全消失,进程号还在,但是系统的进程号是有限的,这就是占着茅坑不拉屎啊,系统想创建新的进程,对不起,没有进程号了。

就像一个餐厅,只有10饭盒,可以供10人吃饭,但是有5个人吃完饭 并没有送回饭盒就离开了。导致餐厅只有5个饭盒,只能供5个人吃饭。

  • 我们来看一下代码,主进程的声明周期是60s ,子进程的声明周期是20s。
<?php
$pid=pcntl_fork();//开启子进程
if($pid>0){
    cli_set_process_title('php father process');//修改PHP进程的名字
    sleep(60);
}else if (0 == $pid) {
// 子进程休息10s结束,父进程对子进程不做任何处理
    cli_set_process_title('php child process');
    sleep(20);
} else {//-1 不会创建子进程

    echo "创建进程失败了" . PHP_EOL;

}

  • 子进程结束后。主进程未对子进程做任何处理操作,导致在前20s的时候,主进程和子进程还是 正常的,php child process 的状态列为 [S+]。20s之后,主进程还在,子进程结束后进程号还在,但是没有进程对其做回收操作,php child process 的状态列为 [Z+],<defunct>从而变为僵尸进程。
  • 查看进程的命令 ps -aux|grep -v "grep\|php-fpm" | grep php

怎么避免僵尸进程

在PHP里面, pcntl_wait() 和 pcntl_waitpid() 两个函数来帮我们解决僵尸进程的问题,其实也能看出来,这两个函数是对 wait() 和 waitpid()的封装

pcntl_wait()

pcntl_wait() 等待或返回 fork 的子进程状态,当主进程使用了这个函数,那么进程就会阻塞挂起等待子进程的状态一直到子进程的退出或者终止。我们说了阻塞等待,说明只要子进程还在,父进程就会一直等下去。 wait函数挂起当前进程的执行直到一个子进程退出或接收到一个信号要求中断当前进程或调用一个信号处理函数。如果一个子进程在调用此函数时已经退出(俗称僵尸进程),此函数立刻返回。子进程使用的所有系统资源将被释放。 pcntl_wait() 将会存储状态信息到 status 参数上,这个通过 status 参数返回的状态信息可以用以下函数 pcntl_wifexited(), pcntl_wifstopped(), pcntl_wifsignaled(), pcntl_wexitstatus(), pcntl_wtermsig() 以及 pcntl_wstopsig() 获取其具体的值。 pcntl_wait() 返回退出的子进程进程号,发生错误时返回 -1,如果提供了 WNOHANG 作为 option(wait3可用的系统)并且没有可用子进程时返回 0。

  • 代码演示
<?php
$pid=pcntl_fork();//开启子进程
if($pid>0){
    echo "我是主进程,我的ppid是". posix_getppid() .",我的pid是".getmypid()."\n";
    cli_set_process_title('php father process');//修改PHP进程的名字
    $wait_result = pcntl_wait($status);//等待子进程的状态

    print_r($wait_result."\n");

    print_r($status."\n");

    sleep(60);
}else if (0 == $pid) {
// 子进程休息10s结束,父进程对子进程不做任何处理
    echo "我是子进程,我的ppid是". posix_getppid() .",我的pid是 ".getmypid()."\n";
    cli_set_process_title('php child process');
    sleep(10);
} else {//-1 不会创建子进程

    echo "创建进程失败了" . PHP_EOL;

}

  • 我们看到pid 1246并没有称为僵尸进程,而是被回收了

pcntl_waitpid

pcntl_wait() 有个很大的问题,就是阻塞。父进程必须等待子进程结束或者终止,在此期间父进程说明都不能做,那么有没有一种机制,可以让父进程不用阻塞等待也可以完成对子进程的回收呢?pcntl_waitpid() 闪亮登场

  • pcntl_waitpid 等待或返回fork的子进程状态 ,挂起当前进程的执行直到参数pid指定的进程号的进程退出, 或接收到一个信号要求中断当前进程或调用一个信号处理函数。如果pid指定的子进程在此函数调用时已经退出(俗称僵尸进程),此函数 将立刻返回。。。。。。等等等等,感觉和pcntl_wait差不多,但是多了一句话或接收到一个信号要求中断当前进程或调用一个信号处理函数。 也就是说,你可以一直等子进程结束,也可以当收到信号的时候再处理
  • 我们来看一下代码
<?php
$pid=pcntl_fork();//开启子进程
if($pid>0){
    echo "我是主进程,我的ppid是". posix_getppid() .",我的pid是".getmypid()."\n";
    cli_set_process_title('php father process');//修改PHP进程的名字
    $wait_result = pcntl_waitpid($pid, $status,WNOHANG);//WNOHANG标识会让该函数非阻塞

    print_r($wait_result."\n");

    print_r($status."\n");
    echo "我是在pcntl_waitpid后面哦\n";

    sleep(60);
}else if (0 == $pid) {
// 子进程休息10s结束,父进程对子进程不做任何处理
    echo "我是子进程,我的ppid是". posix_getppid() .",我的pid是 ".getmypid()."\n";
    cli_set_process_title('php child process');
    sleep(10);
} else {//-1 不会创建子进程

    echo "创建进程失败了" . PHP_EOL;

}


  • 我们来看一下结果 status wait_result 包括后面一个echo输出,很快就打印出来了,没有一丝的等待(前面同步的话会有10s的延迟),但是同时也发现一个问题wait_result的值并不是pid,而且子进程还是变成了僵尸进程,因为这个函数还没等子进程执行完毕,它倒执行完毕了,让你去火车站接人,你一看人没来就回去了,工作没做。
  • 如果想要实现回收子进程,必须得等到子进程完毕之后再去回收,就是在这个函数前面sleep一下
<?php
$pid=pcntl_fork();//开启子进程
if($pid>0){
    echo "我是主进程,我的ppid是". posix_getppid() .",我的pid是".getmypid()."\n";
    cli_set_process_title('php father process');//修改PHP进程的名字
    sleep(15);
    $wait_result = pcntl_waitpid($pid, $status,WNOHANG);

    print_r($wait_result."\n");

    print_r($status."\n");
    echo "我是在pcntl_waitpid后面哦\n";

    sleep(60);
}else if (0 == $pid) {
// 子进程休息10s结束,父进程对子进程不做任何处理
    echo "我是子进程,我的ppid是". posix_getppid() .",我的pid是 ".getmypid()."\n";
    cli_set_process_title('php child process');
    sleep(10);
} else {//-1 不会创建子进程

    echo "创建进程失败了" . PHP_EOL;

}



SIGCHLD

我们也可以使用SIGCHLD来完成对僵尸进程的回收,可以用signal函数为SIGCHLD安装handler,因为子进程结束后,父进程会收到该信号,可以在handler中调用pcntl_wait或pcntl_waitpid来回收。

  • 代码演示
<?php
pcntl_async_signals(true);

pcntl_signal(SIGCHLD, function () {
    echo "SIGCHLD 到我执行啦!" . PHP_EOL;
    pcntl_wait($status);  //父进程阻塞方式等待子进程的退出
});
$pid=pcntl_fork();//开启子进程
if($pid>0){
    echo "我是主进程,我的ppid是". posix_getppid() .",我的pid是".getmypid()."\n";
    cli_set_process_title('php father process');//修改PHP进程的名字
    echo "我是在后面哦\n";
    sleep(60);
}else if (0 == $pid) {
// 子进程休息10s结束,父进程对子进程不做任何处理
    echo "我是子进程,我的ppid是". posix_getppid() .",我的pid是 ".getmypid()."\n";
    cli_set_process_title('php child process');
    sleep(10);
} else {//-1 不会创建子进程

    echo "创建进程失败了" . PHP_EOL;

}

  • 这样的话 在(pid的代码段里面都不会出现阻塞,但是代码运行完毕,必须等待子进程运行完毕,触发pcntl_signal,主进程才会退出

内核回收

如果父进程不关心子进程什么时候结束,那么可以用pcntl_signal(SIGCHLD, SIG_IGN)通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收,并不再给父进程发送信号。

当子进程结束后,SIGCHLD信号并不会发送给父进程,而是通知内核对子进程进行了回收。

<?php
declare(ticks = 1);

pcntl_signal(SIGCHLD, SIG_IGN);
$pid=pcntl_fork();//开启子进程
if($pid>0){
    echo "我是主进程,我的ppid是". posix_getppid() .",我的pid是".getmypid()."\n";
    cli_set_process_title('php father process');//修改PHP进程的名字
    echo "我是在后面哦\n";
    sleep(60);
}else if (0 == $pid) {
// 子进程休息10s结束,父进程对子进程不做任何处理
    echo "我是子进程,我的ppid是". posix_getppid() .",我的pid是 ".getmypid()."\n";
    cli_set_process_title('php child process');
    sleep(10);
} else {//-1 不会创建子进程

    echo "创建进程失败了" . PHP_EOL;

}


pcntl_fork两次

通过pcntl_fork两次,也就是父进程fork出子进程,然后子进程中再fork出孙进程,这时子进程退出。那么init进程会接管孙进程,孙进程退出后,init会回收。不过子进程还是需要父进程进行回收。我们把业务逻辑放到孙进程中执行,父进程就不需要pcntl_wait或pcntl_waitpid来等待孙进程(即业务进程)。

<?php

$pid = pcntl_fork();//开启子进程
if ($pid > 0) {
    echo "我是主进程,我的ppid是" . posix_getppid() . ",我的pid是" . getmypid() . "\n";
    cli_set_process_title('php father process');//修改PHP进程的名字
    pcntl_wait($status);
    echo "我是在后面哦\n";
    sleep(60);
} else if (0 == $pid) {
    echo "我是子进程,我的ppid是" . posix_getppid() . ",我的pid是" . getmypid() . "\n";
    cli_set_process_title('php son process');//修改PHP进程的名字

    $cpid = pcntl_fork();
    if ($cpid == -1) {
        die("fork error");
    } else if ($cpid) {
        //这里是子进程,直接退出
        exit;
    } else {
        //这里是孙进程,处理业务逻辑
        cli_set_process_title('php grandson process');//修改PHP进程的名字

        for ($i = 0; $i < 5; ++$i) {
            echo "我是孙子进程,我的ppid是" . posix_getppid() . ",我的pid是" . getmypid() . "\n";
            sleep(1);
        }
    }
} else {//-1 不会创建子进程

    echo "创建进程失败了" . PHP_EOL;

}