신호는 내부적으로 어떻게 작동합니까?

신호는 내부적으로 어떻게 작동합니까?

일반적으로 프로세스를 종료하려면 다음과 같은 신호를 생성 SIGKILL합니다 SIGTSTP.

하지만 누가 특정 신호를 발행했는지, 누가 특정 프로세스에 신호를 보냈는지, 신호가 일반적으로 어떻게 작업을 수행하는지 어떻게 알 수 있습니까? 신호는 내부적으로 어떻게 작동합니까?

답변1

50,000피트에서의 시야는 다음과 같습니다.

  1. 신호는 커널(예: SIGSEGV유효하지 않은 주소에 액세스하거나 + SIGQUIT키를 누를 때 )이나 시스템 호출 (또는 여러 관련 호출)을 사용하는 프로그램에 의해 내부적으로 생성됩니다.Ctrl\kill

  2. 시스템 호출 중 하나에 의해 실행되면 커널은 호출 프로세스에 신호를 보낼 수 있는 충분한 권한이 있는지 확인합니다. 그렇지 않으면 오류가 반환됩니다(신호가 발생하지 않습니다).

  3. 두 개의 특수 신호 중 하나인 경우 커널은 대상 프로세스의 입력 없이 무조건 해당 신호에 대해 작동합니다. 이 두 가지 특수 신호는 SIGKILL 및 SIGSTOP입니다. 기본 작업, 신호 차단 등에 대한 아래의 모든 내용은 이들 중 하나와 관련이 없습니다.

  4. 다음으로, 커널은 신호로 무엇을 해야 할지 알아냅니다.

    1. 각 프로세스에 대해 각 신호에는 연관된 작업이 있습니다. 많은 기본값이 있으며 프로그램은 sigaction등을 사용하여 다른 기본값을 설정할 수 있습니다. signal여기에는 "완전히 무시", "프로세스 종료", "코어 덤프를 사용하여 프로세스 종료", "프로세스 중지" 등이 포함됩니다.

    2. 프로그램은 신호별로 신호 전달("차단")을 끌 수도 있습니다. 그런 다음 신호는 차단 해제될 때까지 보류 상태로 유지됩니다.

    3. 프로그램은 커널이 자체적으로 특정 작업을 수행하지 않고 대신 동기식( sigwait, wait 또는 사용 signalfd) 또는 비동기식(프로세스가 수행 중인 모든 작업을 중단하고 지정된 함수를 호출하여)으로 프로세스에 신호를 전달하도록 요청할 수 있습니다.

"실시간 신호"라고 하는 두 번째 신호 그룹이 있는데, 이는 특별한 의미가 없으며 여러 신호를 대기열에 넣을 수도 있습니다(일반 신호는 신호가 차단될 때 각 신호 중 하나만 대기열에 넣습니다). 스레드 간 통신을 위해 다중 스레드 프로그램에서 사용됩니다. 예를 들어, glibc에 대한 여러 POSIX 스레드 구현이 있습니다. 또한 서로 다른 프로세스 간에 통신하는 데에도 사용할 수 있습니다(예를 들어 여러 실시간 신호를 사용하여 fooctl 프로그램이 foo 데몬에 메시지를 보내도록 할 수 있습니다).

50,000피트 이외의 뷰에 대해서는 man 7 signal커널 내부 문서(또는 소스 코드)를 살펴보세요.

답변2

신호 구현은 매우 복잡하고 커널마다 다릅니다. 즉, 서로 다른 커널은 서로 다른 방식으로 신호를 구현합니다. 간단한 설명은 다음과 같습니다.

CPU는 특수 레지스터 값을 기반으로 실제로 벡터 테이블인 "인터럽트 설명자 테이블"을 찾을 것으로 예상되는 메모리 주소를 가지고 있습니다. 0으로 나누기 또는 INT 3(디버그)과 같은 트랩과 같은 가능한 각 예외에 대한 벡터가 있습니다. CPU에서 예외가 발생하면 플래그와 현재 명령어 포인터를 스택에 저장한 다음 관련 벡터에 지정된 주소로 점프합니다. Linux에서 이 벡터는 항상 예외 처리기가 있는 커널을 가리킵니다. 이제 CPU가 완료되었으므로 Linux 커널이 대신합니다.

소프트웨어에서 예외를 트리거할 수도 있습니다. 예를 들어, 사용자가 CTRL- 를 누르면 C이 호출은 자체 예외 처리기를 호출하는 커널로 이동합니다. 일반적으로 핸들러에 도달하는 방법은 다양하지만 기본적으로 동일한 일이 발생합니다. 즉, 컨텍스트가 스택에 저장되고 커널의 예외 핸들러로 점프됩니다.

그런 다음 예외 처리기는 어떤 스레드가 신호를 받아야 하는지 결정합니다. 0으로 나누기와 같은 일이 발생하면 쉽습니다. 예외를 발생시킨 스레드가 신호를 가져오지만 다른 유형의 신호의 경우 결정이 매우 복잡할 수 있으며 일부 특이한 경우에는 다소 임의의 스레드가 신호를 가져올 수 있습니다.

신호를 보내기 위해 커널은 먼저 SIGHUP신호 유형 또는 기타 값을 나타내는 값을 설정합니다. 이것은 단지 정수입니다. 각 프로세스에는 이 값이 저장되는 "보류 중인 신호" 저장 영역이 있습니다. 그런 다음 커널은 신호 정보를 포함하는 데이터 구조를 만듭니다. 이 구조에는 기본값, 무시 또는 처리될 수 있는 신호 "처리"가 포함됩니다. 그런 다음 커널은 자체 함수를 호출합니다 do_signal(). 다음 단계가 시작됩니다.

do_signal()여부를 먼저 결정하세요.그것신호가 처리됩니다. 예를 들어, 다음과 같은 경우죽이다그런 다음 do_signal()프로세스를 종료하면 스토리가 끝납니다. 그렇지 않으면 구성을 살펴봅니다. disposition이 default인 경우 do_signal()신호는 신호 종속 기본 정책에 따라 처리됩니다. 핸들이 핸들인 경우 해당 신호를 처리하도록 설계된 함수가 사용자 프로그램에 있다는 의미이며 해당 함수에 대한 포인터는 위의 데이터 구조에 위치하게 됩니다. 이 경우 do_signal()은 다른 커널 함수를 호출한 handle_signal()다음 사용자 모드로 다시 전환하고 해당 함수를 호출하는 과정을 거칩니다. 이 스위치의 세부 사항은 매우 복잡합니다. 프로그램의 이 코드 조각은 일반적으로 .NET에서 함수를 사용할 때 프로그램에 자동으로 연결됩니다 signal.h.

보류 중인 신호 값을 적절하게 확인함으로써 커널은 프로세스가 모든 신호를 처리하고 있는지 확인할 수 있으며, 그렇지 않은 경우 신호에 따라 프로세스를 절전 모드로 전환하거나 프로세스를 종료하거나 다른 작업을 수행할 수 있는 적절한 조치를 취할 수 있습니다.

답변3

이 질문에 대한 답변은 이미 나와 있지만 Linux 커널의 자세한 이벤트 흐름을 게시해 보겠습니다.
이것은 정확히 복사되었습니다Linux 포스트: Linux 신호 - 내부 구조 sklinuxblog.blogspot.com의 "Linux 게시물" 블로그. 나도 이런 블로그를 쓴다.

시그널 사용자 공간 C 프로그램

간단한 신호 사용자 공간 C 프로그램을 작성하는 것부터 시작해 보겠습니다.

#include<signal.h>
#include<stdio.h>

/* Handler function */
void handler(int sig) {
    printf("Receive signal: %u\n", sig);
};

int main(void) {
    struct sigaction sig_a;

    /* Initialize the signal handler structure */
    sig_a.sa_handler = handler;
    sigemptyset(&sig_a.sa_mask);
    sig_a.sa_flags = 0;

    /* Assign a new handler function to the SIGINT signal */
    sigaction(SIGINT, &sig_a, NULL);

    /* Block and wait until a signal arrives */
    while (1) {
            sigsuspend(&sig_a.sa_mask);
            printf("loop\n");
    }
    return 0;
};

이 코드는 SIGINT 신호에 새 핸들러를 할당합니다. Ctrl+ C키 조합을 사용하여 실행 중인 프로세스에 SIGINT를 보낼 수 있습니다 . Ctrl+를 누르면 C비동기 신호 SIGINT가 작업으로 전송됩니다. 이는 kill -INT <pid>다른 터미널에서 명령을 보내는 것과 동일합니다 .

kill -la ("list"를 의미하는 소문자 ) 를 수행하면 L실행 중인 프로세스에 보낼 수 있는 다양한 신호에 대해 배우게 됩니다.

[root@linux ~]# 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
16) SIGSTKFLT    17) SIGCHLD      18) SIGCONT      19) SIGSTOP      20) SIGTSTP
21) SIGTTIN      22) SIGTTOU      23) SIGURG       24) SIGXCPU      25) SIGXFSZ
26) SIGVTALRM    27) SIGPROF      28) SIGWINCH     29) SIGIO        30) SIGPWR
31) SIGSYS       34) SIGRTMIN     35) SIGRTMIN+1   36) SIGRTMIN+2   37) SIGRTMIN+3
38) SIGRTMIN+4   39) SIGRTMIN+5   40) SIGRTMIN+6   41) SIGRTMIN+7   42) SIGRTMIN+8
43) SIGRTMIN+9   44) SIGRTMIN+10  45) SIGRTMIN+11  46) SIGRTMIN+12  47) SIGRTMIN+13
48) SIGRTMIN+14  49) SIGRTMIN+15  50) SIGRTMAX-14  51) SIGRTMAX-13  52) SIGRTMAX-12
53) SIGRTMAX-11  54) SIGRTMAX-10  55) SIGRTMAX-9   56) SIGRTMAX-8   57) SIGRTMAX-7
58) SIGRTMAX-6   59) SIGRTMAX-5   60) SIGRTMAX-4   61) SIGRTMAX-3   62) SIGRTMAX-2
63) SIGRTMAX-1   64) SIGRTMAX

다음 키 조합을 사용하여 특정 신호를 보낼 수도 있습니다.

  • Ctrl+ C– SIGINT를 보내면 기본 동작은 애플리케이션을 종료하는 것입니다.
  • Ctrl+</kbd>  – sends SIGQUIT which default action is to terminate the application dumping core.
  • Ctrl+ Z– 프로그램을 일시 중지하려면 SIGSTOP을 보냅니다.

위의 C 프로그램을 컴파일하고 실행하면 다음과 같은 결과가 나타납니다.

[root@linux signal]# ./a.out
Receive signal: 2
loop
Receive signal: 2
loop
^CReceive signal: 2
loop

Ctrl+C를 사용해도 kill -2 <pid>프로세스가 종료되지 않습니다. 대신 신호 처리기를 실행하고 반환합니다.

신호가 프로세스로 전송되는 방법

프로세스로 전송되는 신호의 내부 구조를 살펴보고 __send_signal함수에 dump_stack과 함께 Jprobe를 배치하면 다음과 같은 호출 추적을 볼 수 있습니다.

May  5 16:18:37 linux kernel: dump_stack+0x19/0x1b
May  5 16:18:37 linux kernel: my_handler+0x29/0x30 (probe)
May  5 16:18:37 linux kernel: complete_signal+0x205/0x250
May  5 16:18:37 linux kernel: __send_signal+0x194/0x4b0
May  5 16:18:37 linux kernel: send_signal+0x3e/0x80
May  5 16:18:37 linux kernel: do_send_sig_info+0x52/0xa0
May  5 16:18:37 linux kernel: group_send_sig_info+0x46/0x50
May  5 16:18:37 linux kernel: __kill_pgrp_info+0x4d/0x80
May  5 16:18:37 linux kernel: kill_pgrp+0x35/0x50
May  5 16:18:37 linux kernel: n_tty_receive_char+0x42b/0xe30
May  5 16:18:37 linux kernel:  ? ftrace_ops_list_func+0x106/0x120
May  5 16:18:37 linux kernel: n_tty_receive_buf+0x1ac/0x470
May  5 16:18:37 linux kernel: flush_to_ldisc+0x109/0x160
May  5 16:18:37 linux kernel: process_one_work+0x17b/0x460
May  5 16:18:37 linux kernel: worker_thread+0x11b/0x400
May  5 16:18:37 linux kernel: rescuer_thread+0x400/0x400
May  5 16:18:37 linux kernel:  kthread+0xcf/0xe0
May  5 16:18:37 linux kernel:  kthread_create_on_node+0x140/0x140
May  5 16:18:37 linux kernel:  ret_from_fork+0x7c/0xb0
May  5 16:18:37 linux kernel: ? kthread_create_on_node+0x140/0x140

따라서 신호를 보내기 위한 주요 함수 호출은 다음과 같습니다.

First shell send the Ctrl+C signal using n_tty_receive_char
n_tty_receive_char()
isig()
kill_pgrp()
__kill_pgrp_info()
group_send_sig_info() -- for each PID in group call this function
do_send_sig_info()
send_signal()
__send_signal() -- allocates a signal structure and add to task pending signals
complete_signal()
signal_wake_up()
signal_wake_up_state()  -- sets TIF_SIGPENDING in the task_struct flags. Then it wake up the thread to which signal was delivered.

이제 모든 것이 설정되었으며 task_struct프로세스에 필요한 변경이 이루어졌습니다.

신호 처리

이 신호는 프로세스가 시스템 호출에서 반환되거나 인터럽트에서 반환을 완료할 때 프로세스에 의해 확인/처리됩니다. 시스템 호출의 반환 결과는 file 에 있습니다 entry_64.S.

entry_64.S함수가 호출 되는 int_signal 함수가 호출됩니다 do_notify_resume().

이 기능을 확인해 보겠습니다 do_notify_resume(). 이 함수는 다음 TIF_SIGPENDING위치에 플래그를 설정했는지 확인합니다 task_struct.

 /* deal with pending signal delivery */
 if (thread_info_flags & _TIF_SIGPENDING)
  do_signal(regs);
do_signal calls handle_signal to call the signal specific handler
Signals are actually run in user mode in function:
__setup_rt_frame -- this sets up the instruction pointer to handler: regs->ip = (unsigned long) ksig->ka.sa.sa_handler;

시스템 호출 및 신호

읽기 / 쓰기 차단과 같은 "느린" 시스템 호출은 프로세스를 대기 상태로 전환합니다 TASK_INTERRUPTIBLE.TASK_UNINTERRUPTIBLE

상태의 작업은 TASK_INTERRUPTIBLE신호를 통해 상태로 변경됩니다. 프로세스를 예약할 수 있음을 의미합니다.TASK_RUNNINGTASK_RUNNING

실행되면 해당 신호 처리기는 "느린" 시스템 호출이 완료되기 전에 실행됩니다. 기본적으로 수행되지 않습니다 syscall.

SA_RESTART플래그가 설정된 경우 syscall신호 처리기가 완료된 후 다시 시작하세요.

인용하다

관련 정보