Подтвердить что ты не робот

Wait3 (waitpid alias) возвращает -1 с errno, установленным в ECHILD, когда он не должен

Контекст - это проблема Redis. У нас есть вызов wait3(), который ожидает, что ретранслятор AOF создаст новую версию AOF на диске. Когда ребенок выполнен, родитель уведомляется через wait3(), чтобы заменить старый AOF на новый.

Однако в контексте вышеупомянутой проблемы пользователь уведомил нас об ошибке. Я немного изменил реализацию Redis 3.0, чтобы четко регистрировать, когда wait3() вернулся -1 вместо сбоя из-за этого неожиданного состояния. Так вот что происходит, по-видимому:

  • wait3() вызывается, когда у нас есть ожидающие ожидания дети.
  • SIGCHLD должен быть установлен в SIG_DFL, код не установлен вообще в Redis, поэтому это поведение по умолчанию.
  • Когда происходит первая перезапись AOF, wait3() успешно работает, как ожидалось.
  • Начиная с второй перезаписи AOF (второй созданный дочерний элемент), wait3() начинает возвращать -1.

AFAIK невозможно в текущем коде, который мы вызываем wait3(), в то время как нет ожидающих дочерних элементов, так как при создании дочернего элемента AOF мы устанавливаем значение server.aof_child_pid на значение pid, а reset это только после успешного вызова wait3().

Итак, wait3() не должно иметь никаких причин для отказа с -1 и ECHILD, но это так, поэтому, возможно, ребенок-зомби не создан по какой-то неожиданной причине.

Гипотеза 1. Возможно, что Linux во время определенных нечетных условий отбросит ребенка-зомби, например, из-за давления в памяти? Не выглядит разумным, так как зомби имеет только прикрепленные к нему метаданные, но кто знает.

Обратите внимание, что мы вызываем wait3() с WNOHANG. И учитывая, что по умолчанию SIGCHLD установлен на SIG_DFL, единственное условие, которое должно привести к сбою и возврату -1 и ECHLD, не должно быть зомби, доступным для сообщения информации.

Гипотеза 2: Другая вещь, которая может произойти, но нет объяснений, если это произойдет, заключается в том, что после того, как первый ребенок умирает, обработчик SIGCHLD установлен в SIG_IGN, вызывая wait3() для возврата -1 и ECHLD.

Гипотеза 3: Есть ли способ удалить детей-зомби извне? Возможно, у этого пользователя есть какой-то script, который удаляет процессы зомби в фоновом режиме, чтобы затем информация больше не была доступна для wait3()? Насколько мне известно, никогда не удастся удалить зомби, если родитель не ждет его (с помощью waitpid или обработки сигнала), и если SIGCHLD не игнорируется, но, возможно, существует определенный Linux-способ.

Гипотеза 4. В коде Redis есть ошибка, так что мы успешно wait3() ребенок в первый раз без правильного сброса состояния, а затем мы снова и снова вызываем wait3() но больше нет зомби, поэтому он возвращает -1. Анализ кода кажется невозможным, но, возможно, я ошибаюсь.

Еще одна важная вещь: мы никогда не замечали этого в прошлом. Это происходит только в этой конкретной системе Linux.

ОБНОВЛЕНИЕ. Йосси Готтлиб предположил, что SIGCHLD по какой-либо причине получен другим потоком в процессе Redis (обычно это не происходит, только в этой системе). Мы уже маскируем SIGALRM в bio.c потоках, возможно, мы могли бы попытаться маскировать 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, который reset server.aof_child_pid равен -1.

Ошибки, зарегистрированные в Redis во время выпуска

21353: C 29 ноября 04: 00: 29.957 * Запись AOF: 8 МБ памяти, используемой при копировании на запись

27848: M 29 Nov 04: 00: 30.133 ^ @wait3() возвратил ошибку: никаких дочерних процессов. rdb_child_pid = -1, aof_child_pid = 21353

Как вы видите, aof_child_pid не равен -1.

4b9b3361

Ответ 1

TL;DR: вы в настоящее время полагаетесь на неуказанное поведение 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() будет заблокирован до тех пор, пока все дети не будут завершены, а затем с ошибкой, установленным на ECHILD. (Исходный стандарт POSIX оставил поведение параметра SIGCHLD до SIG_IGN неуказанным. Обратите внимание, что даже если расположение по умолчанию SIGCHLD равно "игнорировать", явное указание расположения на SIG_IGN приводит к различной обработке процесса зомби дети.) Linux 2.6 соответствует этой спецификации. Однако в Linux 2.4 (и ранее) нет: если вызов wait() или waitpid() выполняется, когда SIGCHLD игнорируется, вызов ведет себя так, как если бы SIGCHLD не игнорировались, то есть вызов блокирует до тех пор, пока следующий ребенок не завершится, а затем вернет идентификатор процесса и статус этого дочернего элемента.

Обратите внимание, что если обработка сигнала ведет себя как SIG_IGN, то (под Linux 2.6+) вы увидите поведение, которое вы видите, т.е. wait() вернет -1 и ECHLD потому что ребенок будет автоматически получен.

Во-вторых, обработка сигналов с помощью pthreads (который, как я думаю, вы используете здесь), печально тяжела. Способ, которым он предназначался для работы (как я уверен, вы знаете), заключается в том, что направленные сигналы направляются в произвольный поток в процессе, у которого сигнал разомкнут. Но в то время как потоки имеют собственную сигнальную маску, существует обработчик действия с широким спектром действий.

Объединяя эти две вещи, я думаю, что вы столкнулись с проблемой, с которой я столкнулся раньше. У меня возникли проблемы с обработкой SIGCHLD для работы с signal() (что достаточно справедливо, поскольку оно было устаревшим до pthreads), которые были исправлены, переместившись на sigaction и тщательно настроив маски потока. Мой вывод в то время состоял в том, что библиотека C подражала (с sigaction) тем, что я говорил ему делать с signal(), но сработал pthreads.

Обратите внимание, что в настоящее время вы полагаетесь на неуказанное поведение. На странице руководства signal(2):

Эффекты signal() в многопоточном процессе не определены.

Здесь я рекомендую вам:

  • Переместитесь на 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);
  1. По возможности установите все ваши обработчики сигналов и маски и т.д. перед любыми операциями pthread. По возможности не меняйте обработчики и маски сигналов (вам может понадобиться сделать это до и после вызовов fork()).

  2. Если вам нужен обработчик сигнала для SIGCHLD (вместо того, чтобы полагаться на SIG_DFL), если возможно, пусть он будет получен любым потоком и будет использовать метод self-pipe или аналогичный для предупреждения основного программа.

  3. Если у вас есть потоки, которые выполняют/не обрабатывают определенные сигналы, попробуйте ограничить себя pthread_sigmask в соответствующем потоке, а не sig*.

  4. На всякий случай, когда вы запускаете headlong в следующую проблему, с которой я столкнулся, убедитесь, что после того, как у вас есть fork() 'd, вы снова настроили обработку сигнала с нуля (у ребенка), а не полагаетесь на что-либо вы можете наследовать родительский процесс. Если есть что-то хуже, чем сигналы, смешанные с pthread, он сигнализирует смешать с pthread с помощью fork().

Примечание. Я не могу полностью объяснить, почему работает изменение (1), но оно зафиксировало то, что похоже на очень похожую проблему для меня, и в конце концов полагалось на то, что раньше было "неопределенным". Это ближе всего к вашей "гипотезе 2", но я думаю, что это действительно неполная эмуляция устаревших сигнальных функций (в частности, эмулирование ранее совершенного поведения signal(), что и заставило его заменить на sigaction() в первую очередь), но это это просто предположение).

Кстати, я предлагаю вам использовать wait4() или (поскольку вы не используете rusage) waitpid(), а не wait3(), поэтому вы можете указать определенный PID для ожидания. Если у вас есть что-то другое, что генерирует детей (у меня была библиотека), вы можете в конечном итоге ожидать неправильного. Тем не менее, я не думаю, что здесь происходит.