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

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

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

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

Насколько я знаю, в текущем коде невозможно вызвать 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()? Насколько мне известно, должно быть никогда удалить зомби, если родитель не ждет его (с 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, который сбрасывает server.aof_child_pid в -1.

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

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

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

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


person antirez    schedule 30.11.2015    source источник
comment
Для меня это звучит так, как будто вы тестируете на голодание, на раннее, ребенок просто еще не кончился.   -  person alk    schedule 30.11.2015
comment
Возможно, вам захочется уточнить, как убедиться в следующем: wait3() вызывается, когда у нас есть ожидающие дочерние элементы, которых нужно ждать. это действительно так, хотя очевидно, что это не так. Должен признаться, я не знаю кода Redis, но какую другую механику вы бы использовали для синхронизации процессов относительно их времени жизни, кроме как с помощью вызовов wait*()? Я бы сказал, что вам предстоит гонка.   -  person alk    schedule 30.11.2015
comment
@alk: о тестировании слишком быстрого комментария, wait3 должен вернуть 0, если дочерний элемент все еще работает, а не -1, поскольку я использовал WNOHANG, как указано. Что касается вашего второго комментария, вы можете найти соответствующий фрагмент кода в приложении, где выделено, где вызывается wait3, как работа, которую он выполняет, обрабатывается функцией backgroundRewriteDoneHandler(), и как эта функция может привести только, AFAIK, к установке aof_child_pid в -1.   -  person antirez    schedule 30.11.2015
comment
Возможно, в тот момент, когда вы вызываете wait3(), ребенок еще даже не запустился (полностью)?   -  person alk    schedule 30.11.2015
comment
Невозможно из-за семантики fork(). Родитель уведомляется с дочерним pid, когда fork() возвращается, и с этого момента дочерний процесс существует. Однако, если вы проверите исходную проблему, вызовы wait3() постоянно просматриваются, поэтому даже в будущем он никогда не возвращает что-то другое.   -  person antirez    schedule 30.11.2015
comment
Для отладки: обработайте pid==-1/ECHLD так же, как pid==0, и запишите, найдет ли wait3() рассматриваемый процесс позже.   -  person alk    schedule 30.11.2015
comment
Я не уверен, что правильно понял настройку: наблюдаете ли вы описанное поведение также при явной установке signal(SIGCHLD, SIG_DFL);?   -  person alk    schedule 30.11.2015
comment
Для man 7 signal значение по умолчанию для SIGCHLD должно игнорироваться.   -  person alk    schedule 30.11.2015
comment
Кроме того, чтобы иметь более переносимый код (и, возможно, меньше проблем, которые вы наблюдаете), вы хотите заменить signal() на sigaction().   -  person alk    schedule 30.11.2015
comment
SIGCHLD по умолчанию имеет значение SIG_DFL, просто документация немного сбивает с толку. Также обратите внимание, что это работает во всех установках Redis, но в этой системе явно и воспроизводимо. Действием по умолчанию для SIGCHLD является игнорирование сигнала в том смысле, что обработчик сигнала не вызывается, но зомби-процессы будут стоять в очереди, ожидая, пока waitpid() получит их статус. Кстати, если вы проверите в конце проблемы Redis на Github, пользователю было предложено явно установить сигнал SIG_DFL и повторить попытку. Однако обратите внимание, что при первом появлении дочернего элемента работает функция waitpid().   -  person antirez    schedule 30.11.2015
comment
@antirez Старый сигнал unix сбрасывал обработчик сигнала по умолчанию (SIG_DFL) после первой обработки сигнала. Так что вполне возможно, что гипотеза 2 сбудется. Просто замените вызов signal() на sigaction() (который не сбрасывается в SIG_DFL), чтобы убедиться, что это правда.   -  person P.P    schedule 30.11.2015
comment
@Blue Moon: да, я думал об этом, но это недавняя система Linux, Ubuntu 12.04, и парадокс в том, что SIG_DFL — это то, что нам нужно, поэтому почти невозможно быть причиной. Вторая гипотеза состоит в том, что, что невероятно, он будет сброшен на SIG_IGN вместо SIG_DFL. В любом случае, используйте sigaction() в остальной части кода, но для этих двух сигналов, так что действительно, для ясности лучше переключить все на sigaction(), но в этом конкретном вопросе это не должно быть связано.   -  person antirez    schedule 30.11.2015
comment
@antirez что касается ваших обновлений о потоках и маскирующих сигналах, это может помочь замаскировать все сигналы во всех потоках, кроме потоков, в которых вы можете получать сигналы. На самом деле это не связано, но, например, в PulseAudio, в клиентской библиотеке, мы явно маскируем получение каких-либо сигналов, поскольку мы не хотим прерывать поток ввода-вывода, потенциально работающий в реальном времени.   -  person Ford_Prefect    schedule 01.12.2015
comment
@antirez, поскольку, по-видимому, вы знаете pid ребенка, которого ждете, и хотите ждать именно этого конкретного ребенка, кажется, было бы разумнее использовать wait4(), чем wait3(). Я сомневаюсь, что это решит проблему, о которой вы спрашивали, но это может избавить вас от горя позже.   -  person John Bollinger    schedule 01.12.2015
comment
@antirez, с другой стороны, есть ли шанс, что другой поток сначала соберет ожидаемого потомка?   -  person John Bollinger    schedule 01.12.2015
comment
В любом случае, если поведение действительно может быть воспроизведено только в определенной системе, то это убедительно свидетельствует о влиянии на него какой-то необычной характеристики этой системы. Возможно, это система сломана, а не Redis. Возможно, дело в ошибочном модуле ядра или неправильно сконфигурированном пользовательском ядре.   -  person John Bollinger    schedule 01.12.2015
comment
У Redis есть еще один вызов wait3() в sentinelCollectTerminatedScripts(), можем ли мы быть уверены, что в данном случае это не съест процессы, обозначенные rdb_child_pid /server.aof_child_pid?   -  person nos    schedule 08.12.2015
comment
Основываясь на комментарии @nos, stopAppendOnly в aof.c также убивает и ждет3, особенно для рассматриваемого процесса. Можете ли вы быть уверены, что какой-то другой код уже не выиграл гонку за потомством? Это может быть отвлекающим маневром, но в вашем отчете об ошибках все ошибки происходят очень близко к пятиминутным границам — возможно, cron или какая-то другая запланированная работа по обслуживанию вызывает неожиданный урожай?   -  person pilcrow    schedule 08.12.2015
comment
Я бы начал с запуска кода позади strace(1), обязательно используйте -e, чтобы вы видели только вызовы wait3(2). Это скажет вам, действительно ли код пытается получить один и тот же дочерний элемент более одного раза, или это что-то внешнее, которое стирает зомби (неправильно настроенное или ошибочное ядро, или другие странные скрипты, работающие на машине этого пользователя).   -  person Filipe Gonçalves    schedule 08.12.2015
comment
Кроме того, вам удалось воспроизвести эту проблему? Может быть, установить тот же образ ядра на виртуальную машину?   -  person Filipe Gonçalves    schedule 08.12.2015


Ответы (1)


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() будет блокироваться до тех пор, пока все дочерние процессы завершаются, а затем завершаются с ошибкой, установленной на 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() на многопоточный процесс не указано.

Вот что я рекомендую вам сделать:

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

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

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

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

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

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

person abligh    schedule 08.12.2015
comment
Не уместно, я думаю. И OP, и grep источников предполагают, что расположение SIGCHLD никогда не задается в коде. Он также не замаскирован (не то, чтобы это имело значение здесь). Симптомы, кроме того, предполагают, что SIG_IGN не мог быть унаследован, так как wait3 хотя бы один раз нормально отработал. - person pilcrow; 08.12.2015
comment
Я бы все же посоветовал правильно настроить его с помощью sigaction. Работа с первого раза меня не удивляет; у меня это работало ненадежно, и некоторая семантика для эмулируемого сигнала требует сброса обработки сигнала в обработчике. Если это не дает никакого эффекта, то это по-прежнему правильный способ настройки обработки сигналов. - person abligh; 09.12.2015
comment
Мудрый совет, но опять же не применимый здесь. Он вообще не настроен — нет вызовов signal(SIGCHLD, ...) и sigaction(SIGCHLD,...), и мы можем сделать вывод, что процесс начинается с SIGCHLD, установленного в SIG_DFL. - person pilcrow; 09.12.2015
comment
Верно, но по-прежнему существует состояние по умолчанию для обработки SIGCHLD; это может быть 'обработано signal()'. В моем случае жизнь была более сложной (линковка с libxl, которая делает свои собственные ответвления и обработку сигналов, если вам нужны кровавые подробности), но я могу только сказать, что приведенное выше заклинание все исправило. Учитывая, что явная настройка обработки сигналов в любом случае не является плохой идеей (особенно с учетом того, что значение по умолчанию для SIGCHLD несколько непрозрачно), я думаю, что это стоит попробовать. Это может не иметь ничего общего с тем, что я предложил, и в этом случае OP просто получает более чистую настройку сигнала. - person abligh; 09.12.2015
comment
Здесь мы можем зайти на территорию чата, но расположение SIGCHLD по умолчанию хорошо определено, даже в тех старых системах, где signal() вел себя как SA_RESETHAND. Этот ответ представляет собой прекрасное техническое изложение, но это не ответ на заданный вопрос. - person pilcrow; 09.12.2015