Archivo por días: 11 abril, 2010

Trabajando con señales

Estos días he seguido trasteando con Bftpd, y me puse a trabajar en resolver un problema bastante molesto: la cantidad de procesos zombie que deja tras de sí.

Para entender lo que pasa, primero hay que explicar que Bftpd lanza un nuevo proceso por cada nuevo usuario que se conecta, en lugar de utilizar un solo proceso para gestionar todas las conexiones. Así, tenemos un proceso padre que se limita a escuchar, y cada vez que llega una petición de conexión crea un proceso hijo para que la gestione; también comprueba cuando un hijo se muere (porque se haya cerrado la conexión).

Y aquí es de donde surge el problema: cada vez que un hijo termina, emite una señal (en concreto SIGCHLD) hacia el padre, el cual, al recibirla, ejecutará una pequeña función o callback y luego seguirá su ejecución normal. En el caso de Bftpd, dicho callback comprueba qué hijo es el que se ha muerto, pide su valor de retorno (para evitar que se quede zombie) y libera una serie de recursos que le reservó. En concreto, el código de dicha función comienza con un:

pid = wait(NULL);
[codigo para liberar los recursos del proceso PID]

Hasta aquí todo parece correcto; por desgracia, después de cada sesión con Bftpd quedaban varios procesos zombie. La razón era, obviamente, que el proceso padre no había leído su valor de retorno, por lo que el sistema operativo no los podía hacer desaparecer; sin embargo, en el callback se lee siempre dicho valor, así que es obvio que algo raro estaba pasando.

Para depurar el código empecé por añadir unos printfs: uno en el punto en que se crean los procesos hijo, mostrando su PID; otro en el punto de finalización de dichos hijos, y otro en la función de callback de SIGCHLD. Ejecuté el servidor, hice unas cuantas operaciones, vi la salida por pantalla, y… ¡Sorpresa! ¡Había menos llamadas a la función de callback que muertes! Si todo funcionase como se esperaba, el número debería ser exactamente el mismo (cada muerte debería emitir una señal SIGCHLD, la cual haría que se ejecutase el callback). Sin embargo, por alguna misteriosa razón, algunos procesos morían sin emitir la señal.

Para aquellos a los que les de igual el por qué, y simplemente necesiten saber el como, daré primero la solución: se trata de leer los PIDs de todos los hijos muertos en cada llamada al callback, en lugar de leer el de uno sólo. En otras palabras, el código del callback para SIGCHLD debe ser:

do {
        pid = waitpid(-1,NULL,WNOHANG);
        if (pid>0) {
            [codigo para liberar los recursos del proceso PID]
        }
} while (pid>0);

Vale, pero ¿por qué ocurre esto?

Para entender lo que ocurre hay que irse un poco a las profundidades del núcleo, porque se debe a un problema de cómo están implementadas las señales.

En Linux, cada proceso tiene un conjunto de bits, y cada uno representa una señal. Cuando un proceso quiere enviar una señal a otro, lo que hace realmente es poner a uno el bit de dicha señal en el proceso receptor.

¿Y cuando comprueba el receptor la llegada de una señal? En cada cambio de contexto: en un sistema operativo multitarea, los procesos se van turnando en el uso de la CPU, de manera que primero la usa unos milisegundos el proceso A, luego otros milisegundos el proceso B, y así sucesivamente hasta que se acaba la lista y se vuelve al proceso A. Cada vez que se cambia de un proceso al siguiente se realiza un cambio de contexto, en el que primero se guarda el estado del proceso actual, se busca quien será el siguiente proceso, se carga su estado y se le cede el control. Pero, y aquí está la cuestión, antes de este último paso se comprueba la máscara de bits de las señales, y si alguna está activa se ejecutará primero el callback correspondiente.

¿Y por qué se pierden entonces señales SIGCHLD? Pues porque dos o más procesos se mueren «a la vez»; esto es, se mueren dentro del mismo intervalo entre dos ejecuciones del proceso padre. Así, lo que ocurre es que cuando se muere el primero, pone a 1 el bit de la señal SIGCHLD del proceso padre; el repartidor de tareas, al ver que ha muerto, libera todo lo que puede y pasa al siguiente proceso; éste muere también, y también pone a 1 el bit de la señal SIGCHLD del proceso padre… pero ese bit ya estaba a uno, por lo que se queda como está. Finalmente, el repartidor decidirá que es el momento de ejecutar de nuevo el proceso padre, pero antes verá que el bit de la señal SIGCHLD está activo, por lo que lo pondrá a cero y  llamará al callback. El resultado: se murieron dos procesos pero sólo se ejecutó una vez la función asociada a la señal, por culpa de que las señales se almacenan con un único bit.

Es por esto que la solución indicada arriba funciona: la señal nos indica que se ha muerto AL MENOS un proceso hijo, así que debemos comprobar todos, y no asumir que cada uno enviará una señal.

Este caso nos demuestra que las señales son un sistema de comunicación bastante frágil, por lo que no se debe abusar de él. Cosas como «contar el número de veces que llega una señal» o similares pueden dar problemas incluso si hay un único emisor, porque si emite varias veces la misma señal antes de que el padre recupere el control de la CPU, contarán como una única señal. Y como muestra para los incrédulos, un pequeño programita que ejemplifica todo esto:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>

int v;

void senal(int valor) {

 printf("Recibidan");
 v++;
}

int main(int argc, char **argv) {

 int pid1,pid2,pid3,pid4,loop;

 v=0;
 signal(SIGUSR1,senal);
 pid1=getpid();
 pid2=fork();
 if(pid2==0) {
 fork();
 fork();
 printf("hijon");
 kill(pid1,SIGUSR1);
 kill(pid1,SIGUSR1);
 kill(pid1,SIGUSR1);
 kill(pid1,SIGUSR1);
 kill(pid1,SIGUSR1);
 sleep(2);
 } else {
 printf("padren");
 for(loop=0;loop<21;loop++) {
 sleep(1);
 }
 printf("Total: %dn",v);
 }
}

Este código crea un primer hijo, el cual se divide en cuatro hijos en total, y cada uno emite, de golpe, cinco señales SIGUSR1 al padre. Sin embargo, al ejecutarlo veremos que nunca se detectan todas ellas, sino, como mucho, una por proceso hijo (y a veces ni eso).