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

62

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


Isto é o que eu tentei:

Em execução she depois cat:

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

Enviando SIGINTpara catoutro 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 catquando enviado ao processo pai sh?

Isso não funciona:

[user@host ~]$ kill -SIGINT $PID_OF_SH
rob87
fonte
11
O shell tem uma maneira de ignorar sinais SIGINT não enviados do teclado ou terminal.
precisa saber é o seguinte

Respostas:

86

Como CTRL+ Cfunciona

A primeira coisa é entender como CTRL+ Cfunciona.

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

Qual é o grupo de processos em primeiro plano?

Então, agora a pergunta é: como você determina qual é o grupo de processos em primeiro plano? O grupo de processos em primeiro plano é simplesmente o grupo de processos que receberá qualquer sinal gerado 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 do POSIX de enviar um sinal para todo o grupo.

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

kill -INT -1234

 


Simule CTRL+ Cusando o número do terminal.

Portanto, o exposto acima mostra como simular CTRL+ Ccomo um processo manual. Mas e se você souber o número TTY e quiser simular CTRL+ Cpara esse terminal?

Isso se torna muito fácil.

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

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

Isso enviará um SIGINT para qualquer que seja o grupo de processos em primeiro plano $tty.

Patrick
fonte
6
Vale ressaltar que os sinais que vêm diretamente da verificação de permissão de desvio do terminal, portanto, Ctrl + C sempre consegue emitir sinais, a menos que você o desative nos atributos do terminal, enquanto um killcomando pode falhar.
Brian Bi
4
+1, parasends a SIGINT to the foreground process group of the terminal.
andy
Vale ressaltar que o grupo de processos da criança é o mesmo que o pai depois fork. Exemplo mínimo de C executável em: unix.stackexchange.com/a/465112/32558
Ciro Santilli
15

Como vinc17 diz, não há razão para isso acontecer. Quando você digita uma sequência de teclas geradora de sinal (por exemplo, Ctrl+ C), o sinal é enviado a todos os processos conectados ao (associados ao) terminal. 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 matar (1) e matar (2) . Os filhos de um shell geralmente estão no grupo de processos do shell (pelo menos, se não forem assíncronos); portanto, o envio do sinal ao negativo do PID do shell pode fazer o que você deseja.


Opa

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

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

ps -pPID_of_shellobtém informações do processo no shell.  o tpgid=diz pspara emitir apenas o ID do grupo de processos do terminal, sem cabeçalho. Se for menor que 10000, psserá exibido com espaços à esquerda; Esse $(echo …)é um truque rápido para remover os espaços à esquerda (e à direita).

Eu fiz isso funcionar em testes superficiais em uma máquina Debian.

G-Man Diz 'Reinstate Monica'
fonte
11
Isso não funciona quando o processo é iniciado em um shell interativo (que é o que o OP está usando). Eu não tenho uma referência para esse comportamento, no entanto.
precisa saber é
12

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

Para ser mais preciso, o caractere de interrupção ( ^Cpor padrão) envia SIGINTpara todos os processos no grupo de processos em primeiro plano do terminal. Se em vez de catvocê estiver executando um comando mais complicado envolvendo vários processos, você teria que matar o grupo de processos para obter o mesmo efeito que ^C.

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

É aí que você parece ter se tornado vítima de um equívoco comum: a idéia de que o shell está fazendo algo para facilitar a interação entre seus processos filhos e o terminal. Isso não é verdade. Depois de concluir o trabalho de configuração (criação do processo, configuração do modo de terminal, criação de pipes e redirecionamento de outros descritores de arquivos e execução do programa de destino), o shell simplesmente espera . O que você digita catnão passa pelo shell, seja uma entrada normal ou um caractere especial gerador de sinal ^C. O catprocesso tem acesso direto ao terminal através de seus próprios descritores de arquivo, e o terminal tem a capacidade de enviar sinais diretamente ao catprocesso, porque é o grupo de processos em primeiro plano.

Depois que o catprocesso morre, o shell será notificado, porque é o pai do catprocesso. Em seguida, o shell se torna ativo e se coloca em primeiro plano novamente.

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

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

exec cat

A execpalavra-chave faz com que o shell seja executado catsem criar um processo filho. O shell é substituído por cat. O PID que anteriormente pertencia ao shell agora é o PID de cat. Verifique isso psem um terminal diferente. Digite algumas linhas aleatórias e veja que as catrepete novamente, provando que ainda está se comportando normalmente, apesar de não ter um processo de shell como pai. O que acontecerá quando você pressionar ^Cagora?

Responda:

O SIGINT é entregue ao processo do gato, que morre. Por ser o único processo no terminal, a sessão termina, como se você tivesse dito "sair" em um prompt de shell. Na verdade, o gato foi sua concha por um tempo.


fonte
A concha saiu do caminho. +1
Piotr Dobrogost 01/02
Não entendo por que, depois de exec catpressionar ^C, não caia ^Cem um gato. Por que encerraria o catque agora substituiu o shell? Desde que o shell foi substituído, o que implementa a lógica de enviar o SIGINT para seus filhos após o recebimento ^C.
Steven Lu
O ponto é que o shell não envia o SIGINT para seus filhos. O SIGINT vem do driver do terminal e é enviado para todos os processos em primeiro plano.
3

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

Se o shell propagasse o recebido SIGINT, por exemplo, após um Ctrl-C real, isso significaria que o processo filho receberia o SIGINTsinal duas vezes, o que pode ter um comportamento indesejado.

vinc17
fonte
O shell não precisa implementar isso com system(). Mas você está certo, se ele capta o sinal (obviamente), não há razão para propagá-lo para baixo.
Goldilocks
@goldilocks Eu completei minha resposta, talvez dando um motivo melhor. Observe que o shell não pode saber se a criança já recebeu o sinal, daí o problema.
precisa saber é
1

setpgid Exemplo mínimo do grupo de processos POSIX C

Pode ser mais fácil entender com um exemplo executável mínimo da API subjacente.

Isso ilustra como o sinal é enviado à criança, se a criança não mudou seu grupo de processos setpgid.

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>

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

Ajuntar com:

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

Corra sem setpgid

Sem nenhum argumento da CLI, isso setpgidnão é feito:

./setpgid

Resultado possível:

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

e o programa trava.

Como podemos ver, o pgid de ambos os processos é o mesmo, à medida que é herdado fork.

Então, sempre que você clicar em:

Ctrl + C

Ele produz novamente:

sigint parent
sigint child

Isso mostra como:

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

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

Correr com setpgid

Se você executar com um argumento, por exemplo:

./setpgid 1

então o filho altera seu pgid e agora apenas uma única assinatura é impressa todas as vezes somente do pai:

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

E agora, sempre que você clicar em:

Ctrl + C

somente os pais também recebem o sinal:

sigint parent

Você ainda pode matar o pai como antes com um SIGQUIT:

Ctrl + \

no entanto, a criança agora tem um PGID diferente e não recebe esse sinal! Isso pode ser visto em:

ps aux | grep setpgid

Você terá que matá-lo explicitamente com:

kill -9 16470

Isso deixa claro por que existem grupos de sinais: caso contrário, teríamos um monte de processos restantes para serem limpos manualmente o tempo todo.

Testado no Ubuntu 18.04.

Ciro Santilli adicionou uma nova foto
fonte