wait3(waitpid别名)返回-1,并且errno设置为ECHILD时不应该

ant*_*rez 31 c linux system-calls zombie-process waitpid

上下文就是这个Redis问题.我们有一个wait3()等待AOF重写子项在磁盘上创建新AOF版本的调用.当孩子完成后,通知父母wait3()以便用新的AOF替换旧的AOF.

但是,在上述问题的上下文中,用户向我们通知了一个错误.我修改了一点Redis 3.0的实现,以便在wait3()返回-1 时清楚地记录,而不是因为这种意外情况而崩溃.所以这就是显而易见的事情:

  1. wait3() 当有待等待的孩子时,我们会打电话.
  2. 对SIGCHLD应设置为SIG_DFL,没有代码Redis的所有设置这个信号,所以它的默认行为.
  3. 当第一次AOF重写发生时,wait3()按预期成功运行.
  4. 从第二次AOF重写开始(创建第二个子项),wait3()开始返回-1.

AFAIK在我们调用的当前代码中wait3()是不可能的,因为当没有挂起的子节点时,因为当创建AOF子节点时,我们设置server.aof_child_pid为pid的值,并且我们仅在成功wait3()调用之后重置它.

所以wait3()应该没有理由失败-1 ECHILD,但它确实如此,所以可能僵尸孩子不是出于某种意想不到的原因而创建的.

假设1:在某些奇怪的条件下Linux可能会丢弃僵尸孩子,例如因为内存压力?看起来不合理,因为僵尸只附加了元数据但谁知道.

请注意,我们打电话wait3()给WNOHANG.并且鉴于默认情况下SIGCHLD设置为SIG_DFL,唯一应该导致失败并返回-1的条件,并且ECHLD应该没有可用于报告信息的僵尸.

假设2:可能发生的其他事情但是没有解释,如果它发生,是在第一个孩子死后,SIGCHLD处理程序被设置为SIG_IGN,导致wait3()返回-1和ECHLD.

假设3:有没有办法从外部移除僵尸儿童?也许这个用户有某种脚本可以在后台删除僵尸进程,以便信息不再可用wait3()?据我所知,如果父母不等待它(使用或处理信号)并且如果没有被忽略,则永远不可能移除僵尸,但也许有一些特定于Linux的方式.waitpidSIGCHLD

假设4:实际上有在Redis的代码一些bug,使我们成功wait3()的孩子第一次不正确复位状态,后来我们叫wait3()连连,但不再有僵尸,所以它返回-1.分析代码看起来不可能,但也许我错了.

另一件重要的事情:我们过去从未见过这一点.这显然只发生在这个特定的Linux系统中.

更新:Yossi Gottlieb提出SIGCHLDRedis进程中的另一个线程由于某种原因(通常不会发生,仅在此系统上)接收.我们已经SIGALRM在bio.c线程中屏蔽了,也许我们也可以尝试SIGCHLD从I/O线程屏蔽.

附录:Redis代码的选定部分

调用wait3()的地方:

/* Check if a background saving or AOF rewrite in progress terminated. */
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {
    int statloc;
    pid_t pid;

    if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
        int exitcode = WEXITSTATUS(statloc);
        int bysignal = 0;

        if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);

        if (pid == -1) {
            redisLog(LOG_WARNING,"wait3() returned an error: %s. "
                "rdb_child_pid = %d, aof_child_pid = %d",
                strerror(errno),
                (int) server.rdb_child_pid,
                (int) server.aof_child_pid);
        } else if (pid == server.rdb_child_pid) {
            backgroundSaveDoneHandler(exitcode,bysignal);
        } else if (pid == server.aof_child_pid) {
            backgroundRewriteDoneHandler(exitcode,bysignal);
        } else {
            redisLog(REDIS_WARNING,
                "Warning, detected child with unmatched pid: %ld",
                (long)pid);
        }
        updateDictResizePolicy();
    }
} else {
Run Code Online (Sandbox Code Playgroud)

选定部分backgroundRewriteDoneHandler:

void backgroundRewriteDoneHandler(int exitcode, int bysignal) {
    if (!bysignal && exitcode == 0) {
        int newfd, oldfd;
        char tmpfile[256];
        long long now = ustime();
        mstime_t latency;

        redisLog(REDIS_NOTICE,
            "Background AOF rewrite terminated with success");

        ... more code to handle the rewrite, never calls return ...

    } else if (!bysignal && exitcode != 0) {
        server.aof_lastbgrewrite_status = REDIS_ERR;

        redisLog(REDIS_WARNING,
            "Background AOF rewrite terminated with error");
    } else {
        server.aof_lastbgrewrite_status = REDIS_ERR;

        redisLog(REDIS_WARNING,
            "Background AOF rewrite terminated by signal %d", bysignal);
    }

cleanup:
    aofClosePipes();
    aofRewriteBufferReset();
    aofRemoveTempFile(server.aof_child_pid);
    server.aof_child_pid = -1;
    server.aof_rewrite_time_last = time(NULL)-server.aof_rewrite_time_start;
    server.aof_rewrite_time_start = -1;
    /* Schedule a new rewrite if we are waiting for it to switch the AOF ON. */
    if (server.aof_state == REDIS_AOF_WAIT_REWRITE)
        server.aof_rewrite_scheduled = 1;
}
Run Code Online (Sandbox Code Playgroud)

如您所见,所有代码路径必须执行cleanup重置server.aof_child_pid为-1 的代码.

Redis在问题期间记录的错误

21353:C 29 Nov 04:00:29.957*AOF重写:写入时复制使用的内存为8 MB

27848:M 29 Nov 04:00:30.133 ^ @ wait3()返回错误:没有子进程.rdb_child_pid = -1,aof_child_pid = 21353

你可以看到aof_child_pid不是-1.

abl*_*igh 6

TLDR:您目前依赖于signal(2)未指明的行为; 使用sigaction(小心)代替.

首先,SIGCHLD很奇怪.从手册页的sigaction;

POSIX.1-1990不允许设置行动SIGCHLD来SIG_IGN.POSIX.1-2001允许这种可能性,因此SIGCHLD可以使用忽略来防止僵尸的创建(参见wait(2)).然而,用于忽略的历史BSD和System V行为是SIGCHLD不同的,因此确保终止的孩子不会成为僵尸的唯一完全可移植的方法是捕获SIGCHLD信号并执行wait(2)或类似.

以下是wait(2)手册页中的内容:

POSIX.1-2001指定如果处置SIGCHLD设置为SIG_IGN或者设置了SA_NOCLDWAIT标志SIGCHLD(参见sigaction(2)),那么终止的子节点不会成为僵尸,并且会阻止wait() 或waitpid()将阻止所有子节点终止,然后将errno设置为失败ECHILD.(最初的POSIX标准将设置的行为保留SIGCHLD为SIG_IGN未指定.请注意,即使默认处置SIGCHLD为"ignore",也要明确设置处理SIG_IGN结果,以便对僵尸进程子进行不同处理.)Linux 2.6符合此规范.但是,Linux 2.4(及更早版本)不会:如果在忽略时进行wait()或waitpid()调用SIGCHLD,则调用的行为就好像SIGCHLD没有被忽略一样,即调用阻塞,直到下一个子进程终止,然后返回进程ID和那个孩子的状况.

请注意,这样的效果是,如果信号的处理行为类似于SIG_IGN设置,那么(在Linux 2.6+下)您将看到您正在看到的行为 - wait()即将返回-1并ECHLD因为孩子将被自动收获.

其次,信号处理pthreads(我认为你在这里使用)是非常困难的.它的工作方式(我相信你知道)是过程导向的信号被发送到信号未被屏蔽的过程中的任意线程.但是虽然线程有自己的信号掩码,但是有一个进程范围的动作处理程序.

将这两件事放在一起,我认为你遇到了我以前碰过的问题.我在使用SIGCHLD处理时遇到了问题signal()(这在pthreads之前已被弃用,这是公平的),这是通过移动sigaction并仔细设置每个线程信号掩码来修复的.我当时得出的结论是,C库正在仿效sigaction我正在告诉它的事情signal(),但却被绊倒了pthreads.

请注意,您当前依赖于未指定的行为.从手册页的signal(2):

的影响signal()在多线程程序是不确定的.

这是我建议你做的:

  1. 转移到sigaction()和pthread_sigmask().明确设置您关心的所有信号的处理(即使您认为这是当前的默认值),即使将它们设置为SIG_IGN或SIG_DFL.我这样做时会阻止信号(可能过于谨慎但我从某处复制了这个例子).

这是我正在做的(大致):

sigset_t set;
struct sigaction sa;

/* block all signals */
sigfillset (&set);
pthread_sigmask (SIG_BLOCK, &set, NULL);

/* Set up the structure to specify the new action. */
memset (&sa, 0, sizeof (struct sigaction));
sa.sa_handler = handlesignal;        /* signal handler for INT, TERM, HUP, USR1, USR2 */
sigemptyset (&sa.sa_mask);
sa.sa_flags = 0;
sigaction (SIGINT, &sa, NULL);
sigaction (SIGTERM, &sa, NULL);
sigaction (SIGHUP, &sa, NULL);
sigaction (SIGUSR1, &sa, NULL);
sigaction (SIGUSR2, &sa, NULL);

sa.sa_handler = SIG_IGN;
sigemptyset (&sa.sa_mask);
sa.sa_flags = 0;
sigaction (SIGPIPE, &sa, NULL);     /* I don't care about SIGPIPE */

sa.sa_handler = SIG_DFL;
sigemptyset (&sa.sa_mask);
sa.sa_flags = 0;
sigaction (SIGCHLD, &sa, NULL);     /* I want SIGCHLD to be handled by SIG_DFL */

pthread_sigmask (SIG_UNBLOCK, &set, NULL);
Run Code Online (Sandbox Code Playgroud)
  1. 在任何pthread操作之前,尽可能设置所有信号处理程序和掩码等.在可能的情况下,不要更改信号处理程序和掩码(您可能需要在fork()调用之前和之后执行此操作).

  2. 如果你需要一个信号处理程序SIGCHLD(而不是依赖SIG_DFL),如果可能的话让任何线程接收它,并使用自管方法或类似方法来警告主程序.

  3. 如果您必须具有执行/不处理某些信号的线程,请尝试将自己限制pthread_sigmask在相关线程中而不是sig*调用.

  4. 为了防止你遇到我遇到的下一个问题,请确保在你fork()完成之后再次设置从头开始(在孩子中)的信号处理,而不是依赖于你可能从父进程继承的任何东西.如果有一件事比混有pthread的信号更糟糕的话,它的信号与pthread混合在一起fork().

注意我无法完全解释为什么 change(1)有效,但它已经修复了对我来说非常相似的问题,并且毕竟依赖于以前"未指定"的东西.它最接近你的"假设2",但我认为它对传统信号函数的模拟真的是不完全的(特别是模仿之前的活泼行为signal()导致它首先被它取代sigaction()- 但这只是猜测).

顺便提一下,我建议您使用wait4()或(因为您没有使用rusage)waitpid()而不是wait3(),因此您可以指定要等待的特定PID.如果你有其他的东西可以生成孩子(我有一个图书馆这样做),你可能最终会等待错误的事情.那就是说,我不认为这就是这里发生的事情.

  • 我们可能会在这里进入聊天领域,但是默认的SIGCHLD配置是明确指定的,即使在那些`signal()`表现得像SA_RESETHAND的旧系统上也是如此.这个答案是一个很好的技术论述,但它不是问题的答案. (2认同)