Cuando un proceso se está ejecutando, puede recibir interrupciones del sistema operativo o de otros procesos. Estas interrupciones se llaman señales. Una señal es un mensaje asíncrono que le dice al proceso que ha ocurrido un evento (como que el usuario pulsó Ctrl+C o que el proceso ha intentado acceder a una dirección de memoria inválida, provocando un SIGSEGV).
La función signal(int signum, void (*handler)(int)) le permite a tu programa registrar un manejador de señal (handler): una función que el sistema operativo ejecutará inmediatamente, suspendiendo el flujo normal de tu programa, para atender dicho evento. Por ejemplo, SIGINT se dispara al pulsar Ctrl+C, SIGTERM se usa para solicitar la terminación limpia de un daemon, y señales como SIGFPE (error aritmético) o SIGILL (instrucción ilegal) indican fallos fatales. También existen constantes especiales como SIG_DFL para restaurar el comportamiento por defecto (normalmente terminar el proceso) o SIG_IGN para ignorar la señal por completo.
Sin embargo, este mecanismo es peligroso debido a su naturaleza asíncrona. El manejador puede interrumpir al programa en cualquier momento, incluso en medio de una llamada a una función de librería como printf(). Si printf estaba a mitad de una operación interna que utiliza un mutex o un búfer global, y el manejador de señal intenta llamar a printf de nuevo, el programa entrará en un deadlock (bloqueo mutuo) o corromperá la memoria. Por esto, dentro de un manejador de señales solo puedes llamar a funciones que sean async-signal-safe (seguras para señales asíncronas), como write() de [POSIX].
Para comunicar el evento al resto del programa sin causar condiciones de carrera, la única forma segura de manejar una bandera (flag) es mediante el tipo volatile sig_atomic_t. El modificador volatile evita que el compilador optimice la variable asumiendo que su valor no cambia, y sig_atomic_t garantiza que la lectura y escritura de la variable sea una operación atómica, es decir, que no sea interrumpida a mitad de su ejecución a nivel de CPU.
Aunque signal() es la interfaz clásica, su comportamiento es poco consistente entre distintas implementaciones de UNIX. Para código de producción robusto, siempre deberías preferir sigaction [POSIX], que ofrece un control mucho más preciso sobre la máscara de señales y el contexto de ejecución.
#define _POSIX_C_SOURCE 200809L // Para asegurar compatibilidad con write()
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
/*
* Usamos volatile sig_atomic_t para asegurar que el cambio de estado
* sea visible inmediatamente y que el compilador no optimice la comprobación
* en el bucle principal.
*/
volatile sig_atomic_t keep_running = 1;
/*
* El manejador de señales debe ser una función con la firma: void (int).
* Solo llamamos a funciones async-signal-safe.
*/
void handle_signal(int sig) {
// printf es UNSAFE. Usamos write() de [POSIX] para evitar deadlocks.
const char *msg = "\n[Señal recibida] Preparando salida limpia...\n";
write(STDOUT_FILENO, msg, strlen(msg));
// Cambiamos el flag para que el bucle principal termine.
keep_running = 0;
}
int main(void) {
struct sigaction sa;
// Configuramos sigaction [POSIX] en lugar de signal() por robustez.
sa.sa_handler = handle_signal;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
// Registramos SIGINT (Ctrl+C)
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("Error al configurar sigaction");
return 1;
}
// Registramos SIGTERM (terminación solicitada)
if (sigaction(SIGTERM, &sa, NULL) == -1) {
perror("Error al configurar sigaction");
return 1;
}
printf("Proceso ejecutándose (PID: %d). Presiona Ctrl+C...\n", getpid());
while (keep_running) {
// Simulamos trabajo pesado
printf("Trabajando...\n");
sleep(2);
}
printf("Limpieza de recursos completada. Adiós.\n");
return 0;
}
En el ejemplo anterior, hemos utilizado sigaction [POSIX] para registrar el manejador handle_signal para las señales SIGINT y SIGTERM. Observa que en el bucle while, la condición depende de keep_running. Si no hubiéramos declarado keep_running como volatile sig_atomic_t, el compilador, al analizar el bucle while y ver que nada dentro del cuerpo de main modifica la variable, podría asumir que su valor nunca cambia y optimizar el bucle de forma que la señal nunca sea procesada efectivamente, convirtiéndolo en un bucle infinito.
Dentro de handle_signal, no podemos usar printf porque printf es una función compleja que utiliza buffers internos y bloqueos de memoria. Si el hilo principal estuviera ejecutando un printf y fuera interrumpido justo cuando tiene un lock activado, el manejador intentaría adquirir ese mismo lock y el programa se congelaría. En su lugar, usamos write(), que es una llamada al sistema directa y segura para este contexto.
El uso de sigemptyset es crucial para limpiar la máscara de señales del manejador, asegurando que no se bloqueen otras señales inesperadas mientras se atiende la actual.
El error frecuente
Un error clásico es intentar usar funciones de alto nivel como printf o malloc dentro de un manejador de señales.
// ERROR CRÍTICO
void unsafe_handler(int sig) {
// printf no es async-signal-safe.
// Si el programa fue interrumpido durante un printf() previo,
// esto causará un deadlock o corrupción de memoria.
printf("Señal %d recibida\n", sig);
}
Si intentas ejecutar este código, es probable que no pase nada (el programa se queda colgado) o que obtengas un error de segmentación intermitente. Herramientas como AddressSanitizer pueden no detectar esto siempre, pero Valgrind con el plugin de detección de señales puede ayudar a identificar comportamientos anómalos en la gestión de la pila durante la interrupción.
N° 100