2014-01-03 2 views
1

Некоторое время назад я написал простой 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.

+0

Рассмотреть возможность временного вызова кода sig_chld(), например, в потоке. Вместо функции обработки сигнала. У обработчиков сигналов, подобных вашим, есть вероятность, что они не будут корректно завершены, когда будет подавлен сигнал. Кажется, это ваша проблема. –

+0

Что делает ваша функция 'Fork', когда' fork' терпит неудачу? – Duck

+0

Он печатает сообщение об ошибке и выходит из вызова 'exit (1)'. – TPhaster

ответ

0

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

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

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

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

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

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

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

+0

Да, путь с манипуляцией сигнальной маской хорошо работает! И намного проще. Спасибо за этот ответ. – TPhaster

0

Я отвечу сам.

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

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

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

Имея в виду, что может случиться во время обработки сигнала (например, доставка других сигналов), совершенно ясно, что процедура обработки сигналов должна быть как можно более кратким. Это намного лучше, чем set a flag in handler и проверять его время от времени в главной программе или посвященной теме. Я выбираю второе решение.

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

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

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() */ 

Guarding нить рутина:

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; 
} 
+0

Обратите внимание, что 'smime_gate_service()' может использовать только безопасные функции async-signal, такие как 'execve()', если вы это сделаете. Это связано с тем, что 'fork()' реплицирует только вызывающий поток в новом процессе, тогда как другие потоки могут иметь блокировки (включая блокировки, используемые внутри системы). – jilles

+0

Спасибо за этот комментарий, я не задумывался о том, как фолки и потоки мешают. Но это скорее не проблема. 'Smime_gate_service()' является однопоточным, не выполняет вилки и не имеет обработки сигналов. Больше нет потоков и вилок, чем вы видите в этом отрывке кода. Еще лучше, что только основной поток реплицируется - потому что это единственное, что мне нужно. – TPhaster

+0

Все еще может быть проблема, например, если вилка происходит, пока поток защиты дочернего процесса находится в 'err_msg()', а дочерний процесс также использует 'err_msg()' позже. Структуры данных могут быть несогласованными, или вы можете ожидать, что поток, который не существует в дочернем процессе, чтобы разблокировать что-либо (в частности, stdio 'FILE' объекты указаны для блокировки). – jilles