Por que o SIGINT não é propagado para o processo filho quando enviado para seu processo pai?

53

Dado um processo shell (por exemplo, sh ) e seu processo filho (por exemplo, cat ), como eu posso simular o comportamento de Ctrl + C usando o ID do processo do shell?

Isso é o que eu tentei:

Executando sh e, em seguida, cat :

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

Enviando SIGINT para cat de outro terminal:

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

cat recebeu o sinal e terminou (como esperado).

Enviar o sinal para o processo pai parece não funcionar. Por que o sinal não é propagado para cat quando enviado para seu processo pai sh ?

Isso não funciona:

[user@host ~]$ kill -SIGINT $PID_OF_SH
    
por rob87 11.08.2014 / 20:27

5 respostas

68

Como CTRL + C funciona

A primeira coisa é entender como funciona o CTRL + C .

Quando você pressiona CTRL + C , o emulador de terminal envia um caractere ETX (fim do texto / 0x03).
O TTY é configurado de tal forma que, quando recebe este caractere, ele envia um SIGINT para o grupo de processos de primeiro plano do terminal. Essa configuração pode ser visualizada fazendo stty e observando intr = ^C; . A especificação POSIX diz que quando o INTR é recebido, ele deve enviar um SIGINT para o primeiro plano grupo de processos desse terminal.

O que é o grupo de processos em primeiro plano?

Então, agora a questão é: como você determina o que é o grupo de processos em primeiro plano? O grupo de processos em primeiro plano é simplesmente o grupo de processos que receberão quaisquer sinais gerados pelo teclado (SIGTSTOP, SIGINT, etc).

A maneira mais simples de determinar o ID do grupo de processos é usar ps :

ps ax -O tpgid

A segunda coluna será o ID do grupo de processos.

Como envio um sinal para o grupo de processos?

Agora que sabemos qual é o ID do grupo de processos, precisamos simular o comportamento POSIX de enviar um sinal para o grupo inteiro.

Isso pode ser feito com kill colocando - na frente do ID do grupo.
Por exemplo, se o seu ID de grupo de processos for 1234, você usaria:

kill -INT -1234

Simule CTRL + C usando o número do terminal.

Portanto, o texto acima mostra como simular CTRL + C como um processo manual. Mas e se você souber o número TTY e quiser simular CTRL + C para aquele terminal?

Isso se torna muito fácil.

Vamos supor que $tty é o terminal que você deseja segmentar (você pode obter isso executando tty | sed 's#^/dev/##' no terminal).

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

Isso enviará uma SIGINT para qualquer grupo de processos em primeiro plano de $tty .

    
por 11.08.2014 / 22:24
14

Como vinc17 diz, não há razão para isso acontecer. Quando você digita uma sequência de chave geradora de sinal (por exemplo, Ctrl + C ), o sinal é enviado para todos os processos conectados ao terminal (associados ao). Não existe esse mecanismo para sinais gerados por kill .

No entanto, um comando como

kill -SIGINT -12345

enviará o sinal para todos os processos no grupo de processos 12345; veja mate (1) e kill (2) . Filhos de uma concha normalmente estão no grupo de processos da shell (pelo menos, se eles não são assíncronos), então enviar o sinal para o negativo do PID do shell pode fazer o que você quiser.

Ops

Como vinc17 aponta, isso não funciona para shells interativos. Aqui está uma alternativa que pode funcionar:

kill -SIGINT -$(echo $(ps -pPID_of_shell o tpgid=))

ps -pPID_of_shell obtém informações sobre o processo no shell. o tpgid= informa ps para gerar apenas o ID do grupo de processos do terminal, sem cabeçalho. Se isso for menor que 10000, ps irá exibi-lo com espaço (s) à esquerda; o $(echo …) é um truque rápido para remover os espaços iniciais (e posteriores).

Eu consegui fazer isso funcionar em testes rápidos em uma máquina Debian.

    
por 11.08.2014 / 20:54
12

A pergunta contém sua própria resposta. Enviar o SIGINT para o processo cat com kill é uma simulação perfeita do que acontece quando você pressiona ^C .

Para ser mais preciso, o caractere de interrupção ( ^C por padrão) envia SIGINT para cada processo no grupo de processos de primeiro plano do terminal. Se, em vez de cat , você estivesse executando um comando mais complicado envolvendo vários processos, teria que eliminar o grupo de processos para obter o mesmo efeito que ^C .

Quando você executa qualquer comando externo sem o operador & background, o shell cria um novo grupo de processos para o comando e notifica o terminal de que esse grupo de processos agora está em primeiro plano. O shell ainda está em seu próprio grupo de processos, que não está mais em primeiro plano. Então o shell aguarda o comando sair.

É aí que você parece ter se tornado vítima por um equívoco comum: a ideia de que a casca está fazendo algo para facilitar a interação entre o (s) processo (s) infantil (ais) e o terminal. Isso não é verdade. Uma vez feito o trabalho de configuração (criação do processo, configuração do terminal, criação de pipes e redirecionamento de outros descritores de arquivos, e execução do programa alvo) o shell apenas aguarda . O que você digita em cat não está passando pelo shell, seja uma entrada normal ou um caractere especial gerador de sinal como ^C . O processo cat tem acesso direto ao terminal por meio de seus próprios descritores de arquivos, e o terminal tem a capacidade de enviar sinais diretamente para o processo cat porque é o grupo de processos em primeiro plano. A casca saiu do caminho.

Após o processo cat morrer, o shell será notificado porque é o pai do processo cat . Então o shell se torna ativo e se coloca novamente em primeiro plano.

Aqui está um exercício para aumentar sua compreensão.

No prompt do shell em um novo terminal, execute este comando:

exec cat

A palavra-chave exec faz com que o shell execute cat sem criar um processo filho. O shell é substituído por cat . O PID que antigamente pertencia ao shell agora é o PID de cat . Verifique isso com ps em um terminal diferente. Digite algumas linhas aleatórias e veja que cat as repete para você, provando que ainda está se comportando normalmente apesar de não ter um processo shell como pai. O que acontecerá quando você pressionar ^C agora?

Resposta:

SIGINT is delivered to the cat process, which dies. Because it was the only process on the terminal, the session ends, just as if you'd said "exit" at a shell prompt. In effect cat was your shell for a while.

    
por 11.08.2014 / 22:32
3

Não há motivo para propagar o SIGINT para o filho. Além disso, a system() especificação POSIX diz: "A função system () deve ignorar o SIGINT e SIGQUIT sinaliza, e deve bloquear o sinal SIGCHLD, enquanto aguarda o término do comando. "

Se o shell propagou o SIGINT recebido, por ex. seguindo um Ctrl-C real, isso significaria que o processo filho receberia o sinal SIGINT duas vezes, o que pode ter um comportamento indesejado.

    
por 11.08.2014 / 20:40
0

setpgid exemplo mínimo do grupo de processos POSIX C

Pode ser mais fácil entender o que está acontecendo em detalhes estudando a API subjacente.

main.c:

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

void signal_handler(int sig) {
    char sigint_str[] = "sigint\n";
    if (sig == SIGINT) {
        write(STDOUT_FILENO, sigint_str, sizeof(sigint_str));
    }
    signal(sig, signal_handler);
}

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

    signal(SIGINT, signal_handler);
    signal(SIGUSR1, signal_handler);
    pid = fork();
    assert(pid != -1);
    if (pid == 0) {
        /* 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.
         */
        if (argc == 1) {
            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);
}

Compile e execute:

gcc -g -std=c99 -Wall -Wextra -o setpgid setpgid.c -lpthread
./setpgid

Resultado:

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

e o programa trava.

O pgid dos dois processos é o mesmo, pois é herdado em fork .

Então, se você acertar:

Ctrl + C

Emite novamente:

sigint
sigint

Isto é como:

  • para enviar um sinal para um grupo de processos inteiro com kill(-pgid, SIGINT)
  • Ctrl + C no terminal envia um kill para todo o grupo de processos por padrão

Se você tiver um argumento, por exemplo:

./setpgid 1

o filho muda seu pgid, e agora apenas um único sigint é impresso toda vez: os pais.

Saia do programa enviando um sinal diferente para os processos, por ex. SIGQUIT com Ctrl + \ .

Se você mudou o id do grupo filho, ele também não receberá este SIGQUIT, e você deverá eliminá-lo explicitamente:

ps aux | grep setpgid
kill -9 $PID

Isso deixa claro por que o sinal é enviado para todos os processos por padrão: caso contrário, teríamos um monte de processos para serem limpos manualmente.

Testado no Ubuntu 18.04. GitHub upstream .

    
por 27.08.2018 / 17:41