Herramientas de usuario

Herramientas del sitio


senales

Señales

Las señales informan a un proceso cuando ha ocurrido un evento. Los eventos pueden ser síncronos (p.ej. errores) o asíncronos (p.ej. terminación de otro proceso). Un proceso tiene que realizar una acción en respuesta a la señal. Durante el tiempo entre que la señal se activa y el proceso la atiende se dice que la señal está pendiente.

Un proceso puede bloquear algunas señales, mediante una máscara de señales (signal mask). Esta máscara se hereda en caso de fork/exec.

Posibilidades de proceso de una señal:

  • Terminar el proceso.
  • Ignorar la señal.
  • Detener el proceso.
  • Reanudar el proceso.
  • Atrapar la señal mediante una función del programa.

Señales estándares:

señal significado
SIGALRM Alarma
SIGFPE División por cero
SIGHUP Hangup
SIGINT control-C
SIGKILL Terminación (no se puede atrapar)
SIGPIPE Broken Pipe
SIGTERM Terminación

Para contarle al sistema cómo se debe manejar una señal, se usa

  signal(señal, accion);

Acciones asociadas a una señal:

nombre significado
SIG_DFL Default action
SIG_IGN Ignore
Puntero a una función Se ejecuta la función con la señal como parámetro y luego se reanuda el programa

Un programa le puede enviar una señal a un proceso invocando:

  kill(pid, señal);

y para enviarse una señal a sí mismo:

  raise(señal);

Para esperar una señal:

  pause(); /* suspende hasta que llegue una señal */

Para esperar durante un número dado de segundos:

  sleep(num_segundos); /* despierta al final de ese período o cuando
                          llega una señal; retorna número de segundos restantes */

Para programar una alarma:

  alarm(num_segundos); /* genera SIGALRM en ese número de segundos */

Ejemplo: Lectura con timeout.

  /* Leer una línea del teclado dándole 10 segundos de plazo */
  #include <stdio.h>
  #undef __USE_BSD /* para que el read pueda ser interrumpido por una señal */
  #include <signal.h>
  #include <unistd.h>
        
  volatile int flag; 
        
  /* función para atrapar la señal de alarma */
  void ring() {
    flag=0;
  }
        
  /* función que lee con timeout */
  int gettext(char *buf, int bufsize, int timeout) {
    int nchars;
        
    signal(SIGALRM, ring);
    flag= 1;
    alarm(timeout);
    nchars= read(STDIN_FILENO, buf, bufsize);
    alarm(0); /* para cancelar alarma pendiente */
    if (!flag)
      nchars= 0;
    buf[nchars]= '\0';
    return nchars;
  }
        
  #define MAXLINEA 100
  int main() {
    char linea[MAXLINEA+1];
        
    printf("Escriba su nombre: ");
    fflush(stdout);
    if (gettext(linea, MAXLINEA, 10)>0)
      printf("Gracias %s", linea);
    else
      printf("*** TIMEOUT ***\n");
    return 0;
  }

Señales y setjmp/longjmp

Supongamos que queremos llamar una función, pero si la función toma más tiempo que digamos 2 segungos queremos abortar la llamada. Para ello configuramos el timer, ¿pero cómo detenemos la función? La solución es usar longjmp desde la rutina de atención de la señal:

#include <unistd.h>
#include <signal.h>
#include <setjmp.h>
#include <stdio.h>

/*
 * Ejemplo de uso de signal y longjmp para implementar un timeout
 * Esta implementacion usualmente solo funciona para la primera invocacion
 */

static jmp_buf ring;

void clock(int sig)
{
    longjmp(ring, 1);
}

int call_with_timeout(int (*f)(void *), void *p, int timeout)
{
    int res;
    void (*hdlr)();

    hdlr = signal(SIGALRM, clock);

    if (setjmp(ring) != 0) {
        signal(SIGALRM, hdlr); /* si llegamos aqui es que ocurrio el timeout */
        fprintf(stderr, "timeout!\n");
        return 1;
    }

    alarm(timeout); /* programamos el timer */
    res = f(p);
    printf("exito!\n");
    alarm(0);  /* apagamos el timer */

    signal(SIGALRM, hdlr);
    return(res);
}

int fun(void *p) {
  sleep(2);
  return 0;
}

int main() {
  call_with_timeout(fun, NULL, 1);
  call_with_timeout(fun, NULL, 1);
  return 0;
}

Este código funciona en Linux, pero debido a un problema de estandarización, podría no funcionar en algunos Unix. El problema es que en algunos Unix la rutina de atención del timer (ejemplo: clock) se llama con la señal SIGALRM deshabilitada. Al retorno de clock se vuelve a activar. Pero si se usa longjmp, la señal queda deshabilitada y por lo tanto la señal de un segundo timeout podría nunca recibirse.

Este es el caso de Linux, pero afortunadamente hay otro aspecto que evita el problema el Linux. La función setjmp graba el estado de las señales que están activas/deshabilitas y longjmp restaura el mismo estado. En buenas cuentas longjmp activa nuevamente la señal SIGALRM y por eso funciona. Pero esto tampoco es estándar en todos los Unix. Sin embargo sí es estándar que las funciones sigsetjmp y siglongjmp graban el estado de las señales o no de acuerdo a un parámetro de sigsetjmp. Entonces lo más correcto es usar estas funciones en el código de más arriba:

  #define _POSIX_C_SOURCE 1

  void clock(int sig) {   
    siglongjmp(ring, 1);
  }
  
  ...
  
  int call_with_timeout(int (*f)(void *), void *p, int timeout) {
    ...
    if (sigsetjmp(ring, 1) != 0) {
      ...
    }
    ...
  }

Es necesario definir la macro _POSIX_C_SOURCE porque no es una función estándar de ansi-C. Pero el estándar POSIX sí la define.

Discusión

Otro punto importante que no se debe olvidar es lo que pasa con los recursos solicitados por la función. ¿Qué pasa con la memoria pedida o los archivos abiertos? Estos recursos no se liberarán jamás y por lo tanto se transforman en goteras. Si se va a emplear este enfoque, hay que mantener una traza de todos los recursos pedidos para así liberarlos cuando se cumpla el timeout. Esto no es trivial de hacer.

También hay que considerar que la invocación de la rutina que atiende la señal puede provocar dataraces. Por ejemplo supongamos que la función f usa malloc. La señal del timeout puede gatillarse justo a la mitad del malloc y por lo tanto el heap que maneja malloc queda en un estado inconsistente. Si la función que atiende la señal también invoca malloc, se puede producir una inconsistencia que gatille un segmentation fault o que se entregue 2 veces el mismo pedazo de memoria. Por lo tanto las acciones de la rutina de atención de la señal deben ser simples como asignar una variable global por ejemplo.

sigaction/sigprocmask

La función signal es la primera función que existió en Unix para atrapar señales. Hoy se prefiere usar la función sigaction para tener más control sobre donde y cómo se debe ejecutar la rutina de atención. Por ejemplo:

  • si la señal se deshabilita o no mientras se ejecuta la rutina de atención
  • en que pila se ejecuta la señal
  • si la señal vuelve a su estado por omisión una vez que se gatilla la señal
  • etc.

Del mismo modo, sigprocmask permite deshabilitar/activar señales explícitamente. Esta función es de utilidad por ejemplo para resolver el datarace asociado al uso de malloc tanto en el código en donde puede ocurrir una señal como en la función que atiende esa señal. La solución está en invocar sigprocmask antes de invocar malloc para inhibir la señal en cuestión e invocar nuevamente sigprocmask para reactivar nuevamente la señal. De esta forma si se gatilla la señal mientras se ejecuta malloc, la señal quedará pendiente y solo se invocará la rutina de atención cuando se reactive la señal en la segunda llamada de sigprocmask.

Consulte la página del manual para averiguar más sobre estas funciones.

Ejercicio

Resuelva la parte c de la pregunta 1 del control 3 de 2014/2. Pruebe su solución con el archivo largo.zip. En este problema la variable nodo podría apuntar a una dirección inválida. Si trata de acceder a nodo→prox se gatillará la señal SIGSEGV que normalmente termina el proceso. Para evitar que el proceso termine, Ud. debe capturar la señal SIGSEGV. La dificultad está en que si la rutina que atiende la señal retorna, se volverá a hacer el acceso inválido. Para eludir el acceso inválido Ud. debe usar sigsetjmp en la función largo e invocar siglongjmp en la rutina que atiende la señal.

senales.txt · Última modificación: 2016/11/24 09:13 por lmateu