Controlar procesos con senales - Procesos e Hilos
Volver a Procesos e Hilos Bloque 5 - Esperar y terminar - Tema 23 de 43

Tema 23

Controlar procesos con senales

Las senales son el mecanismo de comunicacion asincrona entre procesos. Permiten terminar, detener, reanudar y notificar a procesos. En este tema veras las senales más importantes (SIGTERM, SIGKILL, SIGSTOP, SIGCONT, SIGCHLD), Como enviarlas con kill y raise, y como manejarlas con sigaction.

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:

Las senales son interrupciones de software

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:

SenalNumeroAccion por omisionDescripcion
SIGTERM15TerminarTerminacion elegante. Se puede capturar.
SIGKILL9Terminar (forzado)Terminacion inmediata. No se puede capturar ni ignorar.
SIGINT2TerminarInterrupcion desde terminal (Ctrl+C). Se puede capturar.
SIGSTOP19DetenerDetiene el proceso. No se puede capturar ni ignorar.
SIGTSTP20DetenerDetencion desde terminal (Ctrl+Z). Se puede capturar.
SIGCONT18ContinuarReanuda un proceso detenido.
SIGCHLD17IgnorarUn hijo terminó o se detuvo.
SIGSEGV11Terminar + coreViolacion de segmento.
SIGABRT6Terminar + coreAbort (llamada a abort()).
SIGFPE8Terminar + coreError aritmetico (division por cero).
SIGALRM14TerminarTemporizador (alarm).
SIGUSR110TerminarSenal definida por el usuario.
SIGUSR212TerminarSenal definida por el usuario.
SIGPIPE13TerminarEscritura en una tuberia sin lectores.
SIGWINCH28IgnorarCambio de tamano de la terminal.

Las senales se pueden listar con kill -l en la shell:

terminal - listar senales

$ 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>:

C - firma de kill

#include 

int kill(pid_t pid, int sig);

El parámetro pid determina a quién se envia la senal:

Valor de pidSignificado
Mayor que 0Enviar al proceso con ese PID.
0Enviar a todos los procesos del mismo grupo que el emisor.
-1Enviar a todos los procesos que el emisor tiene permiso de senalar (excepto init).
Menor que -1Enviar 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).

enviar_senal.c

#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.

C - firma de raise

#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)).

raise_demo.c

#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:

  1. Instalar un manejador de SIGTERM: Que establece un flag (volatile sig_atomic_t).
  2. El bucle principal verifica el flag: Sí esta activo, sale del bucle.
  3. Despues del bucle: hacer limpieza (cerrar archivos, guardar estado) y terminar.
Sigterm_handler.c

#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.

No hacer limpieza en el manejador

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:

SIGKILL no es elegante

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:

CaracteristicaSIGTERM (15)SIGKILL (9)
Se puede capturarSiNo
Se puede ignorarSiNo
Ejecuta atexitSi (Sí capturada y sale con exit)No
Vacia buferes de stdioSiNo
TerminacionElegante (el proceso decide cuándo)Inmediata (el núcleo mata al proceso)
Uso tipicoTerminacion normalUltimo 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.

terminal - detener y reanudar

$ 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():

detener_reanudar.c

#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).

terminal - ignorar SIGCHLD

$ 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 debe ser simple

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:

  1. El manejador establece un flag: terminar = 1;
  2. El bucle principal verifica el flag: Sí esta activo, sale del bucle.
  3. 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:

CaracteristicaSignal()Sigaction()
PortabilidadPOSIX, pero comportamiento varia entre sistemasPOSIX, comportamiento estandar
FlagsNo soporta flagsSoporta SA_RESTART, SA_NOCLDSTOP, etc.
Mascara de senalesNo soportaPermite bloquear senales durante el manejador
Informacion adicionalNoSiginfo_t con detalles de la senal
RecomendacionUsar sólo para compatibilidadUsar siempre

Sigaction se usa así:

C - Sigaction

#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).

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.

terminal - enviar a grupo de procesos

$ 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:

control_senales.c

#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;
}
terminal - ejecutar control_senales

$ 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

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

  1. Compila y ejecuta enviar_senal.c. Observa como el padre termina al hijo con SIGTERM.
  2. Compila y ejecuta detener_reanudar.c. Observa como el hijo se detiene y se reanuda.
  3. Compila y ejecuta control_senales.c. Observa el control completo: crear, detener, reanudar, terminar.
  4. Escribe un programa que capture SIGINT (Ctrl+C) y haga limpieza antes de terminar.
  5. Usa kill -l para listar todas las senales de tu sistema.
  6. Escribe un programa que use SIGUSR1 para comunicar entre padre e hijo.