¿Por qué SIGINT no se propaga al proceso hijo cuando se envía a su proceso padre?

62

Dado un proceso de shell (p sh. Ej. ) Y su proceso hijo (p cat. Ej. ), ¿Cómo puedo simular el comportamiento de Ctrl+ Cusando la ID de proceso del shell?


Esto es lo que he intentado:

Corriendo shy luego cat:

[user@host ~]$ sh
sh-4.3$ cat
test
test

Enviar SIGINTa catotra terminal:

[user@host ~]$ kill -SIGINT $PID_OF_CAT

cat recibió la señal y terminó (como se esperaba).

Enviar la señal al proceso padre no parece funcionar. ¿Por qué no se propaga la señal catcuando se envía a su proceso padre sh?

Esto no funciona:

[user@host ~]$ kill -SIGINT $PID_OF_SH
rob87
fuente
1
El shell tiene una forma de ignorar las señales SIGINT no enviadas desde el teclado o terminal.
konsolebox

Respuestas:

86

Cómo CTRL+ Cfunciona

Lo primero es entender cómo funciona CTRL+ C.

Cuando presiona CTRL+ C, su emulador de terminal envía un carácter ETX (fin de texto / 0x03).
El TTY está configurado de tal manera que cuando recibe este carácter, envía un SIGINT al grupo de proceso en primer plano del terminal. Esta configuración se puede ver haciendo sttyy mirando intr = ^C;. La especificación POSIX dice que cuando se recibe INTR, debe enviar un SIGINT al grupo de procesos en primer plano de ese terminal.

¿Qué es el grupo de procesos en primer plano?

Entonces, ahora la pregunta es, ¿cómo se determina cuál es el grupo de procesos en primer plano? El grupo de procesos en primer plano es simplemente el grupo de procesos que recibirá cualquier señal generada por el teclado (SIGTSTOP, SIGINT, etc.).

La forma más sencilla de determinar la ID del grupo de procesos es usar ps:

ps ax -O tpgid

La segunda columna será la ID del grupo de procesos.

¿Cómo envío una señal al grupo de proceso?

Ahora que sabemos cuál es la ID del grupo de procesos, necesitamos simular el comportamiento POSIX de enviar una señal a todo el grupo.

Esto se puede hacer killponiendo un -delante de la ID del grupo.
Por ejemplo, si su ID de grupo de proceso es 1234, usaría:

kill -INT -1234

 


Simule CTRL+ Cusando el número de terminal.

Entonces, lo anterior cubre cómo simular CTRL+ Ccomo un proceso manual. Pero, ¿qué sucede si conoce el número TTY y desea simular CTRL+ Cpara ese terminal?

Esto se vuelve muy fácil.

Supongamos que $ttyes el terminal al que desea apuntar (puede obtener esto ejecutándose tty | sed 's#^/dev/##'en el terminal).

kill -INT -$(ps h -t $tty -o tpgid | uniq)

Esto enviará un SIGINT a cualquiera que sea el grupo de procesos en primer plano $tty.

Patricio
fuente
66
Vale la pena señalar que las señales que provienen directamente de la terminal omiten la verificación de permisos, por lo que Ctrl + C siempre logra entregar señales a menos que lo desactive en los atributos del terminal, mientras que un killcomando puede fallar.
Brian Bi
44
+1, parasends a SIGINT to the foreground process group of the terminal.
andy
Vale la pena mencionar que el grupo de proceso del niño es el mismo que el padre después fork. Ejemplo mínimo de C ejecutable en: unix.stackexchange.com/a/465112/32558
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件
15

Como dice vinc17, no hay razón para que esto suceda. Cuando escribe una secuencia de teclas generadoras de señal (por ejemplo, Ctrl+ C), la señal se envía a todos los procesos que están conectados (asociados con) al terminal. No existe tal mecanismo para las señales generadas por kill.

Sin embargo, un comando como

kill -SIGINT -12345

enviará la señal a todos los procesos en el grupo de procesos 12345; ver kill (1) y kill (2) . Los hijos de un shell suelen estar en el grupo de procesos del shell (al menos, si no son asíncronos), por lo que enviar la señal al negativo del PID del shell puede hacer lo que desee.


Ups

Como señala vinc17, esto no funciona para shells interactivos. Aquí hay una alternativa que podría funcionar:

kill -SIGINT - $ (echo $ (ps -p PID_of_shell o tpgid =))

ps -pPID_of_shellobtiene información del proceso en el shell.  o tpgid=le indica psa la salida solo la ID del grupo de proceso del terminal, sin encabezado. Si es inferior a 10000, pslo mostrará con espacios iniciales; Este $(echo …)es un truco rápido para quitar los espacios iniciales (y finales).

Obtuve esto para trabajar en pruebas superficiales en una máquina Debian.

G-Man dice 'restablecer a Monica'
fuente
1
Esto no funciona cuando el proceso se inicia en un shell interactivo (que es lo que está utilizando el OP). Sin embargo, no tengo una referencia para este comportamiento.
vinc17
12

La pregunta contiene su propia respuesta. Enviar el SIGINTal catproceso con killes una simulación perfecta de lo que sucede cuando presiona ^C.

Para ser más precisos, el carácter de interrupción ( ^Cpor defecto) se envía SIGINTa cada proceso en el grupo de procesos en primer plano del terminal. Si en lugar de catestar ejecutando un comando más complicado que involucra múltiples procesos, tendría que matar al grupo de procesos para lograr el mismo efecto que ^C.

Cuando ejecuta cualquier comando externo sin el &operador de fondo, el shell crea un nuevo grupo de proceso para el comando y notifica al terminal que este grupo de proceso está ahora en primer plano. El shell todavía está en su propio grupo de procesos, que ya no está en primer plano. Luego el shell espera a que salga el comando.

Ahí es donde parece haberse convertido en la víctima por un error común: la idea de que el shell está haciendo algo para facilitar la interacción entre sus procesos secundarios y el terminal. Eso no es verdad. Una vez que ha realizado el trabajo de configuración (creación del proceso, configuración del modo de terminal, creación de canalizaciones y redirección de otros descriptores de archivo y ejecución del programa de destino), el shell simplemente espera . Lo que escribe catno pasa por el shell, ya sea entrada normal o un carácter especial generador de señal como ^C. El catproceso tiene acceso directo al terminal a través de sus propios descriptores de archivo, y el terminal tiene la capacidad de enviar señales directamente al catproceso porque es el grupo de procesos en primer plano.

Después de que el catproceso muere, se notificará al shell, ya que es el padre del catproceso. Luego, el caparazón se activa y vuelve a ponerse en primer plano.

Aquí hay un ejercicio para aumentar su comprensión.

En el indicador de shell en una nueva terminal, ejecute este comando:

exec cat

La execpalabra clave hace que el shell se ejecute catsin crear un proceso secundario. La cáscara se reemplaza por cat. El PID que anteriormente pertenecía al shell ahora es el PID de cat. Verifique esto con psen una terminal diferente. Escriba algunas líneas aleatorias y vea que se las catrepite, demostrando que todavía se comporta normalmente a pesar de no tener un proceso de shell como padre. ¿Qué pasará cuando presiones ^Cahora?

Responder:

SIGINT se entrega al proceso del gato, que muere. Debido a que era el único proceso en la terminal, la sesión finaliza, como si hubiera dicho "salir" en un indicador de comandos de shell. En efecto, el gato fue tu caparazón por un tiempo.


fuente
El caparazón se ha salido del camino. +1
Piotr Dobrogost
No entiendo por qué después de exec catpresionar ^Cno aterrizaría ^Cen gato. ¿Por qué terminaría el catque ahora ha reemplazado el shell? Dado que el shell ha sido reemplazado, el shell es lo que implementa la lógica de enviar SIGINT a sus hijos al recibirlo ^C.
Steven Lu
El punto es que el shell no envía SIGINT a sus hijos. El SIGINT proviene del controlador de terminal y se envía a todos los procesos en primer plano.
3

No hay razón para propagarlo SIGINTal niño. Además, la system()especificación POSIX dice: "La función system () ignorará las señales SIGINT y SIGQUIT, y bloqueará la señal SIGCHLD, mientras espera que termine el comando".

Si el shell propaga lo recibido SIGINT, por ejemplo, siguiendo un Ctrl-C real, esto significaría que el proceso secundario recibiría la SIGINTseñal dos veces, lo que puede tener un comportamiento no deseado.

vinc17
fuente
El shell no tiene que implementar esto con system(). Pero tiene razón, si capta la señal (obviamente lo hace), entonces no hay razón para propagarla hacia abajo.
Ricitos
@goldilocks He completado mi respuesta, quizás dando una mejor razón. Tenga en cuenta que el shell no puede saber si el niño ya ha recibido la señal, de ahí el problema.
vinc17
1

setpgid Ejemplo mínimo del grupo de procesos POSIX C

Puede ser más fácil de entender con un ejemplo ejecutable mínimo de la API subyacente.

Esto ilustra cómo se envía la señal al niño, si el niño no cambió su grupo de proceso setpgid.

C Principal

#define _XOPEN_SOURCE 700
#include <assert.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

volatile sig_atomic_t is_child = 0;

void signal_handler(int sig) {
    char parent_str[] = "sigint parent\n";
    char child_str[] = "sigint child\n";
    signal(sig, signal_handler);
    if (sig == SIGINT) {
        if (is_child) {
            write(STDOUT_FILENO, child_str, sizeof(child_str) - 1);
        } else {
            write(STDOUT_FILENO, parent_str, sizeof(parent_str) - 1);
        }
    }
}

int main(int argc, char **argv) {
    pid_t pid, pgid;

    (void)argv;
    signal(SIGINT, signal_handler);
    signal(SIGUSR1, signal_handler);
    pid = fork();
    assert(pid != -1);
    if (pid == 0) {
        is_child = 1;
        if (argc > 1) {
            /* Change the pgid.
             * The new one is guaranteed to be different than the previous, which was equal to the parent's,
             * because `man setpgid` says:
             * > the child has its own unique process ID, and this PID does not match
             * > the ID of any existing process group (setpgid(2)) or session.
             */
            setpgid(0, 0);
        }
        printf("child pid, pgid = %ju, %ju\n", (uintmax_t)getpid(), (uintmax_t)getpgid(0));
        assert(kill(getppid(), SIGUSR1) == 0);
        while (1);
        exit(EXIT_SUCCESS);
    }
    /* Wait until the child sends a SIGUSR1. */
    pause();
    pgid = getpgid(0);
    printf("parent pid, pgid = %ju, %ju\n", (uintmax_t)getpid(), (uintmax_t)pgid);
    /* man kill explains that negative first argument means to send a signal to a process group. */
    kill(-pgid, SIGINT);
    while (1);
}

GitHub aguas arriba .

Compilar con:

gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -Wpedantic -o setpgid setpgid.c

Correr sin setpgid

Sin ningún argumento CLI, setpgidno se hace:

./setpgid

Posible resultado:

child pid, pgid = 28250, 28249
parent pid, pgid = 28249, 28249
sigint parent
sigint child

y el programa se cuelga.

Como podemos ver, el pgid de ambos procesos es el mismo, ya que se hereda fork.

Luego, cada vez que golpeas:

Ctrl + C

Sale de nuevo:

sigint parent
sigint child

Esto muestra cómo:

  • enviar una señal a todo un grupo de procesos con kill(-pgid, SIGINT)
  • Ctrl + C en el terminal envía un kill a todo el grupo de procesos por defecto

Salga del programa enviando una señal diferente a ambos procesos, por ejemplo, SIGQUIT con Ctrl + \.

Corre con setpgid

Si corre con un argumento, por ejemplo:

./setpgid 1

luego el niño cambia su pgid, y ahora solo se imprime una sola señal cada vez solo del padre:

child pid, pgid = 16470, 16470
parent pid, pgid = 16469, 16469
sigint parent

Y ahora, cada vez que golpeas:

Ctrl + C

solo el padre recibe la señal también:

sigint parent

Todavía puedes matar al padre como antes con un SIGQUIT:

Ctrl + \

¡Sin embargo, el niño ahora tiene un PGID diferente y no recibe esa señal! Esto se puede ver en:

ps aux | grep setpgid

Tendrás que matarlo explícitamente con:

kill -9 16470

Esto deja en claro por qué existen grupos de señales: de lo contrario, nos quedarían un montón de procesos que se limpiarían manualmente todo el tiempo.

Probado en Ubuntu 18.04.

Ciro Santilli 新疆 改造 中心 法轮功 六四 事件
fuente