【问题标题】:wait3 (waitpid alias) returns -1 with errno set to ECHILD when it should notwait3(waitpid 别名)返回 -1 且 errno 不应该设置为 ECHILD
【发布时间】:2016-03-03 20:22:57
【问题描述】:

上下文是Redis issue。我们有一个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,导致@987654342 @ 返回 -1 和 ECHLD

假设3:有没有办法从外部去除僵尸孩子?也许这个用户有某种脚本可以在后台删除僵尸进程,这样wait3() 的信息就不再可用?据我所知,如果父母不等待它(使用waitpid 或处理信号)并且如果SIGCHLD 未被忽略,则应该永远不可能删除僵尸,但也许存在是一些 Linux 特有的方式。

假设4:实际上Redis代码中存在一些bug,导致我们第一次成功wait3()child没有正确重置状态,后来我们一次次调用wait3()但不再有僵尸,所以它返回 -1。分析代码看起来不可能,但也许我错了。

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

更新:Yossi Gottlieb 提出 SIGCHLD 出于某种原因被 Redis 进程中的另一个线程接收(不正常发生,仅在此系统上)。我们已经在 bio.c 线程中屏蔽了 SIGALRM,也许我们也可以尝试从 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;
}

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

问题期间 Redis 记录的错误

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

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

如您所见,aof_child_pid 不是 -1。

【问题讨论】:

  • 对我来说,这听起来好像你正在测试快,早,孩子根本还没有结束。
  • 也许您可能想详细说明如何确保这一点:“wait3() 在我们有待处理的孩子等待时调用。”确实如此,因为显然不是。我不得不承认,我不知道 Redis 代码,但是您会使用哪些其他机制来同步进程的实时时间,但使用对 wait*() 的调用?我会说你正面临一场比赛。
  • 还有更多的可移植代码(并且可能会减少您观察到的此类问题),您希望将 signal() 替换为 sigaction()
  • @antirez 旧的 unix 信号确实将信号处理程序重置为默认值 (SIG_DFL)第一次处理信号之后。所以假设 2 有可能发生。只需将 signal() 调用替换为 sigaction()(不会重置为 SIG_DFL)即可查看是否为真。
  • Redis 在 sentinelCollectTerminatedScripts() 中有另一个 wait3() 调用,我们可以确定这不会占用 rdb_child_pid /server.aof_child_pid 在这种情况下标识的进程吗?

标签: c linux system-calls zombie-process waitpid


【解决方案1】:

TLDR:您当前依赖于signal(2) 的未指定行为;请改用sigaction(小心)。

首先,SIGCHLD 很奇怪。来自manual pagesigaction

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

这是来自wait(2) 的manual page 的部分内容:

POSIX.1-2001 指定如果SIGCHLD 的处置设置为SIG_IGNSA_NOCLDWAIT 标志为SIGCHLD 设置(请参阅sigaction(2)),那么终止的子代执行不会变成僵尸,并且对 wait()waitpid() 的调用将阻塞,直到所有子节点都终止,然后失败,errno 设置为 ECHILD。 (原始 POSIX 标准未指定将 SIGCHLD 设置为 SIG_IGN 的行为。请注意,即使 SIGCHLD 的默认处置是“忽略”,但将处置显式设置为 SIG_IGN 会导致对僵尸进程的不同处理孩子。)Linux 2.6 符合此规范。但是,Linux 2.4(及更早版本)不会:如果在 SIGCHLD 被忽略时调用 wait()waitpid(),则调用的行为就像 SIGCHLD 未被忽略一样,即调用阻塞,直到下一个子进程终止,然后返回该子进程的进程 ID 和状态。

注意这样做的效果是,如果信号的处理行为类似于设置了 SIG_IGN,那么(在 Linux 2.6+ 下)您将看到您所看到的行为 - 即 wait() 将返回 -1 和 @987654355 @ 因为孩子会被自动收割。

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

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

请注意,您目前依赖于未指定的行为。来自signal(2)manual page

signal() 在多线程进程中的作用未指定。

我建议你这样做:

  1. 移至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);
  1. 在可能的情况下,在任何pthread 操作之前设置所有信号处理程序和掩码等。尽可能不要更改信号处理程序和掩码(您可能需要在调用 fork() 之前和之后执行此操作)。

  2. 1234563程序。
  3. 如果您必须拥有处理/不处理某些信号的线程,请尝试将自己限制为相关线程中的pthread_sigmask,而不是sig* 调用。

  4. 1234563您可能会从父进程继承。如果有比与 pthread 混合的信号更糟糕的事情,那就是与 pthread 混合的信号与fork()

请注意,我无法完全解释 为什么 更改 (1) 有效,但它已经解决了对我来说看起来非常相似的问题,毕竟它依赖于以前“未指定”的东西。它最接近您的“假设 2”,但我认为它确实是对遗留信号功能的不完整仿真(特别是模拟 signal() 以前的活泼行为,这首先导致它被 sigaction() 取代 - 但这只是猜测)。

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

【讨论】:

  • 我认为没有密切关系。 OP 和来源的 grep 都表明 SIGCHLD 的处置从未在代码中设置。它也没有被掩盖(在这里并不重要)。这些症状进一步表明 SIG_IGN 不能被继承,因为 wait3 至少正常工作一次。
  • 我仍然建议使用sigaction 正确设置它。它第一次工作并不让我感到惊讶。它对我来说工作不可靠,并且模拟信号的某些语义需要在处理程序中重置信号处理。如果它没有效果,那么它仍然是设置信号处理的正确方法。
  • 明智的建议,但同样不适用于此处。它根本没有“设置”——没有调用signal(SIGCHLD, ...)sigaction(SIGCHLD,...),我们可以推断该进程以 SIGCHLD 设置为 SIG_DFL 开始。
  • 对,但是还有SIGCHLD处理的默认状态;这可能是由signal()'处理的'。在我的情况下,生活更复杂(如果你想要血淋淋的细节,与 libxl 链接,它会执行自己的分叉和信号处理),但我只能说上面的咒语解决了问题。鉴于明确设置信号处理无论如何都不是一个坏主意(特别是考虑到 SIGCHLD 的默认值有些不透明),我认为这值得 OP 尝试。这可能与我的建议无关,在这种情况下,OP 只会获得更清晰的信号设置。
  • 我们可能会进入聊天领域,但默认的 SIGCHLD 配置是明确指定的,即使在 signal() 行为类似于 SA_RESETHAND 的旧系统上也是如此。这个答案是一个很好的技术阐述,但它不是对所提出问题的答案。
猜你喜欢
  • 1970-01-01
  • 2014-05-06
  • 1970-01-01
  • 1970-01-01
  • 2015-10-15
  • 2010-10-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多