多进程编程 - 孤儿进程/僵尸进程/信号量通信-程序员宅基地

技术标签: 运维  php  c/c++  

多进程编程中经常会涉及到孤儿进程/僵尸进程的概念,下面将用代码实际的演示一遍。

孤儿进程

孤儿进程指的是父进程结束运行时,还未运行结束的子进程。这些子进程将会成为系统的孤儿进程,系统将会调用 pid = 1 的 init 进程来负责接管这些孤儿进程,监听其是否运行结束,以便回收资源。

所以,孤儿进程对系统没有太大的影响。在某些场景下我们可能还会可以的利用孤儿进程,比如编写守护进程,或一些不需要等待执行结果的多任务时。

僵尸进程

僵尸进程(zombie),可以方便的使用 top 命令来查看系统是否存在。僵尸进程产生的原因是父进程仍在运行,但没有对子进程运行结束时发送的 SIGCHLD 信号进行处理(常用的是使用 wait/waitpid 来配合处理) 或 手动捕获 SIGCHLD 信号却没做任何处理(作死)。因为父进程还在,所以也无法被系统的 init 进程接管,便成为了会影响系统性能的僵尸进程。

查看系统中是否存在僵尸进程

top

clipboard.png

ps -A -o stat,ppid,pid,cmd | grep -e '^[Zz]'

clipboard.png

3261 即为父进程的 pid,kill -9 {pid} 即可清除这些僵尸进程

如何避免僵尸进程

在了解了僵尸进程产生的原因和影响后,我们要尽可能的避免僵尸进程的产生。

其实一般你随便写个多进程大都不会产生长期存在的僵尸进程,如果不需要同步父子进程的执行状态,或父进程在短时间内会立即退出,子进程可能会出现短暂的僵尸进程的状态,但最终都会因为父进程退出转为孤儿进程被 init 回收。

但如果父进程在提供常驻服务时创建子进程,且不使用 wait/waitpid 或 对 SIGCHLD信号 做 SIG_IGN 处理,子进程就变成僵尸进程长期占用系统资源了。

SIG_DFL / SIG_IGN

// c/c++
signal(SIGCHLD, SIG_IGN)
// php
pcntl_signal(SIGCHLD, SIG_IGN)

显式的声明将 SIGCHLD 信号做 SIG_IGN 处理(简单的理解成强制孤儿化子进程就好),结束的子进程则会交由系统 init 回收,父进程无需关注子进程的退出,故可以提高服务性能。

这里还需要理解 SIG_IGN 是在运行时层面忽略信号,而并非捕获了不作处理,部分信号量 SIG_IGN 和 捕获但不处理 的效果相同,但对一些特别的信号量(SIGCHLD)二者是有很大区别的。

<?php
// 都可以屏蔽终端暂停信号 ctrl+z
pcntl_signal(SIGTSTP, SIG_IGN);// 忽略
pcntl_signal(SIGTSTP, function () {
    // 捕获但不做处理
});
// 都可以屏蔽终端结束信号 ctrl+c
pcntl_signal(SIGINT, SIG_IGN);// 忽略
pcntl_signal(SIGINT, function () {
    // 捕获但不做处理
});

// 二者结果完全不同
pcntl_signal(SIGCHLD, SIG_IGN);// 忽略 交由 init 处理 安全
pcntl_signal(SIGCHLD, function () {
    // 捕获但不做处理 就会导致子进程成为僵尸进程 非安全
});

父进程已退出,子进程成为孤儿进程最终被 init 回收。
父进程未退出,但不处理子进程退出时发送过来的的 SIGCHLD 信号,子进程成为僵尸进程。

我们只需要保证:在父进程退出前,父进程调用了 wait/waitpid 函数处理了可能来自子进程的 SIGCHLD 信号即可。

wait/waitpid 会阻塞父进程,等待/返回某子进程的退出状态及 pid。

我们可以用来避免僵尸进程的产生/同步父子进程的执行状态,在一些场景下我们可能正需要父进程等待所有的子进程执行完毕后去汇总某些数据。

<?php
/**
 * 安全的多进程处理
 */
if (!function_exists('pcntl_fork')) {
    trigger_error("need pcntl extension!", E_USER_ERROR);
}

$workers_num = 4;
$workers_pid = [];

for ($i = 0; $i < $workers_num; $i++) {
    $pid = pcntl_fork();

    if ($pid == -1) {
        trigger_error("child process create failed!", E_USER_ERROR);
    }

    if ($pid == 0) {
        // 子进程执行模块
        echo "I am child pid: " . getmypid() . PHP_EOL;
        sleep(rand(1, 3));
        exit(0);
    } else {
        // 父进程管理子进程的pid
        $workers_pid[] = $pid;
    }
}

// 父进程使用 wait/waitpid 等待/处理子进程的 SIGCHLD 信号
while (true) {
    // 若有未退出的子进程
    if (! empty($workers_pid)) {
        // pcntl_wait 会阻塞/等待子进程发送的信号量
        $worker_pid = pcntl_wait($status);
        if ($status == 0) { // 正常退出
            echo 'child ' . $worker_pid . ' safe exited!' . PHP_EOL;
        } else { // exit 状态码
            echo 'child ' . $worker_pid . ' wrong end with status: ' . $status . PHP_EOL;
        }

        // 删除子进程的 pid
        $key = array_search($worker_pid, $workers_pid);
        unset($workers_pid[$key]);
    } else {
        // 所有子进程都已执行完毕
        break;
    }
}

// 此时所有的子进程已执行完毕 不会有孤儿/僵尸进程产生
echo "main end" . PHP_EOL;

我们在创建多进程任务时应该极力避免僵尸进程的产生,最常用的即父进程使用 wait/waitpid 函数监听子进程的退出并将其回收,或者可以用上面的 SIG_IGN 处理 SIGCHLD 信号,根据自身业务需求选择正确处理方式即可。

以上的代码父进程因 wait/waitpid 会处于阻塞状态,等待某一个子进程执行完毕发送 SIGCHLD 信号后获取其 pid 以及 exit_code 后才能继续运行。有没有什么更好的方式呢?比如子进程可以发送一个通知给父进程,父进程定时检测是否有此通知,有的话就回收子进程后再去做别的工作,这就是下面要讲的进程通信 -- 信号量。

进程通信--信号量

进程通信的方式有:管道,信号量,消息队列,共享内存。

这里我们简单的使用信号量来进行子父进程间的通信,通信目的也很简单:子执行完毕时通知父将其快点回收,别丢那里不管不问成了僵尸进程。

如果不使用信号量通信

<?php

for ($i = 0; $i < 4; $i++) {
    $pid = pcntl_fork();
    
    if ($pid == -1) {
        trigger_error("child process create failed!" . PHP_EOL, E_USER_ERROR);
    }

    if ($pid == 0) {
        // -- child process code --
        echo "child: " . getmypid() . " running!" . PHP_EOL;
        sleep(rand(1, 3));
        // child process exit code 可以被父进程接受到以判别子进程的退出状态
        exit(0);
        // -- child process code --
    } else {
        // father process code
        $children_pid_set[] = $pid;
        echo "father: child " . $pid . " created!" . PHP_EOL;
    }
}

while (true) {
    // 这里会阻塞直到接收到子进程发送的 SIGCHLD 信号
    $child_pid = pcntl_wait($status);
    
    // 删除子进程的 pid
    $key = array_search($child_pid, $children_pid_set);
    unset($children_pid_set[$key]);
    
    if (empty($children_pid_set)) {
        echo "all children process run finished!" . PHP_EOL;
        break;
    }
}

echo "father process run finished!" . PHP_EOL;

即父进程会被 pcntl_await 阻塞而不能做其他的事情,如果我们用信号量通信就可以更为灵活。

<?php
/**
 * @author big_cat
 * @version 0.0.1
 * 非阻塞版的父进程创建管理子进程
 * 采用 SIGCHLD 通信方式 父进程使用 pcntl_signal_dispatch 定时检测有无子进程的信号量
 * 如有则调用相应的注册好的方法回收子进程
 * 如没有则继续父进程的业务
 */
if (!extension_loaded('pcntl')) {
    trigger_error("need pcntl extension!", E_USER_ERROR);
}

// 子进程数量
$children_num = 4;
// 存放子进程 pid
$children_pid_set = [];
// 是否退出执行
$sign_exit = false;

// 捕获子进程的退出信号 -- SIGCHLD 进行子进程回收
pcntl_signal(SIGCHLD, function ($signo) use (&$children_pid_set) {
    // 回收子进程 防止成为僵尸进程
    $child_pid = pcntl_wait($status);

    // 删除子进程的 pid
    $key = array_search($child_pid, $children_pid_set);
    unset($children_pid_set[$key]);

    if (0 == $status) {
        echo "child: " . $child_pid . " run finished!" . PHP_EOL;
    } else {
        echo "child: " . $child_pid . " run error and exit!" . PHP_EOL;
    }
});

// 做个软退出 -- SIGINT
pcntl_signal(SIGINT, function ($signo) use (&$sign_exit) {
    // 捕获 SIGINT ctrl+c 的退出执行命令后
    // 我们能可控的做一些退出清理工作
    $sign_exit = true;
});

/**
 * 创建一定数量的子进程
 * @param  [type] $process_num  创建的数量
 * @param  [type] &$children_pid_set 全局的子进程pid
 * @return [type]                [description]
 */
function process_pool($process_num, &$children_pid_set)
{
    // 创建子进程
    for ($i = 0; $i < $process_num; $i++) {
        $pid = pcntl_fork();

        if ($pid == -1) {
            trigger_error("child process create failed!" . PHP_EOL, E_USER_ERROR);
        }

        if ($pid == 0) {
            // -- child process code --
            echo "child: " . getmypid() . " running!" . PHP_EOL;
            sleep(rand(1, 3));
            // child process exit code 可以被父进程接受到以判别子进程的退出状态
            exit(0);
            // -- child process code --
        } else {
            // father process code
            $children_pid_set[] = $pid;
            echo "father: child " . $pid . " created!" . PHP_EOL;
        }
    }
}

// 预先创建若干个子进程
process_pool($children_num, $children_pid_set);

// 父进程使用 wait 函数等待所有子进程执行完毕
while (true) {
    // echo "father process running..." . PHP_EOL;

    // 始终维持 $children_num 个子进程
    if (($need_create = $children_num - count($children_pid_set)) > 0) {
        process_pool($need_create, $children_pid_set);
    }

    // PHP 信号捕获回调需要特定的使用此函数进行分发处理
    // declare(ticks=1) 存在浪费性能的可能
    // 故在主循环体中加入信号时间分发器
    pcntl_signal_dispatch();

    // 通过捕获 SIGINT 信号来实现软退出
    if ($sign_exit) {
        break;
    }

    // 模拟父进程耗时处理其他业务
    sleep(2000);
}

if (!empty($children_pid_set)) {
    // 可能会有一些还未结束的子进程 但无需担心 父进程退出后他们会成为孤儿进程被 init 接管
    // 你也可以自行对这些子进程做处理
    echo implode(" ", $children_pid_set) . ' are still running! will be ctrled by init process' . PHP_EOL;
}

echo "father process run finished!" . PHP_EOL;

源码解读:
1、父进程注册信号量 SIGCHLD 的 handler 方法,我们应在此信号量的 handler方法中做 pcntl_wait() 用来处理回收发送此信息号的子进程。
2、父进程创建子进程,并进入非阻塞循环,使用 pcntl_signal_dispatch() 来检测是否有信号待处理(使用 declare(ticks=1) 存在一些性能浪费的可能),若有待处理的信号,则父进程调用 pcntl_signal() 注册的信号及handler,若没有,则继续执行其他业务。
3、SIGCHLD 信号的 handler 中应使用 pcntl_wait() 方法来回收子进程,防止僵尸进程。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_33872566/article/details/88535646

智能推荐

Linux tcp 慢启动重启_slow start after idle-程序员宅基地

文章浏览阅读725次。背景在Linux内核实现了,tcp发送端在连接建立开始时,会通过慢启动流程来避免一次性发送太多的数据到网络,引起网络拥塞丢包,如果tcp连接建立好了之后,发送端正常发送数据,并且已经完成慢启动流程了,这时候发送端没有数据发送,进入idle空闲期,然后过了一段时间才继续发送,因为当前的tcp拥塞状态已经不处于慢启动阶段了,可能拥塞窗口值是比较大的,但是现在的网络环境可能也已经发生变化了,如果按当前的拥塞窗口直接发送数据,有可能会造成拥塞丢包,因此内核提供了sysctl_tcp_slow_start_aft_slow start after idle

递归算法在 Android 开发项目中的运用实战_算法对于android应用开发-程序员宅基地

文章浏览阅读2.6k次。递归算法在 Android 项目中的运用实战_算法对于android应用开发

hdu 2091 空心三角形_把一个字符三角形掏空,就能节省材料成本,减轻重量,但关键是为了追求另一种视-程序员宅基地

文章浏览阅读228次。Problem Description把一个字符三角形掏空,就能节省材料成本,减轻重量,但关键是为了追求另一种视觉效果。在设计的过程中,需要给出各种花纹的材料和大小尺寸的三角形样板,通过电脑临时做出来,以便看看效果。 Input每行包含一个字符和一个整数n(0&lt;n&lt;41),不同的字符表示不同的花纹,整数n表示等腰三角形的高。显然其底边长为2n-1。如果遇到@字符,则表示所做出来的样板三..._把一个字符三角形掏空,就能节省材料成本,减轻重量,但关键是为了追求另一种视

Docker 简介_jiffylab-程序员宅基地

文章浏览阅读2.4k次。Docker能够运行任何应用的“PaaS”云Posted on 2013-09-17 by yankayDocker 简介Docker 是一个开源可以将任何应用包装在"LXC容器”中运行的工具。如果说VMware,KVM包装的虚拟机,Docker包装的是应用。是一个实至名归的PaaS。当应用被打包成Docker Image后,部署和运维就变得极其简单。可以使用统一的_jiffylab

linux目录权限影响文件,Linux下的文件、目录权限-程序员宅基地

文章浏览阅读408次。对于Linux的初学者来说,熟悉了Windows下的文件类型,接触到Linux的下的文件类型是有所区别的。如Windows的:而Linux下:你会发现Linux下前面的几列不同如drwxr-xr-x. 2 root root,这又代表的什么?下面我们来说说Linux的文件权限......一 、文件的属性1. 文件的权限:文件的权限主要针对三类对象进行定义:owner: 属主, ugroup: 属...

python数据科学-多变量数据分析-程序员宅基地

文章浏览阅读2.3k次。总第87篇01|写在前面:在前面我们研究了单列(变量)数据情况,现实中的案例大多都是多列(变量)的,即影响一件事情的因素有多个,我们除了要看单列数据以外还需要看看这不同列之间是否存在某些..._python多变量分析

随便推点

关于小程序尺寸问题px rpx rem vw_微信小程序样式写行内px-程序员宅基地

文章浏览阅读901次。pxpx:绝对单位,页面按精确像素展示。在PC端经常使用的单位,不用计算,直接使用,一般情况不用考虑设计图纸的来改变页面的大小。就直接采用px,方便快捷但是不能自适应。RPXrpx单位是微信小程序中css的尺寸单位,rpx可以根据屏幕宽度进行自适应。微信的自适应单位,同时微信规定:屏幕宽为750rpx。如在 iPhone6 上,屏幕宽度为375px,共有750个..._微信小程序样式写行内px

计算1+2+3+4+5+6...+100用python_python练习题,写一个方法 传进去列表和预期的value 求出所有变量得取值可能性(例如list为[1,2,3,4,5,6,12,19],...-程序员宅基地

文章浏览阅读1.4k次。题目:(来自光荣之路老师)a+b==valuea+b+c=valuea+b+c+d==valuea+b+c+d+...=valuea和b....取值范围都在0-value写一个方法 传进去列表和预期得value 求出所有变量得取值可能性一个有顺序得数字序列 从小到大 不限制个数 序列里面随机两个数相加为value得可能性例子[1,2,3,4,5,6,12,19] value为2019+1==2..._value=a+b-value

CAM350 - 导入光绘(GERBER)文件_cam350导入光绘文件there are no items to show in this vle-程序员宅基地

文章浏览阅读4k次。CAM350 版本信息打开 CAM350,选择菜单 File > Import > AutoImport…在 AutoImport Directory 中:选择 Drives 和 Directories,即光绘文件所在的位置设置英制(English)或公制(Metric)单位完成以上设置之后,点击 Finish 按钮,就可以导入光绘文件:双击可以单独显示某个层:..._cam350导入光绘文件there are no items to show in this vlew.

各种功能的选项卡切换_passport-btn-line-程序员宅基地

文章浏览阅读829次。效果如下图所示:HTML code:myFocus-tab 各种功能的选项卡切换* { margin:0; padding:0; border:0; list-style:none; }body { background:#fff; padding:20px; font:1em Verdana, Geneva, sans-serif; }.box { fl_passport-btn-line

lr_VuGen(关联)_关联通过左右边界查找;-程序员宅基地

文章浏览阅读215次。关联函数1.关联的含义 1)本质:查找 2)从哪里查找:从响应中查找 3)如何查找:通过左右边界查找 4)被查找内容是否动态变化:不是关键!! 关联(correlation):在脚本回放过程中,客户端发出请求,通过关联函数所定义的左右边界值(也就是关联规则),在服务器所响应的内容中查找,得到相应的值,已变量的形式替换录制时的静态值,从而向服务器发出正确的请求,这种动态获得服务器响..._关联通过左右边界查找;

leetcode--Binary Tree Right Side View_view -> settings -> account -> view account detail-程序员宅基地

文章浏览阅读866次。Given a binary tree, imagine yourself standing on the right side of it, return the values of the nodes you can see ordered from top to bottom.For example:Given the following binary tree, 1_view -> settings -> account -> view account details (top right) -> view lice

推荐文章

热门文章

相关标签