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

基本概念

我们知道,在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>0) 的代码段里面和0 ==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;

}


在这里插入图片描述

在这里插入图片描述
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容