Señales y Procesamiento de Señales
Resumen
Este capítulo aborda el concepto de señales y su procesamiento en sistemas operativos; presenta un enfoque unificado para el manejo de señales e interrupciones, ayudando a comprenderlas desde la perspectiva adecuada. Se tratan las señales como interrupciones de procesos, que desvían la ejecución normal hacia un manejador específico. Se explican las fuentes de las señales, incluyendo hardware, excepciones y otros procesos. Posteriormente, se ilustran usos comunes de las señales en entornos Unix/Linux. Se detalla el mecanismo de procesamiento de señales en estos sistemas, cubriendo tipos de señales, vectores de bits, máscaras de señales, estructura PROC del proceso y los pasos involucrados en el procesamiento. Se muestra mediante ejemplos cómo instalar capturadores de señales para manejar excepciones como fallos de segmentación en modo usuario. También se discute la idoneidad de utilizar señales como mecanismo de comunicación entre procesos (IPC). Los lectores pueden implementar un proyecto de programación que utilice señales y tuberías (pipes) para crear un sistema de intercambio de información entre procesos.
Señales e Interrupciones
- Interrupción: Es una solicitud externa enviada desde dispositivos I/O o coprocesadores hacia la CPU, que desvía su ejecución normal hacia un manejador de interrupciones. De manera similar, una "señal" es una solicitud enviada a un proceso, desviándolo de su ejecución normal hacia un manejader específico. (1) Un proceso es una secuencia de actividades. (2) Una interrupción es un evento enviado a un proceso, que lo desvía de sus actividades normales hacia un manejador de interrupciones. El proceso puede retomar sus actividades normales una vez completado el manejo de la interrupción. (3) Las interrupciones pueden aplicarse a cualquier proceso, no limitándose únicamente a la CPU en sistemas computacionales.
- Interrupción de proceso: Son interrupciones enviadas específicamente a procesos. Durante su ejecución, un proceso puede recibir interrupciones de tres fuentes distintas:
(1) Interrupciones de hardware: Teclas de interrupción (como "Ctrl+C"), temporizadores, etc.
(2) Interrupciones de otros procesos.
(3) Auto-interrupciones generadas por el propio proceso.
- Interrupciones de hardware: Son señales enviadas directamente al procesador o CPU.
- Errores de trampa en procesos: Un proceso puede generar sus propias interrupciones. Estas interrupciones son causadas por errores reconocidos por la CPU como excepciones, como división por cero, direcciones inválidas, instrucciones ilegales, violaciones de privilegios, etc. Cuando un proceso encuentra una excepción, entra en el kernel del sistema operativo, convierte la causa de la trampa en un número de señal y se envía la señal a sí mismo.
(1) Excepciones en modo usuario: La acción predeterminada del proceso es terminar, opcionalmente almacenando un volcado de memoria para depuración. Un proceso puede instalar un capturador de señales para reemplazar la acción predeterminada, permitiéndole manejar la señal en modo usuario.
(2) Trampas en modo kernel: Causadas por errores de hardware o fallos en el código del kernel. En este caso, el kernel no puede manejar la situación. En sistemas Unix/Linux, el kernel simplemente imprime un mensaje de error PANIC y se detiene, esperando que el problema se identifique y corrija en futuras versiones del kernel.
Procesamiento de Señales en Unix/Linux
Tipos de Señales
Cada señal tiene un nombre simbólico, como SIGHUP (1), SIGEMT(2), SIGKILL(9), SIGSEGV (11), etc.
Fuentes de Señales
- Señales de interrupciones de hardware: Durante la ejecución, algunas interrupciones de hardware se convierten en señales enviadas al proceso.
(1) La tecla de interrupción (Ctrl+C), que genera una señal SIGINT(2).
(2) Los temporizadores, cuando expiran su tiempo, generan señales SIGALRM(14), SIGTALRM(26) o SIGPROF(27).
(3) Otros errores de hardware, como errores de bus o trampas de E/S.
- Señales de excepciones: Las señales de trampa comunes incluyen SIGFPE(8), que indica una excepción de punto flotante (como división por cero), y la más temida SIGSEGV(11), que indica un fallo de segmentación.
- Señales de otros procesos: Un proceso puede usar la llamada al sistema kill(pid, sig) para enviar una señal al proceso identificado por pid.
Señales en la estructura PROC del proceso
Cada proceso PROC tiene un vector de 32 bits para registrar las señales enviadas al proceso. En este vector, cada bit (excepto el bit 0) representa un número de señal. Además, tiene un vector de bits de máscara para bloquear señales específicas.
Funciones Manejadoras de Señales
Cada proceso PROC tiene un array de manejadores de señales int sig[32]. Cada entrada del array especifica cómo manejar la señal correspondiente: 0 indica DEFault (predeterminado), 1 indica IGNore (ignorar), y otros valores distintos de cero indican una función capturadora (manejadora) previamente instalada en modo usuario. La figura muestra el vector de bits de señal, el vector de bits de máscara y las funciones manejadoras.
Si el bit correspondiente en el vector de bits de señal está activo (1), se genera o envía una señal al proceso. Si el bit correspondiente en el vector de máscara está activo (1), la señal se bloquea o enmascara. De lo contrario, la señal no está bloqueada. Una señal solo se efectúa o se entrega al proceso si existe y no está bloqueada. Cuando un proceso en modo kernel detecta una señal no bloqueada, limpia el bit correspondiente en el vector de bits e intenta manejar la señal usando la función especificada en el array de manejadores. 0 significa DEFault, 1 significa IGNore, y otros valores indican una función capturadora previamente instalada en espacio de usuario.
Instalación de Capturadores de Señales
Un proceso puede usar la llamada al sistema:
int r = signal(int numero_señal, void *manejador);
para modificar el manejador de una señal específica, exceptuando las señales (19) y (9), que no pueden ser modificadas.
La llamada al sistema signal() está disponible en todos los sistemas tipo Unix, pero tiene características no ideales: (1) Antes de ejecutar el manejador instalado, generalmente restablece el manejador a DEFault. Para capturar la siguiente ocurrencia de la misma señal, es necesario reinstalar el capturador. Esto puede crear una condición de carrera entre la llegada de la siguiente señal y la reinstalación del manejador. En cambio, sigaction() bloquea automáticamente la siguiente señal mientras se ejecuta el manejador actual, evitando condiciones de carrera. (2) signal() no puede bloquear otras señales. Si es necesario, el usuario debe usar sigprocmask() explícitamente para bloquear o desbloquear otras señales. Por el contrario, sigaction() puede especificar qué señales bloquear. (3) signal() solo puede pasar el número de señal al manejador. sigaction() puede transmitir información adicional sobre la señal. (4) signal() puede no ser adecuada para hilos en programas multihilo. sigaction() es adecuada para hilos. (5) Diferentes versiones de Unix pueden tener implementaciones diferentes de signal(). sigaction() sigue el estándar POSIX, por lo que es más portable.
Llamada al sistema sigaction()
- Prototipo:
int sigaction (int signum, const struct sigaction *act, struct sigaction *oldact); - La estructura sigaction() se define como:
struct sigaction{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}
- Campos importantes:
sa_handler: Puntero a la función manejadora, con el mismo prototipo que la función manejadora de signal().
sa_sigaction: Alternativa para ejecutar la función manejadora de señales. Incluye dos parámetros adicionales además del número de señal, donde siginfo_t * proporciona información adicional sobre la señal recibida.
sa_mask: Permite especificar qué señales bloquear durante la ejecución del manejador.
sa_flags: Permite modificar el comportamiento del proceso de manejo de señales. Para usar el manejador sa_sigaction, se debe establecer sa_flags en SA_SIGINFO.
Pasos de Procesamiento de Señales
- Cuando un proceso está en modo kernel, verifica y maneja las señales pendientes. Si una señal tiene un manejador de usuario instalado, el proceso primero limpia la señal, obtiene la dirección del manejador capturador, y para la mayoría de las señales de trampa, restablece el manejador instalado a DEFault. Luego, retorna a modo usuario para ejecutar el capturador, manipulando así la ruta de retorno. Cuando el manejador capturador termina, regresa al punto de interrupción original, es decir, al último lugar donde el proceso entró al kernel. Por lo tanto, el proceso desvía su ejecución para manejar la señal antes de retomar su ejecución normal.
- Restablecimiento del manejador de usuario instalado: Los manejadores de señales de trampa instalados por el usuario se utilizan para manejar errores de trampa en código de usuario. Dado que el manejador también se ejecuta en modo usuario, podría ocurrir el mismo error nuevamente. Si esto sucede, el proceso entraría en un bucle infinito, alternando continuamente entre modo usuario y modo kernel. Para evitar esto, el kernel de Unix generalmente restablece el manejador a DEFault antes de permitir que el proceso ejecute el manejador capturador. Esto significa que el manejador capturador instalado por el usuario solo es válido para la primera ocurrencia de la señal. Para capturar la misma señal que aparece nuevamente, es necesario reinstalar el capturador. Sin embargo, el manejo de los manejadores de señales instalados por el usuario varía entre diferentes versiones de Unix. Por ejemplo, en BSD Unix, el manejador de señales no se restablece, pero la señal se bloquea durante la ejecución del manejador capturador. Los lectores interesados pueden consultar las páginas man de señales Linux y la función sigaction para obtener más detalles.
- Señales y despertar: En el kernel de Unix/Linux existen dos tipos de procesos SLEEP: procesos de sueño profundo e interrumpibles. Los primeros no pueden ser interrumpidos, mientras que los segundos sí pueden ser interrumpidos por señales. Si un proceso está en estado SLEEP no interrumpible, las señales que llegan (deben provenir de interrupciones de hardware u otros procesos) no lo despertarán. Si está en estado SLEEP interrumpible, la señal lo despertará. Por ejemplo, cuando un proceso espera entrada de terminal, entra en un sueño de baja prioridad que es interrumpible, y señales como SIGINT pueden despertarlo.
IPC en Linux
IPC (Inter-Process Communication) se refiere a los mecanismos para la comunicación entre procesos. En Linux, el IPC incluye:
- Tuberías (pipes) y FIFO
- Señales
- System V IPC
- Colas de mensajes POSIX
- Mecanismos de sincronización de hilos
- Sockets
Práctica:
Código:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<setjmp.h>
#include<string.h>
jmp_buf entorno;
int contador = 0;
void manejador(int sig, siginfo_t *info_señal, void *contexto)
{
printf ("manejador: señal=%d desde PID=%d UID=%d contador=%d\n",
sig, info_señal->si_pid, info_señal->si_uid, ++contador);
if (contador >= 4) // permitir que ocurra hasta 4 veces
longjmp(entorno, 1234);
}
int FALLO()
{
int *puntero = 0;
printf("en FALLO(): intentando desreferenciar puntero NULL\n");
*puntero = 123; // desreferenciar un puntero NULL
printf("no debería ver esta línea\n");
}
int main (int argc, char *argv[])
{
int resultado;
struct sigaccion accion;
memset(&accion, 0, sizeof(accion));
accion.sa_sigaction = &manejador;
accion.sa_flags = SA_SIGINFO;
sigaction(SIGSEGV,&accion,NULL);
if ((resultado = setjmp(entorno)) == 0)
FALLO();
else
printf("proceso %d sobrevivió a FALLO DE SEGMENTACIÓN: resultado=%d\n",getpid(), resultado);
printf("proceso %d en bucle infinito\n",getpid());
while(1);
}