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
enSIG_IGN
. POSIX.1-2001 permite esta posibilidad, de modo que ignorarSIGCHLD
se puede usar para evitar la creación de zombies (verwait
(2)). Sin embargo, los comportamientos históricos de BSD y System V para ignorarSIGCHLD
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ñalSIGCHLD
y realizar unawait
(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 enSIG_IGN
o el indicadorSA_NOCLDWAIT
se establece enSIGCHLD
(versigaction
(2)), entonces los hijos que terminan no se convierten en zombies y una llamada await()
owaitpid()
bloqueará hasta que todos los hijos hayan terminado, y luego fallará con errno configurado enECHILD
. (El estándar POSIX original dejó el comportamiento de configurarSIGCHLD
enSIG_IGN
especificar. Tenga en cuenta que a pesar de que la disposición predeterminada deSIGCHLD
es "ignorar", establecer explícitamente la disposición enSIG_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 llamadawait()
owaitpid()
mientras se ignoraSIGCHLD
, la llamada se comporta como si no se ignoraraSIGCHLD
, 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:
- Mover a
sigaction()
ypthread_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 aSIG_IGN
oSIG_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);
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 llamadasfork()
).Si necesita un manejador de señal para
SIGCHLD
(en lugar de confiar enSIG_DFL
), si es posible deje que lo reciba cualquier subproceso, y use el método deSIG_DFL
o similar para alertar al programa principal.Si debe tener subprocesos que no manejen ciertas señales, intente restringirse a
pthread_sigmask
en el subproceso relevante en lugar de a las llamadassig*
.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 confork()
.
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:
-
wait3()
se llama cuando tenemos hijos pendientes para esperar. - El
SIGCHLD
debe establecerse enSIG_DFL
, no hay ningún código que establezca esta señal en Redis, por lo que es el comportamiento predeterminado. - Cuando ocurre la primera reescritura de AOF,
wait3()
funciona correctamente como se esperaba. - 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.