上下文是Redis issue。我们有一个wait3()调用,等待AOF重写子级在磁盘上创建新的AOF版本。当 child 完成后,将通过wait3()通知 parent ,以便用新的AOF替换旧的AOF。
但是,在上述问题的背景下,用户通知了我们一个错误。我修改了Redis 3.0的实现,以清楚地记录wait3()返回-1的时间,而不是由于此意外情况而崩溃。因此,显然是这样的:

当我们有未决 child 等待时,会调用

  • wait3()
  • SIGCHLD应该设置为SIG_DFL,Redis中根本没有代码设置此信号,因此这是默认行为。
  • 第一次AOF重写发生时,wait3()成功按预期工作。
  • 从第二次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 :可能发生的其他事情,但如果发生,则没有任何解释,是在第一个 child 去世后,SIGCHLD处理程序设置为SIG_IGN,导致wait3()返回-1和ECHLD
    假设3 :是否可以通过某种方式从外部移除僵尸 child ?也许此用户具有某种脚本,可以在后台删除僵尸进程,以使该信息不再可用于wait3()?据我所知,如果 parent 不等待僵尸(使用waitpid或处理信号)并且不忽略SIGCHLD,则永远不可能删除僵尸,但是也许有一些Linux特定的方式。
    假设4 :Redis代码中实际上存在一些错误,因此我们在没有正确重置状态的情况下第一次成功对 child 进行了wait3(),后来我们一次又一次地调用wait3(),但是不再有僵尸,因此它返回-1 。分析代码似乎是不可能的,但也许我错了。
    另一个重要的事情:我们过去从未观察到这一点。显然,仅在此特定的Linux系统中会发生。
    更新:Yossi Gottlieb提出,由于某种原因,Redis进程中的另一个线程会接收到SIGCHLD(通常不会发生,仅在此系统上会发生)。我们已经在SIGALRM线程中屏蔽了bio.c,也许我们也可以尝试从I/O线程中屏蔽SIGCHLD
    附录: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 {
    
    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;
    }
    
    如您所见,所有代码路径都必须执行cleanup代码,将server.aof_child_pid重置为-1。
    Redis在问题期间记录的错误

    如您所见,aof_child_pid不是-1。

    最佳答案

    TLDR:您当前依赖于signal(2)的未指定行为;请谨慎使用sigaction

    首先,SIGCHLD很奇怪。从manual page中获取sigaction;



    这是wait(2)的manual page的内容:



    请注意,这样做的效果是,如果信号的处理方式像设置了SIG_IGN一样,那么(在Linux 2.6+下)您将看到所看到的行为-即wait()将返回-1ECHLD,因为子级将被自动收割。

    其次,众所周知,使用pthreads(我认为您在此处使用)进行信号处理非常困难。它的工作方式(如我所确定的那样)是,进程定向信号被发送到进程中具有未屏蔽信号的任意线程。但是,尽管线程具有其自己的信号掩码,但存在一个进程范围的 Action 处理程序。

    将这两件事放在一起,我认为您遇到的是我之前遇到的问题。我在使SIGCHLD处理与signal()一起工作时遇到了问题(这很公平,因为在pthreads之前已弃用),已通过移动到sigaction并仔细设置每个线程信号掩码来解决。我当时的结论是C库正在(用sigaction)模拟我告诉它与signal()一起使用的内容,但被pthreads绊倒了。

    请注意,您当前正在依赖未指定的行为。从signal(2)manual page中:



    这是我建议您执行的操作:

  • 移至sigaction()pthread_sigmask()。显式设置您关心的所有信号的处理方式(即使您认为这是当前的默认设置),即使将其设置为SIG_IGNSIG_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);
    
  • 在任何pthread操作之前,请尽可能设置所有信号处理程序和掩码等。尽可能不要更改信号处理程序和掩码(您可能需要在fork()调用之前和之后进行此操作)。
  • 如果需要SIGCHLD的信号处理程序(而不是依赖SIG_DFL),请尽可能让任何线程接收它,并使用self-pipe方法或类似的方法来警告主程序。
  • 如果必须具有不处理某些信号的线程,请尝试将自己限制在相关线程中的pthread_sigmask而不是sig*调用中。
  • 以防万一,您遇到了我遇到的下一个问题,请确保在您完成fork()之后,重新设置从头开始的信号处理(在子级中),而不要依赖于您可能从父级继承的任何东西处理。如果有比混合了pthread的信号更糟糕的事情,那就是混合了fork()的pthread的信号。

  • 请注意,我无法完全解释更改(1)为何有效的原因,但是它已经解决了对我来说看起来非常相似的问题,并且毕竟依赖于以前“未指定”的内容。它最接近您的“假设2”,但我认为这实际上是对遗留信号功能的不完全仿真(特别是仿真signal()的先前行为,这是首先导致它被sigaction()取代的原因-但这只是一个猜测)。

    顺便说一句,我建议您使用wait4()或(因为您没有使用rusage)waitpid()而不是wait3(),因此您可以指定要等待的特定PID。如果您还有其他产生 child 的东西(我有图书馆做过),您可能最终会等待错误的消息。就是说,我认为这不是正在发生的事情。

    关于c - wait3(waitpid别名)在不应将errno设置为ECHILD的情况下返回-1,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/33994543/

    10-09 21:28