Una señal es un "aviso" que puede enviar un proceso a otro proceso. El sistema operativo unix se encarga de que el proceso que recibe la señal la trate inmediatamente. De hecho, termina la línea de código que esté ejecutando y salta a la función de tratamiento de señales adecuada. Cuando termina de ejecutar esa función de tratamiento de señales, continua con la ejecución en la línea de código donde lo había dibujado.
El sistema operativo envía señales a los procesos en determinadas circunstancias. Por ejemplo, si en el programa que se está ejecutando en una shell nosotros apretamos Ctrl-C, se está enviando una señal de terminación al proceso. Este la trata inmediatamente y sale. Si nuestro programa intenta acceder a una memoria no válida (por ejemplo, accediendo al contenido de un puntero a NULL), el sistema operativo detecta esta circunstancia y le envía una señal de terminación inmediata, con lo que el programa "se cae".
Las señales van identificadas por un número entero. No vamos a detallar todas, pero sí alguna de las más interesantes para el programador.
Nombre de señal | Nº | Propósito. Se envia al proceso cuando... |
SIGINT | 2 | ... se pulsa Ctrl-C |
SIGFPE | 8 | ... hay un error en coma flotante (ejemplo, división por cero) |
SIGPIPE | 13 | ... se intenta escribir en una conexión
(socket,
tubería,
...) rota (no hay proceso leyendo al otro lado). |
SIGALRM | 14 | ... cuando termina un temporizador. |
SIGUSR1 | 16 | ... el programador lo decide. Esta señal es
para uso
del programador. No la utiliza el sistema operativo. |
SIGUSR2 | 17 | ... el programador lo decide. Idem a la anterior |
En Senhales.txt tienes un cacho del fichero .h del sistema en el que están todas las posibles señales.
Nosotros, desde un shell de comandos unix, podemos enviar señales a los procesos con el comando kill. Su uso más conocido es el kill -9 idProceso, que envía una señal 9 al proceso, haciendo que termine. También es posible enviar señales desde un programa en C a otro programa en C que esté corriendo en el mismo ordenador.
Todas las señales tienen una función de tratamiento por defecto. Cuando nuestro programa está en ejecución, si recibe una señal, saltará a la función por defecto para tratamiento de esa señal y luego continuará su ejecución normal (si la señal no es de terminación). Todo esto es totalmente invisible para nosotros. Únicamente lo notaremos si nuestro programa se sale inesperadamente.
Sin embargo, nuestro programa puede cambiar casi todas las funciones por defecto de tratamiento de señales, pudiendo hacer que al recibir una señal el programa haga lo que nostros queramos.
Como ejemplo, vamos a hacer un programita que capture la señal que se envía cuando se pulsa Ctrl-C y la ignore.
La función C que nos permite redefinir la función de tratamiento de señal es signal(). Esta función admite dos parámetros:
En nuestro código, para reemplazar la función de tratamiento del Ctrl-C, pondremos:
void controlador (int);
...
signal (SIGINT, controlador);
¡Ya está!. Cuando alguien pulse Ctrl-C sobre nuestro programa, se llamará a nuestra función controlador().
Un pequeño detalle. En algunos sistemas unix, cuando se recibe una señal y se ejecuta nuestra función, se restaura automáticamente la función por defecto. Por ello, a veces es necesario que nuestra función termine volviendo a ponerse ella misma como tratamiento de la esa señal.
También podemos hacer que una señal se ignore, es decir, que no la trate nadie, ni nuestra función ni la de por defecto. Para ello hay que poner
signal (numeroSenhal, SIG_IGN);
Para restaurar el tratamiento por defecto de la señal
signal (numeroSenhal, SIG_DFL);
En ctrlc.c tienes el código de ejemplo. Lo puedes descargar, quitarle la extensión .txt y compilarlo con make ctrlc. Al ejecutarlo, la primera vez que pulses Ctrl-C no te hará caso y a la segunda saldrá normalmente.
La función pause() hace que el proceso quede suspendido hasta que llegue una señal.
Un proceso puede enviarle señales a otro. La función para ello es kill(). Admite dos parámetros:
Las alarmas las genera la función alarm(). Una vez llamada esta función, pasandole como parámetro un tiempo en segundos, se inicia una cuenta atrás de esa duración. Una vez terminada, se envía al proceso una señal SIGALRM, que se puede capturar. De esta forma podemos, por ejemplo, mantener en pantalla actualizado un reloj o mostrar una información durante un tiempo determinado y luego borrarla.
Para tiempos más finos, tenemos la función setitimer(), algo más compleja, pero que permite definir tiempos de micro-segundos. Esta función permite además que se nos avise repetidamente a intervalos de tiempo fijos.
Puesto que la función alarm() es más sencilla, vamos a hacer un ejemplo con la funcion setitimer(). El mecanismo es exactamente igual para las dos, pero setitimer() lleva unos parámetros más complejos.
Unix proporciona tres contadores de tiempo distintos para cada proceso.
La función setitimer() admite tres parámetros:
struct itimerval
{
struct timeval it_interval;
struct timeval it_value;
};
En la primera estructura it_interval se indica cada cuanto queremos recibir la señal. Es decir, si queremos actualizar un reloj en pantalla con milésimas de segundo, en este campo debemo meter una milésima de segundo. Cada milésima de segundo nos enviará una señal SIGALRM. En la segunda it_value, se indica cuándo queremos recibir el primer aviso. Es decir, si it_value es 10 segundos e it_interval es 1 segundo, recibiremos una primera señal a los 10 segundos y a partir de ahí, cada segundo.
Como vemos, estos dos campos son a su vez otras dos estructuras. Estas son
struct timeval
{
long tv_sec;
long tv_usec;
};
El campo tv_sec indica tiempo en segundos. El campo tv_usec indica tiempo en microsegundos. Combinando estos dos campos podemos, por ejemplo, hacer tiempos de 2 segundos y 3 centésimas (tv_sec=2 y tv_usec=30000).
Bueno, ya estamos en disposición de ver el programita de ejemplo, pruebatimer.c. En él se lanza un contador de tiempo real con setitimer(), de forma que la primera vez salte a los 2 segundos y luego cada medio segundo. Se pone una función tratamientoSenhal() para tratar la señal SIGALRM que salta cada vez que el contador termina. Esta funcíón simplemente escribe un texto en pantalla.
Puedes bajarte el fichero, quitarle la extensión .txt y compilarlo con make pruebatimer. El ejecutarlo verás un texto a los dos segundos y luego se repite cada medio.