Administración de señales

Que tal, Padawans! La programacion en un lenguaje es generalmente parecida entre plataformas. O sea, los for(;;) en lenguaje C van a funcionar igual tanto en GNU/Linux como en 'otras plataformas', exceptuando, claro esta, la performance.

Por supuesto, cada sistema operativo tiene sus peculiaridades, y tambien las cosas que le son propias segun su diseño inherente.

A que me refiero con esto? Muy simple. Un compilador de C para MS-DOS no tendria en cuenta funciones para manejar procesos, o multitarea. En cambio, en un kernel como el de Linux, se tienen que tener en cuenta funciones para crear procesos, enviar mensajes entre procesos, compartir regiones de memoria, manejar permisos de usuarios y grupos, obtener informacion que solo el sistema de archivos Unix puede brindar y asi un largo etc, etc.

En este articulo me gustaria comentarles sobre como trabajar con algunas de las funciones que son propias de sistemas Unix y derivados, como GNU/Linux. Vamos a empezar por las...

Señales

El concepto de señal es bastante simple, aunque su implementacion, dependiendo del caso, puede tornarse un poco difusa, ya que se utilizan en muchos ambitos. El concepto es muy simple: una señal es un 'aviso' que recibe un proceso en ejecucion.

Este aviso puede indicar muchisimas cosas, y otros son directamente utilizados por el administrador de procesos, y no por el proceso en cuestion. Saben a que me refiero? No?

Piensen lo siguiente: cuando ustedes utilizan el comando 'kill -9' seguido de un numero identificador de proceso (PID obtenido de "ps ax", etc), estan enviando una señal numero 9, que corresponde al nombre "SIGKILL" (Kill Signal, señal o Aviso de la Muerte, muehehehehe). El programa recibe esta señal pero no puede administrarla, es decir, no puede elegir si ignorarla o hacer algo especifico al momento de recibirla. Es una señal no-atrapable.

En cambio, recordaran que para que algunos programas vuelvan a leer su archivo de configuracion, a veces se les puede enviar la señal SIGHUP (Hang-Up Signal, el nombre ya no tiene mucho sentido, se utilizaba cuando uno se conectaba via modem a un sistema Unix y el getty recibia esta señal cuando cortabamos la comunicacion. Por eso "Hang-Up", accion de "colgar el tubo").

Esta señal si es atrapable, digamos, y el programador utilizo una serie de funciones para definir que accion realizar al recibir dicha señal, en este caso, volver a leer la configuracion del programa, y reiniciarse.

Para ir cerrando el concepto, les cuento que las unicas dos señales que no se pueden "atrapar" son SIGKILL (9) y SIGSTOP (19). En /usr/include/bits/signum.h y en /usr/include/asm/signum.h pueden encontrar la definicion de cada señal y su valor numerico. Por otra parte pueden utilizar el parametro "-l" de "kill", para obtener esta misma lista.

FUNCIONES y EJEMPLOS

La primer funcion que vamos a usar cuando queramos trabajar con señales es la llamada al nucleo Linux llamada "signal". Esta funcion se encuentra en la seccion 2 de las paginas del manual, y es conveniente consultarla ("man 2 signal"). La funcion signal() tiene el siguiente prototipo:

sighandler_t signal(int signum, sighandler_t handler);

Como pueden ver, toma 2 parametros. El primero es la señal que queremos administrar ("SIGHUP", "SIGUSR2", etc). El segundo puede tomar 3 valores:

  • SIG_IGN: Ignorar la señal, no hacer nada al recibirla.
  • SIG_DFL: Ejecutar la accion por defecto para dicha señal. Esto podemos conocerlo por "man 7 signal".
  • El tercer valor se refiere a una funcion definida por el usuario.

Veamos un caso donde querramos ignorar la señal SIGHUP:

signal (SIGHUP,SIG_IGN);

Ahora veamos un caso donde querramos ejecutar una funcion definida por nosotros. El siguiente codigo fuente es completo. Pueden cargarlo en un editor de texto, y luego compilarlo con gcc. Al ejecutarlo el programa esperara la señal SIGUSR2. Por cada vez que la reciba, mostrara un mensaje en pantalla, con un contador que indicara cuantas veces la recibio. Para finalizar el programa debemos enviarle cualquier señal de "muerte": SIGSTOP, SIGKILL, SIGTERM. Presten atencion a como se define la funcion que actuara al recibirse la señal SIGUSR2! Es muy importante.

Veamos el codigo:

#include  // printf()
#include  // signal()
#include  // wait()
#include  // wait()

/* prototipo de la funcion administradora *

void administrador (int que_signal);

int
main ()
{
  signal (SIGUSR2, administrador);

  pause();

}

void
administrador (int que_signal)
{
  static int cuenta_usr2 = 0;
/* deshabilito la señal recibida 
 mientras ejecuto el codigo */
  signal (que_signal, SIG_IGN);

/* determino que señal se recibio 
y actuo acordemente */

  switch (que_signal)
    {
    case SIGUSR2:
      {
	printf ("\nSIGUSR2 %d veces.\n",++cuenta_usr2);
	break;
      }
    }

/* re-habilito la administracion 
de la señal */
  signal (que_signal, administrador);
}

Como pueden ver, la funcion administrador se define como void, y toma un solo parametro de tipo entero. Este parametro indica que señal se recibio, ya que podemos usar un mismo administrador para varias señales. Utilizando el switch() podemos discriminar que señal fue recibida. Otra funcion que aparece aqui es wait(), usada dentro de un bucle for infinito.

Lo que hace esta funcion es esperar hasta que se reciba una señal. O sea, es una especie de sleep(), pero que espera indefinidamente hasta que se reciba la señal, y no una cierta cantidad de segundos. Si solo hubiera usado el for(;;); hubieramos utilizado muchos mas recursos del microprocesador, malgastando asi ciclos de procesamiento. Esto lo podemos verificar utilizando el comando 'top'. Modifiquen el codigo como les parezca. Luego se utiliza una variable entera de tipo estatica, o sea, cuyo valor no cambia de una llamada a la funcion a otra. Los parametros que toma wait() son opcionales, y es por eso que utilice "NULL". Para mas detalles, revisen man 2 wait.

Otra funcion que es muy importante conocer es alarm(). Si alguna vez hicieron un programa en Delphi o, dios nos libre, Visual Basic, habran utilizado seguramente el famoso control de temporizador "Timer". El Timer lo que hacia era recibir un evento propio cada una cierta cantidad de tiempo. La funcion alarm() nos provee exactamente la misma funcionalidad. Su prototipo es el siguiente:

unsigned int alarm(unsigned int seconds);

La funcion alarm nos devolvera la cantidad de segundos que faltan para que se ejecute alguna alarma previamente establecida, o cero si no habia ninguna. Si se utiliza un valor de 'seconds' (segundos) mayor que cero (vean que es un 'unsigned int', entero sin signo, o sea solo positivo o cero), entonces dentro de la cantidad de segundos que ustedes indiquen, se recibira una señal SIGALRM (SIGALRM, y no SIGALARM!).

Si hubiera una alarma ya establecida, se cancela y se utiliza la nueva. Si usamos 0 como valor de seconds, entonces no se establecera ninguna nueva alarma. Requiere que incluyamos unistd.h. ("#include "). Ejemplito:

alarm (5);

Dentro de 5 segundos se recibira una señal SIGALRM. Modifiquen el programa de ejemplo que les di. Podemos utilizar signal(SIGALRM,administrador) y agregar el caso apropiado al switch().

Tambien tenemos una funcion que nos permite enviarle al proceso una señal que nosotros especifiquemos. Esta funcion se llama raise(). Un "man 3 raise" podria ser util... pero no lo es tanto, ya que no toma mas que un solo parametro. Requiere "signal.h". Veamos el prototipo:

int raise(int sig);

Por ejemplo, si usamos raise(SIGKILL), mataremos a nuestro propio programa... desde el programa mismo! Es un suicidio utilizando Lenguaje C, jaja!

Es interesante usar raise cuando tenemos un complejo administrador de señales, y queremos ejecutar el codigo correspondiente a cierta señal. Por ejemplo, podriamos usar signal() para agarrar la señal SIGUSR1, y que el codigo escribiera en un archivo la cadena indicada por cierto puntero de alcance global. De tal forma, podriamos cambiar el contenido de la cadena, hacer raise(SIGUSR1), y asi ahorrarnos el tipear otra funcion (o externalizar la existente del administrador). Obviamente, todo depende del disenio del programa, y de la logica que quiera aplicar el programador.

Como lo dice la pagina del manual, raise(sig) es equivalente a kill(getpid(),sig). Esto como se interpreta? Muy simple: Enviar la señal 'sig' al proceso indicado por getpid(). getpid() es una funcion que devuelve el Identificador de Proceso (PID) del proceso que la utiliza. O sea, apunta a uno mismo, digamos. Podemos revisar su pagina del manual en la seccion 2. getpid() requiere "sys/types.h" y "unistd.h". Veamos su prototipo:

pid_t getpid(void);

Como podemos ver, no toma ningun parametro. Es una funcion puramente informativa.

Para ir finalizando con el contenido de este articulo, veamos la funcion kill(). Esta funcion envia la señal que indiquemos al proceso o grupo de procesos que indiquemos. Para poder enviarle una señal a un proceso, debemos ser duenios del proceso en cuestion o tener privilegios de root. O sea, nuestro programa debe haber generado dicho proceso (debe ser un hijo/child). Requiere "sys/types.h" y "signal.h". Veamos el prototipo:

int kill(pid_t pid, int sig);

El parametro sig es la señal a enviar ("SIGUSR2", por ejemplo). El parametro pid actua diferente segun su valor. Veremos solo dos de estos valores en este articulo, por cuestiones de espacio:

Si pid es un numero mayor que cero (o sea, un numero PID normal), se envia la señal especificada por 'sig' a dicho proceso. Asi es como funciona el raise(). Envia la señal indicada a si mismo.

Y si pid es igual a cero, entonces la señal indicada por sig se envia a todos los procesos del grupo actual. Se define como grupo a los padres e hijos derivados de un proceso padre de mayor jerarquia.

En el proximo articulo seguimos con manejo de procesos, hijos y las señales que intervienen en esto, como asi tambien algo de IPC. Se las dejo picando!

Saludos