多进程信号处理相关:父进程的第一代子进程可以接收到SIGTERM信号,而第二代以后的子进程则无法接收到SIGTERM信号

总结

问题已经得到解决(再次非常感谢),总结原因是:二代子进程(及三代以后)如果在信号处理内启动的话就接收不到SIGTERM信号了。整理最后答主的思想,最后解决问题的代码示例如:

$pid = pcntl_fork();
if ($pid < 0) {
    exit('fork失败');
} elseif($pid > 0) {
     exit('父进程退出');
}

// 升级为守护进程
 if (!posix_setsid()) {
    exit('setsid error');
}

$childs = [];   //管理子进程
for($i=1; $i<3; $i++) {
    fork();
}

function mylog($msg) {
    $file = __DIR__ . '/test.log';
    if (!is_file($file)) {
        @touch($file);
    }
    @error_log($msg . PHP_EOL, 3, $file);
}

function worker($i) {
    while(1) {
        // 可能是一个循环体任务,如AMQP的消费者
    }
}

function fork() {
    global $childs;
    $pid = pcntl_fork();
    if ($pid < 0) {
        exit('fork失败');
    } elseif($pid > 0) {
        $childs[] = $pid;
    } else {
        mylog('pid :'. posix_getpid());
        worker(1);
    }
}

// 子进程reload都放在这,而不是从信号处理内执行
while(true) {
    pcntl_signal_dispatch();
    foreach($childs as $k=>$pid) {
        $result = pcntl_waitpid($pid, $status, WNOHANG);
        if ($result == $pid || $result == -1) {
            unset($childs[$k]);
            fork();
        }
    }
    if (empty($childs)) break;
    sleep(1);
}

—————————分割线 ————————–

问题描述

父进程的第二代子进程执行sudo kill -15 第二代子进程时,父进程无法接收到SIGTERM信号。

期望

希望得到大家的指点!

描述

初衷:模拟Supervisor实现子进程的reload和stop功能

父进程接收子进程退出的信号,用SIGCHLP捕捉并处理reload或者stop。如下两个子进程的信号将会被父进程接收到:

  • SIGTERM(15):实现reload功能
  • SIGSTOP(9):实现stop功能

实现过程:父进程启动之后称为守护进程,并产生第一代子进程,然后子进程通过 sudo kill -15 第一代子进程 之后父进程成功捕获到SIGTERM信号并且在信号处理业务中重启一个子进程(第二代子进程),然后在执行 sudo kill -15 第二代子进程,父进程就无法接收到SIGTERM信号了。

如下是我自己测试这个问题的日志记录:

多进程信号处理相关:父进程的第一代子进程可以接收到SIGTERM信号,而第二代以后的子进程则无法接收到SIGTERM信号
接下来执行sudo kill -15 32946,并且得到截图如:

多进程信号处理相关:父进程的第一代子进程可以接收到SIGTERM信号,而第二代以后的子进程则无法接收到SIGTERM信号
然后在shell窗口中执行sudo kill -15 33217,如截图:

多进程信号处理相关:父进程的第一代子进程可以接收到SIGTERM信号,而第二代以后的子进程则无法接收到SIGTERM信号
可见第二次执行的时候子进程ID为33217的并没退出也没重启。

DEMO

@JaguarJack
demo写的比较仓促,好歹能够复现,再次非常感谢!

步骤

(1)执行 php SignoDemo.php
(2)另起一个窗口:ps -ef | grep Signo,可以看到2个第一代子进程
(3)sudo kill -15 pid(第一代子进程PID),发现重启了一个子进程(二代子进程)
(4)sudo kill -15 pid(第二代子进程PID),发现这个二代子进程不在退出。
(5)可以观看执行demo的目录下有没有生成一个test.log,可以监听到主进程捕获到的信号信息。
(6)最后别忘了执行:ps -ef | grep Signo | grep -v grep | awk '{print "sudo kill -9 " $2}' |sh

代码

<?php


$pid = pcntl_fork();
if ($pid < 0) {
    exit('fork失败');
} elseif($pid > 0) {
    exit('父进程退出');
}

// 升级为守护进程
if (!posix_setsid()) {
    exit('setsid error');
}

@cli_set_process_title('Master Process');

$childs = [];   //管理子进程
$end = false;   //控制父进程退出

// 父进程捕获子进程退出状态
pcntl_signal(SIGCHLD, function($signo, $siginfo) {
    global $childs;
    global $end;
    $sigStatus = @$siginfo['status'];
    $sigPid = @$siginfo['pid'];
    // 这里第一代子进程可以接收到信号,第二代就接收不到了!
    mylog("status:{$sigStatus}, pid:{$sigPid}");
    foreach($childs as $k=>$pid) {
        $result = pcntl_waitpid($pid, $status, WNOHANG);
        if ($result == $pid || $result == -1) {
            // 回收子进程
            unset($childs[$k]);
            if ($sigStatus == SIGKILL)  continue;
             // 这里是我实现reload的地方
            if ($sigStatus == SIGTERM) {
                forkCallback(function() {
                    worker('reload');
                });
            }
        }
    }
    if (empty($childs)) $end = true;
});

for($i=1; $i<3; $i++) {
    forkCallback(function() use ($i) {
        worker($i);
    });
}

function mylog($msg) {
    $file = __DIR__ . '/test.log';
    if (!is_file($file)) {
        @touch($file);
    }
    @error_log($msg . PHP_EOL, 3, $file);
}

function worker($i) {
    while(1) {
        echo "worker: {$i} \n";
        sleep(2);
    }
}


function forkCallback($callback) {
    global $childs;
    $pid = pcntl_fork();
    if ($pid < 0) {
        exit('fork失败');
    } elseif($pid > 0) {
        $childs[] = $pid;
    } else {
        @cli_set_process_title('work process');
        $callback();
        exit;
    }
}

while(!$end) {
    pcntl_signal_dispatch();
    sleep(1);
}
JaguarJack
最佳答案

调试后发现可能是这里出现了

pcntl_signal(SIGCHLD, function($signo, $siginfo) {
    global $childs;
    global $end;
    $sigStatus = @$siginfo['status'];
    $sigPid = @$siginfo['pid'];
    // 这里第一代子进程可以接收到信号,第二代就接收不到了!
    mylog("status:{$sigStatus}, pid:{$sigPid}");
    foreach($childs as $k=>$pid) {
        $result = pcntl_waitpid($pid, $status, WNOHANG);
        if ($result == $pid || $result == -1) {
            // 回收子进程
            unset($childs[$k]);
            if ($sigStatus == SIGKILL)  continue;
             // 这里是我实现reload的地方
            if ($sigStatus == SIGTERM) {
                forkCallback(function() {
                    worker('reload');
                });
            }
        }
    }
    if (empty($childs)) $end = true;
});

二次发送 SIGTERM 信号似乎被忽略,具体什么情况我没有去寻找。需要 trace 追踪下。 这段代码信号监听被删除掉后,改成

while(!$end) {
    pcntl_signal_dispatch();
    foreach($childs as $k=>$pid) {
        $result = pcntl_waitpid($pid, $status, WNOHANG);
        mylog("pid:{$pid}, result:{$result}");
        if ($result == $pid || $result == -1) {
            // 回收子进程
            unset($childs[$k]);
            // 这里是我实现reload的地方
            forkCallback(function() {
                worker('reload');
            });
        }
    }
    pcntl_signal_dispatch();
    sleep(1);
}

应该就可以了,全部代码私信给你了。你这种写法导致了问题出现,具体不详

3年前 评论
so_easy (楼主) 3年前
讨论数量: 2
JaguarJack

你能重启并不是父进程接受到SIGTERM信号,而是父亲进程通过pcntl_waitpid捕获到了子进程退出,重新启动了。但是为什么第二代进程没有退出,这就很奇怪了。难道是没有监听到 SIGTERM 信号吗

3年前 评论
so_easy (楼主) 3年前
JaguarJack (作者) 3年前
so_easy (楼主) 3年前
JaguarJack (作者) 3年前
so_easy (楼主) 3年前
JaguarJack (作者) 3年前
so_easy (楼主) 3年前
JaguarJack

调试后发现可能是这里出现了

pcntl_signal(SIGCHLD, function($signo, $siginfo) {
    global $childs;
    global $end;
    $sigStatus = @$siginfo['status'];
    $sigPid = @$siginfo['pid'];
    // 这里第一代子进程可以接收到信号,第二代就接收不到了!
    mylog("status:{$sigStatus}, pid:{$sigPid}");
    foreach($childs as $k=>$pid) {
        $result = pcntl_waitpid($pid, $status, WNOHANG);
        if ($result == $pid || $result == -1) {
            // 回收子进程
            unset($childs[$k]);
            if ($sigStatus == SIGKILL)  continue;
             // 这里是我实现reload的地方
            if ($sigStatus == SIGTERM) {
                forkCallback(function() {
                    worker('reload');
                });
            }
        }
    }
    if (empty($childs)) $end = true;
});

二次发送 SIGTERM 信号似乎被忽略,具体什么情况我没有去寻找。需要 trace 追踪下。 这段代码信号监听被删除掉后,改成

while(!$end) {
    pcntl_signal_dispatch();
    foreach($childs as $k=>$pid) {
        $result = pcntl_waitpid($pid, $status, WNOHANG);
        mylog("pid:{$pid}, result:{$result}");
        if ($result == $pid || $result == -1) {
            // 回收子进程
            unset($childs[$k]);
            // 这里是我实现reload的地方
            forkCallback(function() {
                worker('reload');
            });
        }
    }
    pcntl_signal_dispatch();
    sleep(1);
}

应该就可以了,全部代码私信给你了。你这种写法导致了问题出现,具体不详

3年前 评论
so_easy (楼主) 3年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!