Señales (signals)

Introducción
    Las señales son interrupciones generadas por software (esto en analogías a las interrupciones generadas por el hardware, como lo debe haber estudiado en estructuras de computadores).

   Las señales permiten el manejo de eventos asíncronos. Situaciones en que intervienen señales: al suspender un proceso usando Control-z, al terminar algunos con Control-c, al recibir aviso de temporizador expirado (más abajo), al recibir aviso de proceso hijo terminado, etc.


   Resumen: Proceso 1 envía señal llamando a función kill() u ocurre evento generador de señal, como resultado el proceso receptor de la señal (sea P2) efectúa una acción. Esta acción puede ser: poner término a P2, invocar a función por omisión, o invocar una función definida por el programador. 

Conceptos
     El archivo < signum.h > contiene la definición de todas las señales. Cada señal tiene un nombre. Ellas comienzan con SIG. Ej. SIGALRM (señala temporizador expirado)
    La ubicación definitiva de este archivo depende del sistema operativo de su máquina. Generalmente se puede llegar a él vía /usr/include/signal.h. En Aragorn este archivo está en (2026): /usr/include/bits/signum.h
    Una forma de ubicar este archivo es usar: $ locate signum.h

    Numerosas condiciones pueden generar señales, entre ellas:

    Ante la llegada de una señal, hay tres acciones que un programa puede solicitar al kernel (kernel es el núcleo del sistema operativo):
    1.     Ignorar la señal. Sin embargo hay dos señales que no pueden ser ignoradas: SIGKILL y SIGSTOP.
    2.     Capturar la señal. Cuando la señal ocurre, el kernel llama una función definida por el programa.
    3.     Permitir que se aplique la acción por defecto. Esta puede ser: ignorar la señal, terminal el proceso (como con Control-c), terminar con un core dump, o suspender el proceso (Control-z).
      Algunas señales:
    SIGALRM: generada cuando el timer asociado a la función alarm expira. También cuando el timer de intervalo es configurado para medir tiempo real (función setitimer)
    SIGCHLD: Cuando un proceso termina o es parado (por ejemplo al enviar señal de SIGSTOP), el proceso envía esta señal a su padre. Por omisión esta señal es ignorada. Ante su llegada y en la función que maneja la señal, el proceso padre puede invoca la función wait para obtener el estado de término del proceso hijo. Se evita así la creación de procesos "zombies".
    SIGCONT: es enviada para reanudar un proceso que ha sido detenido (suspendido) con SIGSTOP.

    SIGINT: generada con Control-c
    SIGKILL: Permite terminar un proceso.
    SIGTSTP: generada cuando presionamos Control-z. Puede ser ignorada.
    SIGSTOP: similar a SIGTSTP pero no puede ser ignorada o capturada.
    SIGUSR1: Es una señal definida por el usuario para ser usada en programas de aplicación.
    SIGUSR2: Otra como la anterior.
    Para probar las señales se recomienda usar
% kill -SIG??? <pid>   
No use los números de determinado sistema, pues éstos han cambiado en el tiempo.
Función signal: Permite registrar la función que atenderá la llegada de la señal.

    #include <signal.h>
    void (*signal (int signo, void (*func) (int)))(int);

Este prototipo es equivalente a:
typedef    void Sigfunc(int);
Sigfunc * signal (int signo, Sigfunc *);

signo: es el número (mejor la constante) de la señal.
func: puede ser la constante SIG_IGN para ignorar la señal, SIG_DFL para usar la acción por omisión, o puede ser una función definida en el programa. El valor retornado es un puntero al manejador previamente instalado para esa señal. Guarde este valor si necesita re-establecer la función que originalmente atendía la señal.

Ejemplo: uso de SIGUSR1 y SIGUSR2

Interrupción de llamados al sistema
    Cuando llega una señal, ésta es atendida por su manejador. En éste se debe poner cuidado de no invocar funciones no re-rentrantes. Éstas son las que no permiten su re-invocación antes de su término; por ejemplo strtok. Esto se explica pues el proceso que recibe la señal pudo estar ejecutando una de tales llamadas y si el manejador la usa, generalmente se obtiene un resultado errado.

Cuando llega una señal a un proceso bloqueado, por omisión éste sigue bloqueado luego de atendida la señal, ver read1.c . En ocasiones deseamos que el proceso salga del bloqueo, por ejemplo, para suspender una entrada de datos cuando ésta se demora mucho. Hay formas de desbloquear un proceso ante la llegada de una señal, ver ejemplo readInterrupted.c.

Un ejemplo de llegada de señales a un proceso bloqueado se presentó en la tarea "rapidez aritmética". Ver soluciones de la época de Daniel Durán y Gonzalo Vásquez. Para entender estas soluciones es necesario estudiar pipes, un mecanismo de comunicación entre procesos.

Funciones kill y raise
    kill envía una señal a un proceso o a un grupo de procesos.
    #include <sys/types.h>
    #include <signal.h>
    int kill (pid_t pid, int signo);

    raise permite enviar señales al mismo proceso (hacia uno mismo).
    int raise(int signo);

    Este último es como kill( getpid(), signo);

Funciones alarm, setitimer, timer_create y pause
alarm es el mecanismos más imple. setitimer le sigue. El más portable (POSIX) es timer_create pero es más complejo de usar.
    La función alarm permite especificar un timer que expire en un tiempo dado.

    #include <unistd.h>
    unsigned int alarm(unsigned int seconds);

    int setitimer(int which, const struct itimerval * new_value, struct itimerval *old_value); /* permite programar un temporizador que, al expirar, genera una señal determinada según el tipo de temporizador seleccionado. */

Existen tres tipos de temporizador en setitimer según el valor de which:
ITIMER_REAL: cuenta tiempo real y genera SIGALRM.
ITIMER_VIRTUAL: cuenta solamente el tiempo que el proceso está ejecutando código de usuario y genera SIGVTALRM.
ITIMER_PROF: cuenta tiempo de ejecución de usuario y kernel y genera SIGPROF.

Actualmente se recomienda usar la función del API POSIX
    int timer_create(clockid_t clockid,
struct sigevent *sevp,
timer_t *timerid); /* Cuando el temporizador expira, puede notificar al proceso mediante una señal o mediante otros mecanismos definidos por POSIX. */

Una vez creado, este timer debe es configurado con la función timer_settimer(). Ver man timer_create y timer_settimer por más detalles. La principal ventaja de este mecanismo es

    La función pause suspende al proceso llamador hasta que llega cualquier señal.
    int pause(void); /* retorna -1 con errno en EINTR */

    El control del tiempo no es exacto por la incertidumbre del kernel, carga del sistema, etc.
    Hay sólo una alarma por proceso!
    La alarma se puede cancelar con seconds=0 en el argumento. El valor retornado es el tiempo restante para la expiración del temporizador.

    Ejemplo 1 de uso de alarma: sleep() vía programa .
    Explicar por qué esta función tiene los siguientes problemas: ¿Qué pasa si el llamador ya tiene la alarma configurada?. ¿Que pasa si el programa tenía otro manejador definido previamente? ¿hay alguna carrera crítica?

    Ejemplo 2 de uso de alarma, llamado a read con timeout: Versión de prueba de read reiniciado (read es retomado luego de recibir una señal), Versión para tener read no reiniciado (read no continúa luego de recibir una señal).

Función sleep
    sleep suspende al proceso llamador por la una cantidad de segundos indicada o hasta que se reciba una señal.
    #include <unistd.h>
    unsiged int sleep (unsigned int seconds);

    Hoy también tenemos disponible
    unsiged int usleep(unsigned int microseconds);

    Retorna 0 o -1 en caso de error.      

    También se dispone hoy de:
    #include <time.h>
       int nanosleep(const struct timespec *req, struct timespec *rem);