c linux system-calls zombie-process waitpid

wait3(waitpid alias) devuelve-1 con errno establecido en ECHILD cuando no debería



fork waitpid (1)

TLDR: actualmente se basa en el comportamiento no especificado de la signal (2); use sigaction (cuidadosamente) en su lugar.

En primer lugar, SIGCHLD es extraño. Desde la página de manual para sigaction ;

POSIX.1-1990 no permitido estableciendo la acción para SIGCHLD en SIG_IGN . POSIX.1-2001 permite esta posibilidad, de modo que ignorar SIGCHLD se puede usar para evitar la creación de zombies (ver wait (2)). Sin embargo, los comportamientos históricos de BSD y System V para ignorar SIGCHLD diferentes, por lo que el único método completamente portátil para garantizar que los niños terminados no se conviertan en zombis es atrapar la señal SIGCHLD y realizar una wait (2) o similar.

Y aquí está el bit de la página de manual de wait (2):

POSIX.1-2001 especifica que si la disposición de SIGCHLD se establece en SIG_IGN o el indicador SA_NOCLDWAIT se establece en SIGCHLD (ver sigaction (2)), entonces los hijos que terminan no se convierten en zombies y una llamada a wait() o waitpid() bloqueará hasta que todos los hijos hayan terminado, y luego fallará con errno configurado en ECHILD . (El estándar POSIX original dejó el comportamiento de configurar SIGCHLD en SIG_IGN especificar. Tenga en cuenta que a pesar de que la disposición predeterminada de SIGCHLD es "ignorar", establecer explícitamente la disposición en SIG_IGN da lugar a un tratamiento diferente de los niños con proceso zombie). Linux 2.6 cumple con esto especificación. Sin embargo, Linux 2.4 (y anteriores) no: si se realiza una llamada wait() o waitpid() mientras se ignora SIGCHLD , la llamada se comporta como si no se ignorara SIGCHLD , es decir, la llamada se bloquea hasta la siguiente child termina y luego devuelve el ID de proceso y el estado de ese niño.

Note que el efecto de eso es que si el manejo de la señal se comporta como SIG_IGN está establecido, entonces (bajo Linux 2.6+) verá el comportamiento que está viendo, es decir, wait() devolverá -1 y ECHLD porque el niño habrá sido automáticamente cosechado

En segundo lugar, el manejo de la señal con pthreads (que creo que está usando aquí) es notoriamente difícil. La forma en que debe funcionar (como estoy seguro que sabes) es que las señales dirigidas al proceso se envían a un hilo arbitrario dentro del proceso que tiene la señal desenmascarada. Pero mientras que los hilos tienen su propia máscara de señal, hay un controlador de acción de todo el proceso.

Al juntar estas dos cosas, creo que te encuentras con un problema que he encontrado antes. He tenido problemas para que el manejo de SIGCHLD funcione con la signal() (que es lo suficientemente justo, ya que estaba en desuso antes de los subprocesos), que se sigaction moviéndose a sigaction y estableciendo cuidadosamente las máscaras de señal por hilo. Mi conclusión en ese momento era que la biblioteca de C estaba emulando (con sigaction ) lo que le estaba diciendo que hiciera con la signal() , pero estaba siendo interrumpido por pthreads .

Tenga en cuenta que actualmente se basa en un comportamiento no especificado . Desde la página de manual de la signal(2) :

Los efectos de la signal() en un proceso multiproceso no están especificados.

Esto es lo que te recomiendo que hagas:

  1. Mover a sigaction() y pthread_sigmask() . Establezca explícitamente el manejo de todas las señales que le interesan (incluso si cree que es el valor predeterminado actual), incluso cuando las ajuste a SIG_IGN o SIG_DFL . Bloqueo las señales mientras hago esto (posiblemente sobreabundancia de precaución pero copié el ejemplo de algún lugar).

Esto es lo que estoy haciendo (aproximadamente):

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. Siempre que sea posible, configure todos los manejadores de señales y máscaras, etc., antes de cualquier operación de pthread . Siempre que sea posible, no cambie los manejadores de señal y las máscaras (es posible que deba hacer esto antes y después de las llamadas fork() ).

  2. Si necesita un manejador de señal para SIGCHLD (en lugar de confiar en SIG_DFL ), si es posible deje que lo reciba cualquier subproceso, y use el método de SIG_DFL o similar para alertar al programa principal.

  3. Si debe tener subprocesos que no manejen ciertas señales, intente restringirse a pthread_sigmask en el subproceso relevante en lugar de a las llamadas sig* .

  4. En caso de que se tope con el siguiente problema que encontré, asegúrese de que después de tener fork() ''d, configure nuevamente el manejo de la señal desde cero (en el niño) en lugar de confiar en lo que pueda heredar del proceso de padres Si hay algo peor que las señales mezcladas con pthread, son señales mezcladas con pthread con fork() .

Tenga en cuenta que no puedo explicar exactamente por qué el cambio (1) funciona, pero ha solucionado lo que parece ser un problema muy similar para mí y, después de todo, confiaba en algo que no se había especificado anteriormente. Es lo más cercano a su ''hipótesis 2'', pero creo que es una emulación realmente incompleta de las funciones de señal heredadas (emulando específicamente el comportamiento previamente picante de la signal() que es lo que hizo que fuera reemplazada por sigaction() en primer lugar, pero esto es solo una suposición).

Por cierto, sugiero que use wait4() o (como no está utilizando rusage ) waitpid() lugar de wait3() , por lo que puede especificar un PID específico para esperar. Si tiene algo más que genera hijos (he hecho que una biblioteca lo haga), puede terminar esperando lo incorrecto. Dicho eso, no creo que eso sea lo que está sucediendo aquí.

El contexto es este problema de Redis . Tenemos una wait3() a wait3() que espera que el niño que reescribe AOF cree la nueva versión AOF en el disco. Cuando el niño wait3() , el padre recibe una notificación a través de wait3() para sustituir el antiguo AOF por el nuevo.

Sin embargo, en el contexto del problema anterior, el usuario nos notificó acerca de un error. wait3() un poco la implementación de Redis 3.0 para registrar claramente cuando wait3() devolvió -1 en lugar de fallar debido a esta condición inesperada. Así que esto es lo que sucede aparentemente:

  1. wait3() se llama cuando tenemos hijos pendientes para esperar.
  2. El SIGCHLD debe establecerse en SIG_DFL , no hay ningún código que establezca esta señal en Redis, por lo que es el comportamiento predeterminado.
  3. Cuando ocurre la primera reescritura de AOF, wait3() funciona correctamente como se esperaba.
  4. A partir de la segunda reescritura AOF (el segundo hijo creado), wait3() comienza a devolver -1.

AFAIK no es posible en el código actual que llamamos wait3() mientras no hay hijos pendientes, ya que cuando se crea el hijo AOF, configuramos server.aof_child_pid al valor del pid, y lo restablecemos solo después de un éxito wait3() llamada.

Entonces, wait3() debería tener ninguna razón para fallar con -1 y ECHILD , pero así es, por lo que probablemente el niño zombie no se haya creado por alguna razón inesperada.

Hipótesis 1 : ¿Es posible que Linux durante ciertas condiciones extrañas deseche al niño zombie, por ejemplo, debido a la presión de la memoria? No parece razonable ya que el zombi solo tiene metadatos adjuntos, pero quién sabe.

Tenga en cuenta que llamamos a wait3() con WNOHANG . Y dado que SIGCHLD está configurado en SIG_DFL de manera predeterminada, la única condición que debería llevar a fallar y devolver -1 y ECHLD debería ser un zombie disponible para informar la información.

Hipótesis 2 : Otra cosa que podría suceder, pero no hay explicación si sucede, es que después de que muere el primer hijo, el controlador SIGCHLD se establece en SIG_IGN , lo que hace que wait3() devuelva -1 y ECHLD .

Hipótesis 3 : ¿Hay alguna forma de eliminar a los niños zombis externamente? ¿Tal vez este usuario tenga algún tipo de script que elimine procesos zombie en segundo plano para que la información ya no esté disponible para wait3() ? Que yo sepa, nunca debería ser posible eliminar al zombi si el padre no lo espera (con waitpid o manejando la señal) y si el SIGCHLD no se ignora, pero tal vez haya alguna forma específica de Linux.

Hipótesis 4 : En realidad, hay un error en el código de Redis, por lo que esperamos con éxito el wait3() niño por primera vez sin restablecer correctamente el estado, y luego llamamos a la wait3() una y otra vez, pero ya no hay zombies, por lo que vuelve. -1. Analizando el código parece imposible, pero tal vez estoy equivocado.

Otra cosa importante: nunca hemos observado esto en el pasado . Solo sucede en este sistema Linux específico al parecer.

ACTUALIZACIÓN : Yossi Gottlieb propuso que el SIGCHLD sea ​​recibido por otro hilo en el proceso de Redis por alguna razón (no sucede normalmente, solo en este sistema). Ya enmascaramos SIGALRM en subprocesos bio.c , quizás también podríamos intentar enmascarar SIGCHLD de subprocesos de E / S.

Apéndice: partes seleccionadas del código redis

Donde wait3 () se llama:

/* 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 {

Partes seleccionadas de 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; }

Como puede ver, todas las rutas de código deben ejecutar el código de cleanup que restablece server.aof_child_pid a -1.

Errores registrados por Redis durante la emisión.

21353: C 29 de noviembre 04: 00: 29.957 * Reescritura de AOF: 8 MB de memoria utilizada por copia en escritura

27848: M 29 de noviembre 04: 00: 30.133 ^ @ wait3 () devolvió un error: No hay procesos secundarios. rdb_child_pid = -1, aof_child_pid = 21353

Como puedes ver, aof_child_pid no es -1.