1. Qué son las senales
Una senal es una notificacion asincrona que el núcleo o un proceso envia a otro proceso. Es un mecanismo de comunicacion de un sólo bit: la senal dice "algo paso", pero no lleva datos. El proceso que recibe la senal puede ignorarla, capturarla (ejecutar un manejador), o dejar que se aplique la accion por omision.
Las senales son el mecanismo más antiguo de comunicacion entre procesos en unix. Son asincronas: pueden llegar en cualquier momento, interrumpiendo al proceso en medio de una instrucción. Esto hace que el manejo de senales sea delicado: el manejador debe ser async-Signal-safe.
Las senales se usan para:
- Terminar procesos: SIGTERM, SIGKILL, SIGINT.
- Detener y reanudar: SIGSTOP, SIGTSTP, SIGCONT.
- Notificar eventos: SIGCHLD (hijo terminado), SIGALRM (temporizador).
- Reportar errores: SIGSEGV (violación de segmento), SIGFPE (error aritmetico).
- Comunicacion entre procesos: SIGUSR1, SIGUSR2 (definidas por el usuario).
Las senales son como interrupciones de hardware, pero generadas por software. Cuando un proceso recibe una senal, el núcleo interrumpe su ejecución, ejecuta el manejador (Sí hay uno), y luego retorna al proceso. Si no hay manejador, se aplica la accion por omision (terminar, ignorar, detener, etc.).
2. Senales comunes
Hay unas 30 senales estandar en linux. Las más importantes son:
| Senal | Numero | Accion por omision | Descripcion |
|---|---|---|---|
| SIGTERM | 15 | Terminar | Terminacion elegante. Se puede capturar. |
| SIGKILL | 9 | Terminar (forzado) | Terminacion inmediata. No se puede capturar ni ignorar. |
| SIGINT | 2 | Terminar | Interrupcion desde terminal (Ctrl+C). Se puede capturar. |
| SIGSTOP | 19 | Detener | Detiene el proceso. No se puede capturar ni ignorar. |
| SIGTSTP | 20 | Detener | Detencion desde terminal (Ctrl+Z). Se puede capturar. |
| SIGCONT | 18 | Continuar | Reanuda un proceso detenido. |
| SIGCHLD | 17 | Ignorar | Un hijo terminó o se detuvo. |
| SIGSEGV | 11 | Terminar + core | Violacion de segmento. |
| SIGABRT | 6 | Terminar + core | Abort (llamada a abort()). |
| SIGFPE | 8 | Terminar + core | Error aritmetico (division por cero). |
| SIGALRM | 14 | Terminar | Temporizador (alarm). |
| SIGUSR1 | 10 | Terminar | Senal definida por el usuario. |
| SIGUSR2 | 12 | Terminar | Senal definida por el usuario. |
| SIGPIPE | 13 | Terminar | Escritura en una tuberia sin lectores. |
| SIGWINCH | 28 | Ignorar | Cambio de tamano de la terminal. |
Las senales se pueden listar con kill -l en la shell:
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
...
3. kill: enviar senales
La función kill() envia una senal a un proceso o a un grupo de procesos. Se declara en <Signal.h>:
#include
int kill(pid_t pid, int sig);
El parámetro pid determina a quién se envia la senal:
| Valor de pid | Significado |
|---|---|
| Mayor que 0 | Enviar al proceso con ese PID. |
| 0 | Enviar a todos los procesos del mismo grupo que el emisor. |
| -1 | Enviar a todos los procesos que el emisor tiene permiso de senalar (excepto init). |
| Menor que -1 | Enviar a todos los procesos del grupo cuyo ID es |pid|. |
El parámetro Sig es el número de la senal (o 0 para verificar sí el proceso existe sin enviar senal).
kill retorna 0 Sí tiene exito, o -1 Sí falla (errno = ESRCH: proceso no existe, EPERM: Sin permiso).
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
#include
int main(void)
{
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) {
/* Hijo: esperar senales */
printf("Hijo (PID %d) esperando senales...\n", (int) getpid());
while (1) pause(); /* esperar cualquier senal */
}
/* Padre: enviar senales */
sleep(1);
printf("Padre: enviando SIGTERM al hijo\n");
kill(pid, SIGTERM);
int status;
waitpid(pid, &status, 0);
if (WIFSIGNALED(status))
printf("Padre: hijo terminado por senal %d\n", WTERMSIG(status));
return 0;
}
4. raise: senal a sí mismo
raise() envia una senal al propio proceso. Es equivalente a kill(getpid(), Sig), pero más portable y más claro.
#include
int raise(int sig);
raise es útil para que un proceso se envie una senal a sí mismo, por ejemplo para abortar (raise(SIGABRT)) o para detenerse (raise(SIGSTOP)).
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
void manejador(int sig)
{
printf("Recibi senal %d\n", Sig);
}
int main(void)
{
Signal(SIGUSR1, manejador);
printf("Enviando SIGUSR1 a mi mismo...\n");
raise(SIGUSR1);
printf("Continuando después de la senal\n");
return 0;
}
5. SIGTERM: terminación elegante
SIGTERM (senal 15) es la senal estandar para pedir a un proceso que termine. Es la senal que envia kill por omision (Sin especificar senal). El proceso puede capturarla y hacer limpieza antes de terminar.
SIGTERM es la forma elegante de terminar un proceso: el proceso tiene la oportunidad de guardar datos, cerrar archivos, y terminar ordenadamente. Por eso, los demonios y servidores deben capturar SIGTERM y hacer limpieza.
El patron tipico es:
- Instalar un manejador de SIGTERM: Que establece un flag (volatile sig_atomic_t).
- El bucle principal verifica el flag: Sí esta activo, sale del bucle.
- Despues del bucle: hacer limpieza (cerrar archivos, guardar estado) y terminar.
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
static volatile sig_atomic_t terminar = 0;
void manejador_Sigterm(int sig)
{
(void) Sig;
terminar = 1; /* Sólo establecer un flag */
}
int main(void)
{
struct sigaction sa;
sa.sa_handler = manejador_Sigterm;
Sigemptyset(&sa.sa_Mask);
sa.sa_flags = SA_RESTART;
Sigaction(SIGTERM, &sa, NULL);
printf("PID %d: esperando SIGTERM...\n", (int) getpid());
while (!terminar) {
printf("Trabajando...\n");
sleep(1);
}
/* Limpieza */
printf("Recibi SIGTERM, limpiando...\n");
/* cerrar archivos, guardar estado, etc. */
printf("Terminando\n");
return 0;
}
El manejador sólo establece un flag. El bucle principal verifica el flag y sale. Despues del bucle, se hace la limpieza. Esto es el patron recomendado para manejar SIGTERM.
El manejador de SIGTERM no debe hacer limpieza (cerrar archivos, liberar memoria, etc.) porque sólo las funciónes async-Signal-safe se pueden llamar en un manejador. La limpieza se hace en el bucle principal, Después de que el manejador establecio el flag.
6. SIGKILL: terminación forzada
SIGKILL (senal 9) es la senal que termina un proceso inmediatamente, Sin darle oportunidad de hacer limpieza. No se puede capturar, ignorar, ni bloquear. Es la "ultima opcion" Cuándo un proceso no responde a SIGTERM.
SIGKILL es la senal que envia kill -9. Cuando un proceso recibe SIGKILL, el núcleo lo termina inmediatamente: no se ejecutan las funciónes atexit, no se vacian los buferes de stdio, no se cierran los archivos (el núcleo los cierra al liberar el proceso, pero no se ejecuta el código de limpieza del programa).
SIGKILL se usa cuándo:
- Un proceso no responde a SIGTERM (esta bloqueado o en un bucle infinito).
- Un proceso esta en estado D (dormido ininterruptible) y no se puede matar con otras senales.
- Se necesita terminar un proceso inmediatamente, Sin esperar.
SIGKILL no da al proceso oportunidad de guardar datos o cerrar archivos ordenadamente. Usar SIGTERM primero, y SIGKILL Sólo sí SIGTERM no funcióna. Un programa bien escrito captura SIGTERM y hace limpieza.
La diferencia entre SIGTERM y SIGKILL es fundamental:
| Caracteristica | SIGTERM (15) | SIGKILL (9) |
|---|---|---|
| Se puede capturar | Si | No |
| Se puede ignorar | Si | No |
| Ejecuta atexit | Si (Sí capturada y sale con exit) | No |
| Vacia buferes de stdio | Si | No |
| Terminacion | Elegante (el proceso decide cuándo) | Inmediata (el núcleo mata al proceso) |
| Uso tipico | Terminacion normal | Ultimo recurso |
7. SIGSTOP y SIGCONT
SIGSTOP (senal 19) detiene un proceso incondicionalmente. No se puede capturar, ignorar ni bloquear. El proceso se detiene inmediatamente, Sin ejecutar código.
SIGCONT (senal 18) reanuda un proceso detenido. Si el proceso no estaba detenido, SIGCONT se ignora (no tiene efecto).
SIGSTOP y SIGCONT son las senales que usa la shell para implementar ctrl+Z (SIGTSTP) y fg/bg. Ctrl+Z envia SIGTSTP (Que se puede capturar), y la shell envia SIGCONT Cuándo se reanuda el proceso.
$ sleep 100 &
[1] 1234
$ kill -STOP 1234
$ ps -o pid,stat,comm -p 1234
PID STAT COMMAND
1234 T sleep
$ kill -CONT 1234
$ ps -o pid,stat,comm -p 1234
PID STAT COMMAND
1234 S sleep
En C, se pueden enviar estas senales con kill():
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
#include
int main(void)
{
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) {
while (1) {
printf("Hijo trabajando...\n");
sleep(1);
}
}
/* Padre: detener al hijo después de 3 Segúndos */
sleep(3);
printf("Padre: deteniendo al hijo\n");
kill(pid, SIGSTOP);
sleep(2);
printf("Padre: reanudando al hijo\n");
kill(pid, SIGCONT);
sleep(3);
printf("Padre: terminando al hijo\n");
kill(pid, SIGTERM);
wait(NULL);
return 0;
}
8. SIGCHLD
SIGCHLD (senal 17) se envia al padre cuándo un hijo termina, se detiene o se reanuda. Por omision, se ignora. Pero se puede capturar para recoger a los hijos terminados.
SIGCHLD es la base de la gestion asincrona de hijos: el padre recibe SIGCHLD Cuándo un hijo termina, y en el manejador recoge al hijo con waitpid(WNOHANG).
El flag SA_NOCLDSTOP hace que SIGCHLD Sólo se envie cuándo un hijo termina, no cuándo se detiene. El flag SA_NOCLDWAIT hace que los hijos terminados se recogan automaticamente (Sin quedar como zombis).
$ cat /proc/sys/kernel/core_pattern
core
$ # En un programa:
$ # Signal(SIGCHLD, SIG_IGN); /* los hijos se recogen automaticamente */
Ignorar SIGCHLD es la forma más simple de evitar zombis en linux. Cuando SIGCHLD se ignora, los hijos terminados se recogen automaticamente por el núcleo, Sin necesidad de llamar a wait().
9. Manejadores de senales
Un manejador de senal es una función que se ejecuta cuándo el proceso recibe una senal. Se instala con Signal() o Sigaction().
El manejador se ejecuta en el contexto del proceso: interrumpe al proceso en medio de una instrucción, ejecuta el manejador, y luego retorna al proceso (Sí la senal no lo terminó).
El manejador debe ser async-Signal-safe: Sólo puede llamar a funciónes que son seguras en un contexto de senal. Las funciónes seguras incluyen: write, _exit, waitpid (con WNOHANG), kill, Sigaction, etc. Las funciónes no seguras incluyen: printf, malloc, free, etc.
El manejador de senal debe ser lo más simple posible: establecer un flag (volatile sig_atomic_t) y retornar. La limpieza y el procesamiento se hacen en el bucle principal, no en el manejador.
El patron recomendado es:
- El manejador establece un flag:
terminar = 1; - El bucle principal verifica el flag: Sí esta activo, sale del bucle.
- Despues del bucle: hacer limpieza y terminar.
10. Sigaction vs signal
Signal() es la forma antigua de instalar un manejador. Sigaction() es la forma moderna y recomendada. Las diferencias son:
| Caracteristica | Signal() | Sigaction() |
|---|---|---|
| Portabilidad | POSIX, pero comportamiento varia entre sistemas | POSIX, comportamiento estandar |
| Flags | No soporta flags | Soporta SA_RESTART, SA_NOCLDSTOP, etc. |
| Mascara de senales | No soporta | Permite bloquear senales durante el manejador |
| Informacion adicional | No | Siginfo_t con detalles de la senal |
| Recomendacion | Usar sólo para compatibilidad | Usar siempre |
Sigaction se usa así:
#define _POSIX_C_SOURCE 200809L
#include
struct sigaction sa;
sa.sa_handler = manejador; /* Función manejadora */
Sigemptyset(&sa.sa_Mask); /* no bloquear senales adicionales */
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
Sigaction(SIGTERM, &sa, NULL); /* instalar */
SA_RESTART hace que las syscalls interrumpidas por la senal se reinicien automaticamente. SA_NOCLDSTOP evita que SIGCHLD se envie cuándo un hijo se detiene (Sólo cuándo termina).
11. Enviar a grupos de procesos
kill puede enviar senales a grupos de procesos, no sólo a procesos individuales. Esto es útil para terminar todos los procesos de un grupo (por ejemplo, todos los procesos de una sesion de terminal).
kill(0, Sig):enviar a todos los procesos del mismo grupo que el emisor.kill(-pgid, Sig):enviar a todos los procesos del grupo pgid.kill(-1, Sig):enviar a todos los procesos que el emisor tiene permiso de senalar.
En la shell, kill %1 envia una senal al grupo de procesos del trabajo 1. kill -- -1234 envia a todos los procesos del grupo 1234.
$ sleep 100 & sleep 200 &
[1] 1234
[2] 1235
$ kill -- -1234 # enviar SIGTERM a todo el grupo
$ jobs
[1]- Terminated sleep 100
[2]+ Terminated sleep 200
Enviar a un grupo de procesos es útil para terminar todos los procesos de un servicio o de una sesion. Pero hay que tener cuidado: puede afectar a procesos que no se querian terminar.
12. Un programa que demuestra control con senales
El siguiente programa crea un hijo, lo detiene, lo reanuda, y finalmente lo termina, demostrando el control con senales:
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
#include
int main(void)
{
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) {
/* Hijo: imprimir su PID y esperar senales */
printf("Hijo (PID %d) iniciado\n", (int) getpid());
while (1) {
printf("Hijo trabajando...\n");
sleep(1);
}
}
/* Padre: controlar al hijo */
printf("Padre: hijo creado (PID %d)\n", (int) pid);
/* Esperar 3 Segúndos */
sleep(3);
printf("Padre: deteniendo al hijo (SIGSTOP)\n");
kill(pid, SIGSTOP);
/* Esperar 2 Segúndos mientras el hijo esta detenido */
sleep(2);
printf("Padre: reanudando al hijo (SIGCONT)\n");
kill(pid, SIGCONT);
/* Esperar 3 Segúndos */
sleep(3);
printf("Padre: terminando al hijo (SIGTERM)\n");
kill(pid, SIGTERM);
/* Esperar al hijo */
int status;
waitpid(pid, &status, 0);
if (WIFSIGNALED(status))
printf("Padre: hijo terminado por senal %d\n", WTERMSIG(status));
else if (WIFEXITED(status))
printf("Padre: hijo terminó con código %d\n", WEXITSTATUS(status));
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o control_senales control_senales.c
$ ./control_senales
Hijo (PID 1030) iniciado
Hijo trabajando...
Hijo trabajando...
Hijo trabajando...
Padre: hijo creado (PID 1030)
Padre: deteniendo al hijo (SIGSTOP)
Padre: reanudando al hijo (SIGCONT)
Hijo trabajando...
Hijo trabajando...
Hijo trabajando...
Padre: terminando al hijo (SIGTERM)
Padre: hijo terminado por senal 15
El programa demuestra el control completo: crear, detener (SIGSTOP), reanudar (SIGCONT), y terminar (SIGTERM). El hijo se detiene durante 2 Segúndos (no imprime "trabajando") y se reanuda después.
13. Resumen
- Las senales son notificaciones asincronas entre procesos.
- SIGTERM (15) pide terminación elegante. Se puede capturar.
- SIGKILL (9) termina inmediatamente. No se puede capturar ni ignorar.
- SIGSTOP (19) detiene incondicionalmente. SIGCONT (18) reanuda.
- SIGCHLD (17) notifica que un hijo terminó.
- kill() envia una senal a un proceso o grupo.
- raise() envia una senal al propio proceso.
- Sigaction() es la forma recomendada de instalar manejadores.
- El manejador debe ser simple: establecer un flag.
- kill puede enviar a grupos de procesos (pid 0, -1, -pgid).
14. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. Qué es una senal?
Una notificacion asincrona que el núcleo o un proceso envia a otro proceso. Es un mecanismo de comunicacion de un sólo bit.
2. Qué diferencia hay entre SIGTERM y SIGKILL?
SIGTERM (15) pide terminación elegante y se puede capturar. SIGKILL (9) termina inmediatamente y no se puede capturar ni ignorar.
3. Qué senales no se pueden capturar ni ignorar?
SIGKILL (9) y SIGSTOP (19). Son las unicas senales que no se pueden capturar ni ignorar.
4. Qué es SIGCHLD?
Una senal que se envia al padre cuándo un hijo termina, se detiene o se reanuda. Se usa para recoger hijos asincronicamente.
5. Qué es sigaction?
La función moderna para instalar manejadores de senales. Permite configurar flags, Máscara de senales, y es más portable que signal().
6. Qué es async-Signal-safe?
Una función que se puede llamar de forma segura dentro de un manejador de senales. Incluye write, _exit, waitpid (con WNOHANG), kill, etc.
7. Cómo se envia una senal a un grupo de procesos?
Con kill(0, Sig) (mismo grupo), kill(-pgid, Sig) (grupo especÃfico), o kill(-1, Sig) (todos los procesos).
8. Qué hace SA_RESTART?
Hace que las syscalls interrumpidas por la senal se reinicien automaticamente, en lugar de fallar con EINTR.
9. Por que el manejador debe ser simple?
Porque sólo las funciónes async-Signal-safe se pueden llamar en un manejador. La limpieza se hace en el bucle principal.
10. Qué es raise()?
Una función que envia una senal al propio proceso. Equivalente a kill(getpid(), Sig).
Para prácticar
- Compila y ejecuta enviar_senal.c. Observa como el padre termina al hijo con SIGTERM.
- Compila y ejecuta detener_reanudar.c. Observa como el hijo se detiene y se reanuda.
- Compila y ejecuta control_senales.c. Observa el control completo: crear, detener, reanudar, terminar.
- Escribe un programa que capture SIGINT (Ctrl+C) y haga limpieza antes de terminar.
- Usa kill -l para listar todas las senales de tu sistema.
- Escribe un programa que use SIGUSR1 para comunicar entre padre e hijo.