Как правильно подсчитать фактическое количество разветвленных дочерних процессов?

Некоторое время назад я написал простой SMTP-шлюз для автоматической обработки S/MIME, и теперь дело доходит до тестирования. Как обычно для почтовых серверов, главный процесс создает дочерний процесс для каждого входящего соединения. Хорошей практикой является ограничение количества создаваемых дочерних процессов, и я так и сделал.

При большой нагрузке (много подключений от многих клиентов одновременно) оказывается, что дочерние процессы учитываются неправильно -- проблема в уменьшении счетчика при завершении дочерних процессов. Через несколько минут интенсивной нагрузки счетчик больше фактического количества дочерних процессов (т.е. через 5 минут он равен 14, но их нет).

Я уже провел некоторые исследования, но ничего не помогло. Все зомби-процессы собраны, поэтому обработка SIGCHLD выглядит нормально. Я думал, что это может быть проблема синхронизации, но добавление мьютекса и изменение типа переменной на volatile sig_atomic_t (как сейчас) не дает никаких изменений. Это также не проблема с маскировкой сигнала, я пробовал маскировать весь сигнал, используя sigfillset(&act.sa_mask).

Я заметил, что waitpid() иногда возвращает странные значения PID (очень большие, вроде 172915914).

Вопросы и код.

  1. Возможно ли, что другой процесс (т.е. init) пожинает некоторые из них?
  2. Может ли процесс не стать зомби после выхода? Можно ли его получить автоматически?
  3. Как это исправить? Может быть, есть лучший способ их подсчета?

Разветвление ребенка в main():

volatile sig_atomic_t sproc_counter = 0;    /* forked subprocesses counter */

/* S/MIME Gate main function */
int main (int argc, char **argv)
{
    [...]

    /* set appropriate handler for SIGCHLD */
    Signal(SIGCHLD, sig_chld);

    [...]

    /* SMTP Server's main loop */
    for (;;) {

        [...]

        /* check whether subprocesses limit is not exceeded  */
        if (sproc_counter < MAXSUBPROC) {
            if ( (childpid = Fork()) == 0) {    /* child process */
                Close(listenfd);                /* close listening socket */
                smime_gate_service(connfd);     /* process the request */
                exit(0);
            }
            ++sproc_counter;
        }
        else
            err_msg("subprocesses limit exceeded, connection refused");

        [...]
    }
    Close(connfd);  /* parent closes connected socket */
}

Обработка сигналов:

Sigfunc *signal (int signo, Sigfunc *func)
{
    struct sigaction    act, oact;

    act.sa_handler = func;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    if (signo == SIGALRM) {
#ifdef  SA_INTERRUPT
        act.sa_flags |= SA_INTERRUPT;   /* SunOS 4.x */
#endif
    }
    else {
#ifdef  SA_RESTART
        act.sa_flags |= SA_RESTART;     /* SVR4, 44BSD */
#endif
    }
    if (sigaction(signo, &act, &oact) < 0)
        return SIG_ERR;

    return oact.sa_handler;
}

Sigfunc *Signal (int signo, Sigfunc *func)
{
    Sigfunc *sigfunc;

    if ( (sigfunc = signal(signo, func)) == SIG_ERR)
        err_sys("signal error");
    return sigfunc;
}

void sig_chld (int signo __attribute__((__unused__)))
{
    pid_t pid;
    int stat;

    while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0) {
        --sproc_counter;
        err_msg("child %d terminated", pid);
    }
    return;
}

ПРИМЕЧАНИЕ. Все функции, начинающиеся с заглавной буквы (например, Fork(), Close(), Signal() и т. д.), делают и ведут себя так же, как и их друзья в нижнем регистре (fork(), close(), signal() и т. д.), но имеют лучшую ошибку. обработка - поэтому мне не нужно проверять их статусы возврата.

ПРИМЕЧАНИЕ 2: я запускаю и компилирую его в Debian Testing (kernel v3.10.11), используя gcc 4.8.2.


person TPhaster    schedule 03.01.2014    source источник
comment
Подумайте о том, чтобы ваш код периодически вызывал sig_chld(), например, в потоке. А не в функции обработчика сигнала. Обработчики сигналов, такие как ваш, могут не завершиться правильно, когда есть шквал сигналов. Что, кажется, ваша проблема.   -  person jim mcnamara    schedule 03.01.2014
comment
Что делает ваша функция Fork, когда fork терпит неудачу?   -  person Duck    schedule 03.01.2014
comment
Он печатает сообщение об ошибке и завершает вызов exit(1).   -  person TPhaster    schedule 03.01.2014


Ответы (2)


Я думаю, что метод сигнала можно исправить, а создание потока заставляет вас выполнять программу для обработки соединения.

Есть несколько проблем:

  • Изменения в sproc_counter могут быть потеряны, если процесс создается и завершается одновременно. Чтобы исправить это, либо используйте маски сигналов (например, sigprocmask(), pselect()), чтобы гарантировать, что обработчик не вызывается, пока основной поток манипулирует sproc_counter, либо заставьте обработчик сигналов установить флаг и выполнить waitpid(), манипулирование счетчиком и регистрацию в основном потоке. поток (но не в новом потоке). Обратите внимание, что метод флага по-прежнему требует манипулирования маской сигнала, если вы хотите избежать ожидания для нового соединения или другого завершающего соединения непосредственно после завершающего соединения.

  • err_msg(), вероятно, небезопасен для асинхронного сигнала. Я вижу три варианта:

    • use the flag method mentioned above, or
    • убедитесь, что никакие небезопасные функции асинхронного сигнала не вызываются, пока SIGCHLD не замаскировано, или
    • удалить вызов из обработчика сигнала.
  • Переопределение signal() может привести к тому, что другой код вызовет вашу версию вместо стандартной версии. Это может привести к странному поведению.

  • Обработчик сигнала не сохраняет и не восстанавливает значение errno.

Если у вас есть проблемы из-за того, что сигналы прерывают другие сигналы, для этого предназначено поле sa_mask в sigaction.

person jilles    schedule 10.01.2014
comment
Ага, способ с манипулированием маской сигнала работает хорошо! И гораздо проще. Спасибо за этот ответ. - person TPhaster; 14.01.2014

сам отвечу.

Есть несколько причин, по которым дочерние процессы не учитываются таким образом. Во-первых, обработчик сигнала может быть прерван другим сигналом. Я не могу найти никакой информации, что на самом деле происходит, когда возникает эта ситуация. Некоторая информация об этом есть на страницах руководства libc и в этом ответе. Но это не может быть проблемой.

Кажется, что операции с переменной volatile sig_atomic_t на самом деле не атомарны и зависят от архитектуры системы. Например, на amd64 скомпилированный код уменьшения значения sproc_counter выглядит так:

movl    sproc_counter(%rip), %eax
subl    $1, %eax
movl    %eax, sproc_counter(%rip)

Как видите, инструкций на ассемблере целых три! Это точно не атомарно, поэтому доступ к sproc_counter приходится синхронизировать.

Хорошо, но почему добавление мьютекса не дало результата? Ответ находится на странице руководства pthread_mutex_lock()/pthread_mutex_unlock():

БЕЗОПАСНОСТЬ АСИНХРОНИЧЕСКОГО СИГНАЛА

Функции мьютекса не безопасны для асинхронных сигналов. Это означает, что их нельзя вызывать из обработчика сигнала. В частности, вызов pthread_mutex_lock или pthread_mutex_unlock из обработчика сигналов может привести к взаимоблокировке вызывающего потока.

Это проясняет. Что еще более важно, вызов функций, которые печатают дату (сообщение журнала), также является плохой идеей - используемый там fputs() не является безопасным для асинхронного сигнала.

Как это сделать правильно?

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

Нет больше слов, давайте посмотрим код.

Обработка сигналов будет выглядеть так:

void sig_chld (int signo __attribute__((__unused__)))
{
  sigchld_notify = 1;
}

Процедура main():

volatile sig_atomic_t sigchld_notify = 0;                /* SIGCHLD notifier */
int sproc_counter = 0;                                   /* forked child process counter */
pthread_mutex_t sproc_mutex = PTHREAD_MUTEX_INITIALIZER; /* mutex for child process counter */

/* S/MIME Gate main function */
int main (int argc, char **argv)
{
    pthread_t guard_id;
    [...]

    /* start child process guard */
    if (0 != pthread_create(&guard_id, NULL, child_process_guard, NULL) )
        err_sys("pthread_create error");

    [...]

    /* SMTP Server's main loop */
    for (;;) {
        [...]

        /* check whether child processes limit is not exceeded */
        if (sproc_counter < MAXSUBPROC) {
            if ( (childpid = Fork()) == 0) { /* child process */
                Close(listenfd);             /* close listening socket */
                smime_gate_service(connfd);  /* process the request */
                exit(0);
            }
            pthread_mutex_lock(&sproc_mutex);
            ++sproc_counter;
            pthread_mutex_unlock(&sproc_mutex);
        }
        else
            err_msg("subprocesses limit exceeded, connection refused");

        Close(connfd); /* parent closes connected socket */
    }
} /* end of main() */

Процедура защиты потока:

extern volatile sig_atomic_t sigchld_notify; /* SIGCHLD notifier */
extern int sproc_counter;                    /* forked child process counter */
extern pthread_mutex_t sproc_mutex;          /* mutex for child process counter */

void* child_process_guard (void* arg __attribute__((__unused__)))
{
    pid_t pid;
    int stat;

    for (;;) {
        if (0 == sigchld_notify) {
            usleep(SIGCHLD_SLEEP);
            continue;
        }

        while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0) {
            pthread_mutex_lock(&sproc_mutex);
            --sproc_counter;
            pthread_mutex_unlock(&sproc_mutex);
            err_msg("child %d terminated", pid);
        }
        sigchld_notify = 0;
    }
    return NULL;
}
person TPhaster    schedule 10.01.2014
comment
Обратите внимание, что smime_gate_service() может использовать только безопасные функции асинхронного сигнала, такие как execve(), если вы делаете это таким образом. Это связано с тем, что fork() реплицирует только вызывающий поток в новом процессе, в то время как другие потоки могут владеть блокировками (включая блокировки, используемые внутри системы). - person jilles; 11.01.2014
comment
Спасибо за этот комментарий, мне не пришло в голову, как мешают форки и треды. Но это скорее не проблема - smime_gate_service() является однопоточным, не выполняет форки и не имеет обработки сигналов. Потоков и ответвлений не больше, чем вы видите в этом фрагменте кода. Еще лучше, если реплицируется только основной поток, потому что мне нужен только он. - person TPhaster; 11.01.2014
comment
Проблема все еще может быть, например, если вилка происходит, когда дочерний поток защиты процесса находится в err_msg(), а дочерний процесс также использует err_msg() позже. Структуры данных могут быть несогласованными, или вы можете ждать несуществующего потока в дочернем процессе, чтобы что-то разблокировать (в частности, объекты stdio FILE указаны как заблокированные). - person jilles; 11.01.2014
comment
Ах, теперь я вижу, но это пугающее видение — и, что еще хуже, оно реально! Как вы думаете, достаточно ли увеличить критические секции? (в main() иметь fork() внутри; в child_process_guard() иметь err_msg() внутри) Безопасны ли waitpid() и usleep() для многопоточного форка? - person TPhaster; 14.01.2014