SIGINT가 상위 프로세스로 전송 될 때 하위 프로세스로 전파되지 않는 이유는 무엇입니까?


62

쉘 프로세스 (예 :) sh와 자식 프로세스 (예 :)가 주어지면 쉘의 프로세스 ID를 사용하여 + cat의 동작을 어떻게 시뮬레이트 할 수 있습니까?CtrlC


이것이 내가 시도한 것입니다.

실행 shcat:

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

보내기 SIGINT까지 cat다른 단말로부터 :

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

cat 신호를 수신하고 (예상대로) 종료했습니다.

상위 프로세스로 신호를 보내는 것이 작동하지 않는 것 같습니다. 상위 프로세스로 전송 될 때 신호가 전파되지 않는 이유는 무엇 입니까?catsh

작동하지 않습니다.

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

1
쉘에는 키보드 나 터미널에서 보내지 않은 SIGINT 신호를 무시하는 방법이 있습니다.
konsolebox

답변:


86

어떻게 CTRL+ C작품

첫 번째는 CTRL+ C작동 방식을 이해하는 것입니다 .

CTRL+ 를 누르면 C터미널 에뮬레이터가 ETX 문자 (텍스트 끝 / 0x03)를 보냅니다.
TTY는이 문자를 수신 할 때 SIGINT를 터미널의 포 그라운드 프로세스 그룹으로 전송하도록 구성됩니다. 이 구성은를 수행 stty하여 볼 수 있습니다 intr = ^C;. POSIX 사양 INTR가 수신되면, 해당 단말의 전면 프로세스 그룹에 SIGINT를 보내는 것을 말한다.

포 그라운드 프로세스 그룹은 무엇입니까?

이제 문제는 포 그라운드 프로세스 그룹이 무엇인지 어떻게 결정합니까? 포 그라운드 프로세스 그룹은 단순히 키보드 (SIGTSTOP, SIGINT 등)에 의해 생성 된 모든 신호를 수신하는 프로세스 그룹입니다.

프로세스 그룹 ID를 판별하는 가장 간단한 방법은 다음을 사용하는 것입니다 ps.

ps ax -O tpgid

두 번째 열은 프로세스 그룹 ID입니다.

프로세스 그룹에 신호를 보내려면 어떻게합니까?

프로세스 그룹 ID가 무엇인지 알았으므로 전체 그룹에 신호를 보내는 POSIX 동작을 시뮬레이션해야합니다.

이 수행 할 수 있습니다 kill를 넣어 -그룹 ID의 앞에.
예를 들어 프로세스 그룹 ID가 1234 인 경우 다음을 사용합니다.

kill -INT -1234

 


터미널 번호를 사용하여 CTRL+ C를 시뮬레이션 하십시오.

위의 내용은 CTRL+ C를 수동 프로세스로 시뮬레이션하는 방법을 다룹니다 . 그러나 TTY 번호를 알고 있고 해당 터미널에 대해 CTRL+ 를 시뮬레이션하려면 C어떻게해야합니까?

이것은 매우 쉬워집니다.

$tty타겟팅하려는 터미널을 가정 합니다 (터미널 tty | sed 's#^/dev/##'에서 실행 하여 얻을 수 있음 ).

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

이것은 포 그라운드 프로세스 그룹이 무엇이든 SIGINT를 보냅니다 $tty.


6
터미널 바이 패스 권한 검사에서 직접 오는 신호는 터미널 속성에서 끄지 않으면 Ctrl + C는 항상 신호 전달에 성공하지만 kill명령은 실패 할 수 있습니다.
Brian Bi

4
+1sends a SIGINT to the foreground process group of the terminal.
andy

자식의 프로세스 그룹이 이후의 부모와 동일하다는 점을 언급 할 가치가 fork있습니다. 최소 실행 가능 C 예 : unix.stackexchange.com/a/465112/32558
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件

15

vinc17이 말했듯이, 이것이 일어날 이유가 없습니다. 신호 생성 키 시퀀스 (예 : Ctrl+ C)를 입력하면 신호가 터미널에 연결된 모든 프로세스로 전송됩니다. 에 의해 생성 된 신호에는 이러한 메커니즘이 없습니다 kill.

그러나 다음과 같은 명령

kill -SIGINT -12345

프로세스 그룹 12345의 모든 프로세스에 신호를 전송합니다 . kill (1)kill (2)를 참조하십시오 . 쉘의 자식은 일반적으로 쉘의 프로세스 그룹에 있습니다.


죄송합니다

vinc17이 지적했듯이 대화 형 쉘에서는 작동하지 않습니다. 여기에 대안의 작업은 :

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

ps -pPID_of_shell쉘에 대한 프로세스 정보를 가져옵니다.  헤더없이 터미널 프로세스 그룹 ID 만 출력하도록 o tpgid=지시 ps합니다. 이 값이 10000보다 작 으면 ps선행 공백과 함께 표시됩니다. 이것은 $(echo …)선행 및 후행 공백을 제거하는 빠른 방법입니다.

나는 이것을 데비안 머신에서 커서 테스트에서 작동하게했습니다.


1
프로세스가 대화식 셸 (OP에서 사용중인 것)에서 시작되면 작동하지 않습니다. 그러나이 동작에 대한 참조는 없습니다.
vinc17

12

질문에는 자체 답변이 포함되어 있습니다. SIGINTcat프로세스로 보내는 것은 을 kill누를 때 발생하는 상황을 완벽하게 시뮬레이션합니다 ^C.

보다 정확하게, 인터럽트 문자 ( ^C기본적으로)는 SIGINT터미널의 포 그라운드 프로세스 그룹의 모든 프로세스로 전송 합니다. 대신의 경우 cat여러 프로세스를 포함하는 더 복잡한 명령을 실행하고, 당신은 같은 효과를 달성하기 위해 프로세스 그룹을 죽여야 ^C.

&백그라운드 연산자 없이 외부 명령을 실행 하면 쉘은 명령에 대한 새 프로세스 그룹을 작성하고이 프로세스 그룹이 현재 포 그라운드에 있음을 터미널에 알립니다. 쉘은 여전히 ​​자체 프로세스 그룹에 있으며 더 이상 포 그라운드에 없습니다. 그런 다음 쉘은 명령이 종료 될 때까지 기다립니다.

여기에서 일반적인 오해로 인해 희생자가 된 것처럼 보입니다. 쉘이 자식 프로세스와 터미널 사이의 상호 작용을 촉진하기 위해 무언가를하고 있다는 생각입니다. 그건 사실이 아닙니다. 설정 작업 (프로세스 생성, 터미널 모드 설정, 파이프 생성 및 다른 파일 디스크립터 리디렉션 및 대상 프로그램 실행)을 마치면 셸 이 대기 합니다. 입력하는 cat것은 일반적인 입력이든 또는 신호 생성 특수 문자와 같은 쉘을 거치지 않습니다 ^C. cat프로세스는 자신의 파일 디스크립터를 통해 단말기에 직접 액세스 할 수 있으며, 단말기는 직접 신호를 보낼 수있는 기능이있다 cat가 전경 프로세스 그룹이기 때문에 과정을.

cat프로세스가 종료되면 프로세스의 상위이므로 쉘에 통지 cat됩니다. 그런 다음 쉘이 활성화되어 다시 포 그라운드에 놓입니다.

이해력을 높이기위한 연습이 있습니다.

새 터미널의 쉘 프롬프트에서 다음 명령을 실행하십시오.

exec cat

exec키워드는 실행할 수있는 쉘의 원인 cat자식 프로세스를 생성하지 않고 있습니다. 쉘이로 바뀝니다 cat. 이전에 쉘에 속한 PID는 이제 PID입니다 cat. ps다른 터미널에서 이것을 확인하십시오 . 임의의 줄을 입력 cat하고 다시 반복하여 부모 프로세스로 쉘 프로세스가 없어도 여전히 정상적으로 작동 함을 알 수 있습니다. 지금 누를 때 어떤 일이 발생 ^C합니까?

대답:

SIGINT는 cat 프로세스로 전달되어 죽습니다. 터미널에서 유일한 프로세스이기 때문에 마치 쉘 프롬프트에서 "종료"한 것처럼 세션이 종료됩니다. 사실상 고양이 잠시 동안 당신의 껍질 이었습니다 .


껍질이 나왔다 +1
Piotr Dobrogost

나는 exec cat압박 후 왜 고양이에 ^C착륙 하지 않는지 이해하지 못합니다 ^C. 왜 cat쉘을 대체 한 것이 종료 됩니까? 쉘이 교체되었으므로 쉘은 SIGINT를 수신하면 자식에게 보내는 논리를 구현하는 것입니다 ^C.
Steven Lu

요점은 쉘 자식에게 SIGINT를 보내지 않는다는 것 입니다. SIGINT는 터미널 드라이버에서 제공되며 모든 포 그라운드 프로세스로 전송됩니다.

3

SIGINT아이 에게 전파 할 이유가 없습니다 . 또한 system()POSIX 사양에 따르면 "system () 함수는 SIGINT 및 SIGQUIT 신호를 무시하고 명령이 종료 될 때까지 SIGCHLD 신호를 차단해야합니다."

쉘이 SIGINT예를 들어 실제 Ctrl-C를 따라 수신 된 것을 전파했다면 이는 자식 프로세스가 SIGINT신호를 두 번 수신한다는 것을 의미하며 , 이는 원하지 않는 동작을 가질 수 있습니다.


쉘은로 이것을 구현할 필요가 없다 system(). 그러나 신호가 잡히면 (아래와 같이) 아래쪽으로 전파 할 이유가 없습니다.
goldilocks

@goldilocks 나는 아마도 더 나은 이유를 제시하면서 대답을 마쳤습니다. 쉘은 자식이 이미 신호를 받았는지 알 수 없으므로 문제가 있습니다.
vinc17

1

setpgid POSIX C 프로세스 그룹 최소 예

기본 API의 실행 가능한 최소 예제로 이해하는 것이 더 쉬울 수 있습니다.

자식이로 프로세스 그룹을 변경하지 않은 경우 신호가 자식에게 전송되는 방식을 보여줍니다 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의 업스트림 .

다음과 같이 컴파일하십시오.

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

없이 실행 setpgid

CLI 인수 setpgid가 없으면 수행되지 않습니다.

./setpgid

가능한 결과 :

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

프로그램이 중단됩니다.

보시다시피, 두 프로세스의 pgid는에 걸쳐 상속됨에 따라 동일 fork합니다.

그런 다음 때릴 때마다 :

Ctrl + C

다시 출력합니다.

sigint parent
sigint child

이것은 방법을 보여줍니다 :

  • 신호를 전체 프로세스 그룹에 전송 kill(-pgid, SIGINT)
  • 터미널의 Ctrl + C는 기본적으로 전체 프로세스 그룹에 종료를 보냅니다.

SIGQUIT with를 사용하여 두 신호에 다른 신호를 보내 프로그램을 종료하십시오 Ctrl + \.

로 실행 setpgid

인수로 실행하면 다음과 같습니다.

./setpgid 1

그런 다음 자식은 pgid를 변경하고 이제는 부모로부터 매번 단일 sigint 만 인쇄됩니다.

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

그리고 지금, 당신이 칠 때마다 :

Ctrl + C

부모 만이 신호를 수신합니다.

sigint parent

여전히 SIGQUIT를 사용하여 이전과 같이 부모를 죽일 수 있습니다.

Ctrl + \

그러나 아이는 이제 다른 PGID를 가지고 있으며 그 신호를받지 않습니다! 이것은 다음에서 볼 수 있습니다.

ps aux | grep setpgid

다음을 사용하여 명시 적으로 종료해야합니다.

kill -9 16470

이를 통해 신호 그룹이 존재하는 이유를 알 수 있습니다. 그렇지 않으면 항상 수동으로 정리할 프로세스가 많이 남아 있습니다.

우분투 18.04에서 테스트되었습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.