Tema 7. Señales y Funciones de Tiempo.

1. CONCEPTO DE SEÑAL

Las señales son interrupciones que pueden ser enviadas a un proceso para informale de algun evento asíncrono o situación especial, se emplea tambien para referirse al evento.

Los procesos pueden enviarse señales unos a otros a través de la llamada kill. Cuando un proceso recibe una señal puede reaccionar de tres formas distintas:

  1. Ignorar la señal.
  2. Invocar a la rutina de tratamiento por defecto. Esta rutina no la codifica el programador, sino que la aporta el núcleo. Según el tipo de señal, la rutina de tratamiento por defecto realizara una accion u otra. Por lo general suele provocar la terminación del proceso mediante una llamada a exit. Algunas señales hacen que el núcleo genere un fichero llamado core, que contiene un volcado de memoria del contexto del proceso y que podra ser examinado con ayuda de un programa depurador (adb, sdb, gdb) para determinar qué señal provocó la terminación del proceso y en qué punto exacto de su ejecución se produjo. Esto es muy util a la hora de depurar programas que contienen errores tales como: incorrecta manipulación de numeros en coma flotante, instrucciones ilegales, acceso a direcciones fuera de rango, etc.
  3. 3- Invocar a una rutina propia que se encarga de tratar la señal.

Tabla de ejemplo de comunicación entre señales.

Cada señal tiene asociado un numero entero positivo y, cuando un proceso le envia una señal a otro le envia este numero. En el UNIX System V hay definidas 19 señales, y estas señales las tienen prácticamente todas las versiones de UNIX, y luego cada fabricante le añade las que considera necesarias.

Las señales se clasifican en los siguientes grupos:

  • Señales relacionadas con la terminación de procesos.
  • Señales relacionadas con las excepciones inducidas por los procesos. Ejemplos: el intento de acceder fuera del espacio de direcciones virtuales, los errores producidos al manejar numeros en coma flotante, etc
  • - Señales relacionadas con los errores irrecuperables originados en el transcurso de una llamada al sistema.
  • - Señales originadas desde un proceso que se está ejecutando en modo usuario. Ejemplos: cuando un proceso envia una señal a otro via kill, cuando un proceso activa un temporizador y se queda en espera de la señal de alarma,etc.
  • - Señales relacionadas con la interacción con el terminal. Ejemplo: pulsar las teclas Ctrl+C.
  • - Señales para ejecutar un proceso paso a paso. Son usadas por los depuradores. En el fichero de cabecera <signal.h> estan definidas las señales que puede manejar el sistema.

Esas señales son las siguientes:

lista con las diferentes señales que hay

3. SEÑALES EN EL UNIX SYSTEM V

Una señal es un evento que debe ser procesado y que puede interrumpir el flujo normal de un programa.

Una señal puede asociarse con una función que procesa el evento que ha ocurrido, el evento no interrumpe el fuljo del programa.

ENVÍO DE SEÑALES (KILL Y RAISE)

La subrutina Kill envía una señal a uno o varios procesos desde otro proceso.

Su formato es:

#include <signal.h>
int kill (pid_t pid,int sig)

Pid que es el identificador de los procesos que recibirán la señal. Los valores que puede tomar son:

  • >0 : identificador del proceso al que le enviamos la señal.
  • =0 : la señal es enviada al proceso cuyo identificador sea igual al pid del proceso actual.
  • < -1 : reciben la señal todos aquellos procesos cuyo identificador sea igual al valor absoluto del proceso que la envía.
  • =-1 : la señal es recibida por aquellos procesos cuyo identificador real sea igual al identificador efectivo del proceso que la envía.

Tabla de los diferentes comportamientos de las señales.

La acción por defecto para la señal puede:

  • Generar un fichero core
  • Ignorar la señal
  • Parar el proceso que recibe la señal.
  • En todos estos casos el proceso que envía la señal tiene que tener el pid de súper usuario o tener privilegios sobre el proceso al que envía la señal sino la llamada kill falla.
  • SIG es el número de la señal que queremos enviar. Si vale 0 efectúa una comprobación de errores, pero no se envía ninguna señal. Si se envía satisfactoriamente Kill devuelve un 0 si no es así devolverá un -1. También sirve para verificar la validez del Pid.

EJEMPLO DEL USO DE KILL

#include < signal.h>
main()
{
int pid;
if (( pid = fork()) == 0) {
while(1) {
printf("HIJO.PID = %d\n", pid);
sleep(1);
}
}
sleep(10);
printf("PADRE. Terminación del proceso %d\n", pid);
kill (pid,SIGTERM);
exit(0);
}

Este ejemplo crea un hijo y mientras exista debe mostrar la palabra hijo cada segundo y el padre a los 10 segundos debe mostrar la palabra PADRE y terminar el proceso del hijo con la señal SIGTERM.

La subrutina raise envía señales al proceso actual, es decir un proceso así mismo. Su formato es:

#include < signal.h>
int raise(int sig);
Los parámetros son:
SIG es el numero d las señal que queremos enviar;

RAISE se codifica con kill de esta forma:<

int raise(int sig)
{
return kill(getpid(),sig);
}

TRATAMIENTO DE SEÑALES (signal)

La subrutina signal asocia una acción determinada con una señal, es del tipo función que devuelve un puntero a una función void y recibe dos parámetros :

Su formato es:

#include < signal.h>
void (*signal 8int sig, void(*action) ())) ();

Los parámetros son:

-SIG numero de señal
-ACTION Puntero a la rutina asociada con la señal o uno de los valores siguientes:
SIG_DFL : acción por defecto para dicha señal, consiste en terminar el proceso y a veces generar un fichero core.
SIG_IGN ignorar la señal.

DIRECCION, es la dirección de la rutina de tratamiento de la señal. Su declaración debe ser de esta forma:

#include < signal.h>
void handler (int sig [, int code, struct sigcontext *scp]);

La rutina handler es la rutina a la cual el núcleo llama y la pasa los parámetros sig,code y scp, su llamada es asíncrona, es decir, se puede dar en cualquier momento de la ejecución del programa y debe estar codificada para tratar las situaciones en las que se produce el envío de señales. Sus parámetros son:

sig, numero de las señal
code, palabra que contiene información del estado del hardware en el momento de invocar a handler (parámetro opcional).

scp contiene información de contexto definida en < signal.h>. (parámetro opcional). La llamada a signal devuelve el valor que tenia la action, que sirve para restaurarlo, si se produce un error signal devuelve SIG_ERR y en errno estará el código del error.

Los valores SIG_DFL, SIG_IGN Y SIG_ERR son direcciones de funciones para que puedan devolver signal y deben estar siempre vacías. Se soluciona definiéndolas de esta forma:

#define SIG_DFL ((void(*) ())0)
#define SIG_IGN ((void(*) ())1)
#define SIG_ERR((void (*) ())-1)

La conversión de las constantes hace que estas sean tratadas como direcciones de inicio de funciones que no contienen ninguna función porque en todas las arquitecturas UNIX son zonas reservadas para el núcleo.

EJEMPLO USO DE SIGNAL(signal-1.c)

#include < stdio.h>
#include < signal.h>
/***
main:inicializa el manejador de la señal SIGINT y se pone en espera para recibir la señal.
***/
main()
{
void manejador_SIGINT();
if (signal(SIGINT, manejador_SIGINT) == SIG_ERR) {
perror ("signal");
exit(-1);
}
while(1) {
printf ("En espera de Ctrl-C\n");
sleep (999);
}
}
/***
manejador_SIGINT : rutina de tratamiento de la señal SIGINT.
***/
void manejador_SIGINT (int sig)
{
printf("Señal número %d recibida.\n", sig);
}

El ejemplo anterior hace que la primera vez que se pulse Ctrl-C aparezca un mensaje que ponga En espera de Ctrl-C y que espere 999 segundos pero la segunda vez que se pulsa Ctrl-C hace que se muestre el numero de la señal y que ha sido recibida y termina la ejecución del proceso, esto es porque el nucleo llama a la rutina de tratamiento, se restaura la rutina por defecto, que se encarga de terminar el proceso. Para resolver el problema se hace este tratamiento:

void manejador_SIGINT (int sig)
{
static cnt = 0;
printf("señal número %d recibida.\n",sig);
if (cnt < 20)
printf ("Contador = %d\n", cnt ++);
else
exit(0);
if (signal (SIGINT, manejador_SIGINT) == SIG_ERR) {
perror ("signal");
exit(-1);
}
}

Esto hace que la rutina de tratamiento siga siendo la misma y se terminara a los 20 segundos.

Si recibimos una señal mientras que tratamos otra del mismo tipo con el caso anterior se terminaría el proceso ya que al recibirse la señal por primera vez la nueva rutina de tratamiento pasa a ser la rutina por defecto.

Para bloquear la recepción de señales de un tipo mientras tratamos otra haremos una llamada a signal pasándole el parámetro SIG_IGN. Como en el siguiente ejemplo:

void manejador_SIGINT (int sig)
{
static cnt =0;
if (signal (signal (SIGINT, SIGIGN) == SIG_ERR){
perror ("manejador_SIGINT");
exit(-1);
}
printf ("Señal número %d recibida.\n",sig);
if (cnt< 20)
printf("Contador = %d\n", cnt++);
else
exit(0);
signal(SIGINT, manejador_SIGINT);
}

Otra opción es que una señal interrumpa a la rutina de tratamiento sin que haga terminar el proceso, pero no es aconsejable ya que puede que la fuente de señales las genere a una velocidad mayor que la de tratamiento del manejador y esto haría que se desbordara el programa. Por ejemplo:

void manejador_SIGINT(int sig)
{
static cnt = 0;
signal(SIGINT,manejador_SIGINT);
printf("Señal número %d recibida.\n", sig);
if (cnt < 20)
print ("Contador = %d\n", cnt++);
else
exit(0);
}

Bajate esta documentación en un archivo Acrobat Reader

Imagen que hace referencia a un archivo para Acrobat Reader