MÓdulo II: programación concurrente señales: interrupciones software de unix. Generalidades




Дата канвертавання26.04.2016
Памер53.63 Kb.
MÓDULO II: PROGRAMACIÓN CONCURRENTE
SEÑALES: INTERRUPCIONES SOFTWARE DE UNIX.
1.- Generalidades.
Durante una sesión cualquiera, el número de procesos depende del trabajo que los usuarios realicen. Se sabe que los procesos tienen su propio contexto, pero esto no quiere decir que estén incomunicados entre sí. Existe un conjunto de métodos mantenidos por el kernel que permiten entablar diálogos entre ellos. Estos métodos se llaman mecanismos IPC (Interprocess Comunication). Dentro del conjunto de IPC’s se tienen a los semáforos, la memoria compartida, colas de mensajes, etc. Estas no son las únicas formas de intercomunicación de que dispone el sistema operativo Unix. Los procesos también pueden enviarse interrupciones software, señales. El conjunto de señales lo maneja el gestor de señales. El número y tipo de señales viene impuesto por el sistema operativo y cada una de ellas será empleada en un caso concreto siendo su número la única información que realmente se transmite entre los procesos cuyo significado dependerá de la interpretación del programador.
1.1.- Finalidad.
Como indica la palabra interrupción, este tipo de llamadas son producidas por el kernel o por otro proceso de forma inesperada y tiene como finalidad parar o desviar el curso normal de las instrucciones que se ejecutan. Una señal puede ser recibida por un proceso si este incurre en un error en coma flotante, si se produce un error de acceso a memoria, si se intenta acceder a una dirección de memoria fuera de su segmento de datos, etc.
Como anteriormente se dijo, el número y significado de las señales depende del tipo de sistema operativo Unix que se tenga instalado. En el fichero de cabecera signal.h están definidas todas las señales, número y nombre.
1.2.- Comportamiento.
Cuando un proceso recibe una señal, puede tratarla de tres formas diferentes:

1.- Ignorar la señal, con lo cual no tiene efecto.

2.- Invocar a la rutina de tratamiento correspondiente al número de señal. Esta rutina no la codifica el programador, sino que la aporta el kernel y normalmente tiene como fin el terminar el proceso que recibe la señal. En algunos casos, antes de eliminar al proceso, el kernel se encarga de generar en el directorio de trabajo actual del proceso un fichero llamado core que contiene un volcado de memoria del contexto del proceso. Analizando dicho fichero se podrá saber en qué punto terminó el proceso y por qué motivo se le envió la señal.

3.- Invocar a una rutina que se encarga de tratar la señal y que ha sido creada por el programador. Esta rutina establecerá un mecanismo de comunicación entre procesos o modificará el curso normal del programa. En estos casos, el proceso no va a terminar a menos que la rutina de tratamiento indique lo contrario.




La siguiente figura pretende reflejar los tres tipos de tratamientos que puede recibir una señal. La primera señal que recibe no provoca que el proceso cambie el curso de su ejecución, esto es debido a que la acción que está activa es que el proceso ignore la señal. El proceso prosigue su ejecución y recibe una segunda señal que le fuerza a entrar en una rutina de tratamiento. Esta rutina, después de tratar la señal, puede optar por tres acciones: restaurar la ejecución del proceso al punto donde se produjo la interrupción, finalizar el proceso o restaurar alguno de los estados pasados del proceso y continuar la ejecución desde ese punto. El proceso puede también recibir una señal que le fuerce a entrar en la rutina de tratamiento por defecto.
1.3.- Conjunto de señales básicas.
Cada señal tiene asociado un número entero positivo, que es lo que se intercambia cuando un proceso envía una señal a otro. Se pueden clasificar las señales en los siguientes grupos:

a) Señales relacionadas con la terminación de procesos.

b) Señales relacionadas con las excepciones inducidas por los procesos. Por ejemplo, el intento de acceder fuera del espacio de direcciones virtuales, los errores producidos al utilizar números en coma flotante, etc.

c) Señales relacionadas con los errores irrecuperables originados en el transcurso de una llamada al sistema.

d) Señales originadas desde un proceso que se está ejecutando en modo usuario. Por ejemplo, cuando un proceso envía una señal a otro, etc.

e) Señales relacionadas con la interacción con el terminal. Por ejemplo, pulsar la tecla break.



f) Señales para ejecutar un programa paso a paso. Son usadas por los depuradores.
A continuación, se van a describir las 19 señales que toman como base la mayoría de los sistemas operativos Unix.
SIGHUP (1) Hangup. Esta señal se envía a todos los procesos de un grupo cuando su líder de grupo termina su ejecución. También se envía cuando un terminal se desconecta de un proceso del que es terminal de control. La acción por defecto de esta señal es terminar la ejecución del proceso que la recibe.
SIGINT (2) Interrupción. Es enviada cuando en medio de un proceso se pulsa las teclas de interrupción (Ctrl + c). Por defecto se termina la ejecución del proceso que recibe la señal.
SIGQUIT (3) Salir. Similar a SIGINT, pero es generada al pulsar la tecla de salida (Ctrl + \). Su acción por defecto es generar un fichero core y terminar el proceso.
SIGILL (4) Instrucción ilegal. Es enviada cuando el hardware detecta una instrucción ilegal. En los programas escritos en C suele producirse este tipo de error cuando se maneja punteros a funciones que no han sido correctamente inicializados. Su acción por defecto es generar un fichero core y terminar el proceso.
SIGTRAP (5) Trace trap.Es enviada después de ejecutar cada instrucción cuando el proceso se está ejecutando paso a paso.
SIGIOT (6) I/O trap instruction. Es enviada a los procesos cuando se detecta un fallo hardware.
SIGEMT (7) Emulator trap instruction.Advierte de errores detectados por el hardware.
SIGFPE (8) Error en coma flotante. Es enviada cuando el hardware detecta un error en coma flotante, como el uso de número en coma flotante con un formato desconocido, errores de overflow o underflow, etc.
SIGKILL (9) Kill. Esta señal provoca irremediablemente la terminación del proceso. No puede ser ignorada ni tampoco se puede modificar la rutina por defecto. Siempre que se recibe se ejecuta su acción por defecto, que consiste en terminar el proceso.
SIGBUS (10) Bus error. Se produce cuando se intenta acceder de forma errónea a una zona de memoria o a una dirección inexistente. Su acción es terminar el proceso que la recibe.
SIGSEGV(11) Violación de segmento. Es enviada a un proceso cuando intenta acceder a datos que se encuentran fuera de su segmento de datos. Su acción por defecto es terminar el proceso.
SIGSYS (12) Argumento erróneo en una llamada al sistema. Si uno de los argumentos de una llamada al sistema es erróneo se envía esta señal.
SIGPIPE (13) Intento de escritura en una tubería de la que no hay nadie leyendo. Esto suele ocurrir cuando el proceso de lectura termina de una forma anormal. De esta forma se evita perder datos. Su acción es terminar el proceso.
SIGALRM(14) Alarm clock. Cada proceso tiene asignados un conjunto de temporizadores. Si se ha activado alguno de ellos y este llega a cero, se envía esta señal al proceso.
SIGTERM(15) Finalización software. Es la señal utilizada para indicarle a un proceso que debe terminar su ejecución. Esta señal no es tajante como SIGKILL y puede ser ignorada. Lo correcto es que la rutina de tratamiento de esta señal se encargue de tomar las acciones necesarias antes de terminar un proceso (como, por ejemplo, borrar los archivos temporales) y llame a la rutina exit. Esta señal es enviada a todos los procesos cuando se produce una para del sistema. Su acción por defecto es terminar el proceso.
SIGUSR1(16) Señal número 1 de usuario. Esta señal está reservada para el usuario. Su interpretación dependerá del código desarrollado por el programador. Suelen emplearse para sincronización de procesos. Ninguna aplicación estándar va a utilizarla y su significado es el que le quiera dar el programador en su aplicación. Por defecto termina el proceso que recibe.
SIGUSR2(17) Señal número 2 de usuario. Su significado es idéntico al de SIGUSR1.
SIGCLD (18) Muerte del proceso hijo. Es enviada al proceso padre cuando alguno de sus procesos hijos termina. Esta señal es ignorada por defecto.
SIGPWR (19) Fallo de alimentación. Esta señal tiene diferentes interpretaciones. En algunos sistemas es enviada cuando se detecta un fallo de alimentación y le indica al proceso que dispone tan sólo de unos instantes de tiempo antes de que se produzca una caída del sistema. En otros sistemas, esta señal es enviada, después de recuperarse de un fallo de alimentación, a todos aquellos procesos que estaban en ejecución y que se han podido rearrancar. En estos casos, los procesos deben disponer de mecanismos para restaurar las posibles pérdidas producidas durante la caída de la alimentación.
2.- Enviar una señal.
Para enviar una señal desde un proceso a otro o a un grupo de procesos, se emplea la llamada kill (pid, sig). Pid identifica al conjunto de procesos a los que se le desea enviar la señal. Pid es un número entero y algunos de los distintos valores que puede tomar tienen los siguientes significados:

Pid > 0 Es el PID del proceso al que se le envía la señal.

Pid = 0 La señal es enviada a todos los procesos que pertenecen al mismo grupo que el proceso que la envía.
Sig es el entero que representa la señal que se quiere enviar. Si sig vale 0 (no existe una señal con tal identificador (señal nula)), se efectúa una comprobación de la validez del pid, es decir, si ese proceso existe, pero no se envía ninguna señal.
Si el envío se realiza satisfactoriamente, kill (pid, sig) devuelve 0; en caso contrario devuelve -1.
En el ejemplo siguiente se puede ver cómo un proceso envía una señal a su proceso hijo para forzar su terminación.
#include /* Para definir las señales */

main ()

{

int pid;
if ((pid=fork ())==0)

{

/* Código del proceso hijo. Entro en un ciclo infinito. */

while (1)

{

printf ("Soy el proceso hijo mi PID es %d\n”, getpid ());

sleep (1);

}

}

/* Código que va a ejecutar el proceso padre. */

sleep (10);

printf ("Soy el proceso padre y mi PID es %d\n", getpid ());

printf(“Eliminando a mi proceso hijo \n”);

kill (pid, SIGTERM); /* También se puede usar SIGKILL. */

exit (0);

}
Si se desea que un proceso se envíe señales a sí mismo, basta con conocer su propio pid, cómo indica el siguiente ejemplo:
int numero_sig;

.....

/* Envío de la señal fin de contador al propio proceso. */

numero_sig=SIGALRM;

kill (getpid(), numero_sig);
3.- Recibir una señal.
Si un proceso no desea que el kernel controle su respuesta ante la recepción de una señal, es necesario primero crear una rutina de tratamiento de señales y luego situar este código “encima” de las sentencias impuestas por defecto en el sistema operativo.
Con la llamada signal (sig, action) se puede realizar este paso. El parámetro sig indicará el número de la señal sobre la que se desea especificar la forma de tratamiento. Action es la acción que se desea iniciar cuando se reciba la señal. Este parámetro tomará uno de los siguientes tres valores:

SIG_IGN Indica que la señal se debe ignorar. No todas las señales pueden ignorarse. Este es el caso de SIGKILL.

SIG_DFL Indica que la acción a realizar cuando se reciba la señal es la acción por defecto asociada a dicha señal.

dirección Es la dirección de inicio de la rutina de tratamiento de la señal.
La llamada a la rutina es asíncrona, lo cual quiere decir que puede darse en cualquier instante de la ejecución del programa. Esta rutina debe estar codificada para tratar las situaciones especiales que ocasionan que se produzca el envío de señales.
La llamada a signal (sig, action) devuelve el valor que tenía action. Valor que puede servir para restaurarlo en cualquier instante posterior. Si se produce algún error, signal (sig, action) devuelve SIG_ERR.
Como ejemplo de tratamiento de señales, el siguiente programa trata la señal SIGINT, que es generada al pulsar las teclas Ctrl + C.
#include

#include /* Para definir las señales */
void sigint(int sig);
/* La función principal inicializa el manejador de la señal SIGINT y se pone en espera para recibir la señal. */

main ()

{

if (signal(SIGINT, sigint)==SIG_ERR)

{

printf (“Se ha producido un error\n”);

exit (-1);

}

while (1)

{

printf ("En espera de Ctrl + C\n”);

sleep (1);

}

}

/* Función sigint que trata la señal SIGINT. */

void sigint(int sig)

{

printf ("Senal número %d recibida.\n”, sig);

}
Al ejecutar este programa, la primera vez que se pulsa Ctrl + C aparece el mensaje por pantalla, pero la segunda vez termina la ejecución del proceso. Esto se debe a que el kernel llama a la rutina de tratamiento, y luego restaura la rutina por defecto como nueva rutina de tratamiento. Así, al salir de la rutina codificada en el programa anterior y recibir por segunda vez la señal, se invoca a la rutina por defecto, que se encarga de terminar el proceso. Para solventar este problema es necesario volver a llamar a signal dentro de la rutina de tratamiento, como se muestra en el siguiente programa:
/* Función sigint que trata la señal SIGINT. */

void sigint(int sig)

{

static cnt=0;

printf ("Senal número %d recibida.\n”, sig);

if (cnt<5)

printf(“Contador = %d\n”, cnt++);

else exit(0);

if (signal(SIGINT, sigint)==SIG_ERR)

{

printf(“Se ha producido un error.\n”);

exit(-1);

}

}
De esta forma, la rutina de tratamiento sigue siendo la misma. Se puede observar, que dicha rutina da lugar a que el proceso termine después de pulsar 6 veces las teclas Ctrl + C. Para las señales SIGILL, SIGTRAP y SIGPWR no es necesario que se tome esta precaución, puesto que no se restaura la rutina por defecto, sino que sigue permaneciendo la que había.
En el ejemplo anterior, si se recibe una señal que no es la que se trata, se producirá la terminación del proceso, ya que al recibirse la señal por primera vez la nueva rutina de tratamiento pasa a ser la rutina por defecto.
Si se desea bloquear la recepción de señales de un tipo mientras se está tratando otra, es necesario hacer una llamada a signal(sig, action) pasándole el parámetro SIG_IGN. El siguiente programa muestra un ejemplo:

void sigint(int sig)

{

static cnt=0;

if (signal(SIGQUIT, SIG_IGN)==SIG_ERR)

{

printf("Se ha producido un error.\n");

exit(-1);

}

printf("Senal número %d recibida.\n", sig);

if (cnt<5)

printf("Contador=%d\n", cnt++);

else exit(0);

signal(SIGINT, sigint);

}
4.- Esperar una señal.
Una señal es una llamada asíncrona, es decir, puede producirse en cualquier momento y se debe preparar el programa para ello. Esto da lugar a que a veces sea interesante parar la ejecución de la aplicación hasta que se produzca una señal. Para ello se puede usar la llamada pause (). Esta llamada hace que el proceso invocador se bloquee (quede en espera) hasta la llegada de una señal cualquiera, no ignorada y no bloqueada. Cuando esto ocurre, y después de ejecutarse la rutina de tratamiento de la señal, pause () devuelve el valor -1 y la ejecución se continúa con la sentencia que sigue a esta llamada.
El siguiente programa presenta un ejemplo de la utilización de esta llamada, puesto que hace que un proceso espere una señal y cuando recibe la señal SIGUSR1 (16) presenta por pantalla un número aleatorio.
#include

#include

#include
void sigusr1(int sig);

void sigterm();
/* Función principal */

main()

{

signal(SIGTERM, sigterm);

signal(SIGUSR1, sigusr1);

while (1) pause ();

}
/* Función sigterm */

void sigterm()

{

printf("Terminación del proceso %d a petición del usuario. \n", getpid());

exit (-1);

}

/* Función sigusr1 */

void sigusr1(int sig)

{

signal(sig, SIG_IGN);

printf("%d\n", rand());

signal(sig, sigusr1);

}
Para ejecutar este programa y que muestre los números aleatorio, se debe enviar la señal 16 con la orden kill y para terminar la ejecución del proceso se envía la señal número 15.
5.- Saltar dentro del código.
La rutina de tratamiento de una señal puede hacer que el proceso vuelva a alguno de los estados por los que ha pasado con anterioridad. Para realizar esto se usa las funciones estándar de librería setjmp(env) y longjmp(env, val).
Setjmp(env) guarda el entorno de pila, es decir el estado de la pila del proceso en ejecución en env para un uso posterior del mismo mediante longjmp(env, val) que restaura el entorno guardado y permite retornar a este punto el flujo de la ejecución del proceso. La variable env es de tipo jmp_buf, que está definido en el fichero de cabecera .
La primera vez que se llama a setjmp(env) esta devuelve un 0 y cuando se llama después, puesto que se salta a ese punto mediante longjmp(env, val), devuelve el valor de val que fue pasado mediante longjmp(env, val). Esta es la forma de averiguar si setjmp(env) está saliendo de una llamada para guardar el entorno o de una llamada de longjmp(env, val). Longjmp(env, val) no puede hacer que setjmp(env) devuelva 0, ya que en el caso de que val valga 0, setjmp(env) va a devolver 1.
Estas funciones pueden verse como una forma elaborada de implementar una sentencia goto capaz de saltar desde una función a etiquetas que están en la misma o en otra función. Las etiquetas serían los entornos guardados por setjmp(env) en la variable env.
Como ejemplo, el siguiente programa cuenta desde 1 hasta 100 incrementando su valor cada 10 segundos. Además, cada 10 segundos se va a establecer un punto de retorno de tal forma que si se recibe la señal SIGUSR1 en algún instante, el programa va a reiniciar su ejecución en ese punto.
#include

#include

#include
jmp_buf env; /* Debe ser global para que la conozcan las rutinas de tratamiento de las señales */

void sigusr1();
/* Función principal */

main()

{

int i;
signal(SIGUSR1, sigusr1);

for (i=0;i<100;i++)

{

if (setjmp(env)==0)

printf("Punto de retorno en el estado %d\n", i);

else

/* Esta parte se ejecuta al realizar una llamada longjmp */

printf("Regreso al punto de retorno del estado %d\n", i);

sleep (10);

}

}
/* Función sigusr1 */

void sigusr1()

{

signal(SIGUSR1, sigusr1);

longjmp(env,1);

}

© E.G.R.




База данных защищена авторским правом ©shkola.of.by 2016
звярнуцца да адміністрацыі

    Галоўная старонка