QUESTION
하드웨어 interrupt가 곧바로 userspace signal이나 epoll event가 되는가?
interrupt는 CPU가 현재 실행 흐름을 커널의 interrupt handler로 넘기는 실행 문맥의 변화다. event는 상태가 바뀌었다는 사실을 가리키는 넓은 표현이고, Linux에 event라는 단일 공통 객체가 있는 것은 아니다. wait queue wakeup, poll readiness, eventfd counter, completion, input event처럼 서브시스템마다 저장 형식과 소비 규칙이 다르다.
signal은 task 또는 thread group을 대상으로 하는 비동기 통지 규약이다. signal은 IRQ 없이 kill syscall, page fault, timer 만료, terminal 제어 문자, 자식 종료에서도 생성된다. 반대로 device IRQ가 발생해도 driver 내부 처리로 끝날 수 있다. IRQ가 데이터를 ring buffer에 넣고 wait queue를 깨운 뒤 userspace가 read나 epoll로 관찰하는 경로가 흔하며, 이때 signal은 전혀 필요하지 않다.
STRUCTURE
구조 그림
do_send_sig_info()
force_sig_fault()
posix_timer_event()
n_tty_receive_signal_char()
do_notify_parent()
kill_fasync()
signal은 하드웨어 IRQ의 별명이 아니다. syscall, 동기 CPU exception, timer, terminal line discipline, process lifecycle, fasync처럼 서로 다른 문맥에서 생성된 뒤 공통 enqueue와 delivery 규칙을 따른다.
CALL PATH
호출 흐름
interrupt는 코드를 실행시키는 커널 진입 원인, event는 관찰할 상태 변화, signal은 task에 pending 상태를 남기는 전달 규약이다. 셋은 연결될 수 있지만 자동으로 일대일 변환되지 않는다.
함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.
SOURCE COORDINATES
Linux 6.18.37 LTS 소스 위치
glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.
| 파일 | 함수·구조체 | 여기서 볼 것 |
|---|---|---|
| kernel/irq/handle.c | handle_irq_event(), handle_irq_event_percpu() | irqaction handler를 hardirq 문맥에서 호출하고 IRQ_WAKE_THREAD 결과를 threaded handler로 넘기는 공통 경로 |
| kernel/softirq.c | irq_exit_rcu(), invoke_softirq(), __do_softirq() | hardirq 종료 뒤 보류된 softirq를 실행하거나 ksoftirqd로 넘기는 문맥 경계 |
| kernel/sched/wait.c | __wake_up_common(), __wake_up_common_lock() | producer가 wait queue의 task를 runnable 상태로 바꾸는 공통 wakeup 경로 |
| fs/eventpoll.c | ep_poll_callback(), ep_send_events() | target file의 wait queue wakeup을 epoll ready list와 userspace events 배열로 연결하는 경로 |
| kernel/signal.c | kill_something_info(), do_send_sig_info(), group_send_sig_info() | kill 계열 syscall이 PID 종류와 permission을 검사한 뒤 process-directed signal을 만드는 시작점 |
| kernel/signal.c | __send_signal_locked(), complete_signal(), get_signal() | 여러 producer가 공통 pending bitmap/queue에 합류하고 실제 전달 가능한 signal을 고르는 핵심 경로 |
| kernel/signal.c | force_sig_fault(), force_sig_info_to_task() | page fault나 illegal instruction 같은 동기 예외를 si_addr와 si_code가 있는 signal로 변환하는 경로 |
| kernel/time/posix-timers.c | posix_timer_fn(), posix_timer_event() | POSIX timer 만료가 SIGEV_SIGNAL 설정에 따라 signal queue로 들어가는 시작점 |
| drivers/tty/n_tty.c | n_tty_receive_signal_char(), __isig() | VINTR·VQUIT·VSUSP 문자를 foreground process group의 SIGINT·SIGQUIT·SIGTSTP로 바꾸는 line discipline 경로 |
| kernel/signal.c | do_notify_parent() | child exit/stop/continue 상태를 parent의 SIGCHLD와 wait 상태로 알리는 process lifecycle 경로 |
| fs/fcntl.c | kill_fasync(), kill_fasync_rcu() | O_ASYNC와 fasync 등록을 사용하는 driver/file이 SIGIO 또는 지정 signal을 보내는 경로 |
| fs/signalfd.c | signalfd_read(), signalfd_poll() | 이미 생성되어 pending인 signal을 handler 대신 fd record와 readiness로 소비하는 경로 |
| arch/x86/kernel/signal.c | arch_do_signal_or_restart(), setup_rt_frame() | userspace 복귀 직전에 register context와 signal mask를 rt_sigframe에 저장하고 handler 진입점을 만드는 경로 |
COMPLETE PROGRAM
실행 예제 원본
아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.
cc -std=c17 -Wall -Wextra -O2 -pthread event_sources.c -o event_sources01#define _GNU_SOURCE
02#include <errno.h>
03#include <pthread.h>
04#include <signal.h>
05#include <stdint.h>
06#include <stdio.h>
07#include <sys/epoll.h>
08#include <sys/eventfd.h>
09#include <sys/signalfd.h>
10#include <sys/timerfd.h>
11#include <sys/wait.h>
12#include <time.h>
13#include <unistd.h>
14
15enum source_tag {
16 SOURCE_SIGNAL = 1,
17 SOURCE_TIMER,
18 SOURCE_EVENTFD
19};
20
21enum seen_bit {
22 SEEN_USR1 = 1U << 0,
23 SEEN_CHLD = 1U << 1,
24 SEEN_TIMER = 1U << 2,
25 SEEN_EVENT = 1U << 3
26};
27
28static int add_source(int epfd, int fd, uint32_t tag)
29{
30 struct epoll_event event = {
31 .events = EPOLLIN,
32 .data.u32 = tag
33 };
34 return epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
35}
36
37static void *worker(void *argument)
38{
39 int event_fd = *(int *)argument;
40 struct timespec delay = { .tv_sec = 0, .tv_nsec = 150000000 };
41 uint64_t credit = 1;
42
43 nanosleep(&delay, NULL);
44 if (write(event_fd, &credit, sizeof(credit)) != sizeof(credit))
45 return (void *)1;
46 return NULL;
47}
48
49int main(void)
50{
51 sigset_t mask;
52 sigemptyset(&mask);
53 sigaddset(&mask, SIGUSR1);
54 sigaddset(&mask, SIGCHLD);
55 if (pthread_sigmask(SIG_BLOCK, &mask, NULL) != 0)
56 return 1;
57
58 int signal_fd = signalfd(-1, &mask, SFD_CLOEXEC | SFD_NONBLOCK);
59 int timer_fd = timerfd_create(CLOCK_MONOTONIC,
60 TFD_CLOEXEC | TFD_NONBLOCK);
61 int event_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
62 int epfd = epoll_create1(EPOLL_CLOEXEC);
63 if (signal_fd < 0 || timer_fd < 0 || event_fd < 0 || epfd < 0)
64 return 1;
65
66 struct itimerspec timer = {
67 .it_value = { .tv_sec = 0, .tv_nsec = 250000000 }
68 };
69 if (timerfd_settime(timer_fd, 0, &timer, NULL) < 0 ||
70 add_source(epfd, signal_fd, SOURCE_SIGNAL) < 0 ||
71 add_source(epfd, timer_fd, SOURCE_TIMER) < 0 ||
72 add_source(epfd, event_fd, SOURCE_EVENTFD) < 0)
73 return 1;
74
75 pid_t child = fork();
76 if (child < 0)
77 return 1;
78 if (child == 0)
79 _exit(42);
80
81 pthread_t thread;
82 if (pthread_create(&thread, NULL, worker, &event_fd) != 0)
83 return 1;
84 if (kill(getpid(), SIGUSR1) < 0)
85 return 1;
86
87 unsigned int seen = 0;
88 while (seen != (SEEN_USR1 | SEEN_CHLD | SEEN_TIMER | SEEN_EVENT)) {
89 struct epoll_event events[4];
90 int count = epoll_wait(epfd, events, 4, -1);
91 if (count < 0 && errno == EINTR)
92 continue;
93 if (count < 0)
94 return 1;
95
96 for (int i = 0; i < count; ++i) {
97 if (events[i].data.u32 == SOURCE_SIGNAL) {
98 for (;;) {
99 struct signalfd_siginfo info;
100 ssize_t n = read(signal_fd, &info, sizeof(info));
101 if (n == (ssize_t)sizeof(info)) {
102 printf("signal: signo=%u code=%d sender=%u\n",
103 info.ssi_signo, info.ssi_code, info.ssi_pid);
104 if (info.ssi_signo == SIGUSR1)
105 seen |= SEEN_USR1;
106 if (info.ssi_signo == SIGCHLD) {
107 int status;
108 if (waitpid(child, &status, 0) == child)
109 printf("child: exit=%d\n", WEXITSTATUS(status));
110 seen |= SEEN_CHLD;
111 }
112 continue;
113 }
114 if (n < 0 && errno == EAGAIN)
115 break;
116 return 1;
117 }
118 } else if (events[i].data.u32 == SOURCE_TIMER) {
119 uint64_t expirations;
120 if (read(timer_fd, &expirations, sizeof(expirations)) !=
121 sizeof(expirations))
122 return 1;
123 printf("timerfd: expirations=%llu\n",
124 (unsigned long long)expirations);
125 seen |= SEEN_TIMER;
126 } else if (events[i].data.u32 == SOURCE_EVENTFD) {
127 uint64_t credits;
128 if (read(event_fd, &credits, sizeof(credits)) != sizeof(credits))
129 return 1;
130 printf("eventfd: credits=%llu\n",
131 (unsigned long long)credits);
132 seen |= SEEN_EVENT;
133 }
134 }
135 }
136
137 void *worker_result;
138 pthread_join(thread, &worker_result);
139 close(signal_fd);
140 close(timer_fd);
141 close(event_fd);
142 close(epfd);
143 return worker_result != NULL;
144}
CODE NOTES
코드 조각별 설명
pthread_sigmask(SIG_BLOCKSIGUSR1과 SIGCHLD를 fd 생성보다 먼저 block한다. 새 thread는 mask를 상속하므로 비동기 handler가 어느 thread에서 갑자기 실행되는 경로를 닫고 signalfd가 pending signal을 소비하게 한다.
signalfd(-1, &masksignalfd는 signal을 새로 생성하지 않는다. mask에 들어 있는 signal이 task/thread-group pending 상태가 되었을 때 읽을 수 있는 struct signalfd_siginfo record로 노출한다.
timerfd_create(CLOCK_MONOTONIC시간 만료를 signal 대신 8-byte expiration counter가 있는 file로 표현한다. wall clock 보정의 영향을 피하려고 경과 시간 측정에는 CLOCK_MONOTONIC을 사용한다.
eventfd(0, EFD_CLOEXECworker가 main loop를 깨우는 credit counter를 만든다. write 한 번과 epoll event 한 번을 동일시하지 않고 read가 돌려준 uint64_t 누적값을 확인해야 한다.
epoll_ctl(epfd, EPOLL_CTL_ADD세 fd를 같은 interest list에 등록한다. epoll은 signal·timer·thread event의 원인을 통일하지 않고 각 file의 poll readiness를 한 배열에 모을 뿐이다.
write(event_fd, &creditworker는 userspace process context에서 eventfd counter를 증가시킨다. 하드웨어 interrupt가 없어도 완전한 fd event와 wakeup이 만들어진다는 사례다.
pid_t child = fork()child는 _exit에서 종료 상태를 남긴다. 커널의 parent 통지 경로가 SIGCHLD를 생성하고, parent가 waitpid로 상태를 회수할 때 zombie가 해제된다.
kill(getpid(), SIGUSR1)kill syscall에서 시작하는 순수 software signal이다. device IRQ나 timer가 없어도 permission 검사 뒤 process-directed pending signal이 만들어진다.
epoll_wait(epfd, eventsmain thread가 runnable하지 않을 때 epoll wait queue에서 잔다. producer의 wakeup은 즉시 userspace callback을 실행하지 않고 task를 runnable하게 만들며 scheduler가 실제 실행 시점을 정한다.
read(signal_fd, &infosignal fd는 고정 크기 record 단위다. nonblocking fd이므로 한 번의 EPOLLIN 뒤 EAGAIN까지 읽어 pending record를 비운다.
info.ssi_signo == SIGCHLDsignalfd record는 통지이고 자식 자원 회수 자체는 아니다. SIGCHLD를 읽은 뒤에도 waitpid 또는 waitid로 child exit status를 회수해야 한다.
read(timer_fd, &expirationstimerfd read는 마지막 read 이후의 누적 만료 횟수를 반환한다. event loop가 늦었다면 값이 1보다 클 수 있다.
read(event_fd, &credits기본 eventfd read는 현재 counter 전체를 반환하고 0으로 만든다. EFD_SEMAPHORE를 사용하면 read마다 1을 소비하는 다른 규칙이 적용된다.
pthread_join(threadevent를 관찰했다는 사실과 producer thread의 수명 종료는 별개다. join으로 thread 자원을 회수한 다음 공유 fd를 닫는다.
DETAILS
세부 동작
interrupt, exception, event, signal을 먼저 분리한다
hardware interrupt는 장치가 CPU의 현재 instruction 흐름과 독립적으로 알리는 비동기 요청이다. CPU exception은 현재 instruction 실행 결과와 연결되는 동기 사건으로 page fault, divide error, invalid opcode가 대표적이다. Linux 문서와 코드에서 둘을 넓게 interrupt/exception entry로 묶어 부르기도 하지만 원인과 재시작 규칙은 다르다.
event는 ABI 이름이 아니라 상태 변화에 붙이는 일반명이다. epoll event는 readiness snapshot, inotify event는 가변 길이 queue record, eventfd는 uint64 counter, completion은 kernel synchronization object다. 이름이 같다고 손실·순서·누적 규칙이 같지 않다.
signal은 번호, disposition, mask, pending state, siginfo, default action을 가진 task 통지 규약이다. interrupt context의 이름도 signal handler라고 부르지 않고, userspace signal handler는 hardirq 문맥에서 실행되지 않는다.
device IRQ에서 userspace read까지는 여러 경계를 지난다
장치가 DMA descriptor를 완료하고 interrupt line 또는 MSI를 올리면 CPU는 architecture entry code를 거쳐 irq_desc와 irqaction을 찾는다. primary handler는 장치 status를 읽고 interrupt 원인을 ACK/mask하며 다음 처리에 필요한 최소 상태를 보존한다.
packet parsing, block completion 후속 처리처럼 오래 걸릴 수 있는 작업은 softirq, NAPI, threaded IRQ, tasklet, workqueue 등으로 미룬다. 어떤 방식이 선택되는지는 subsystem과 driver의 locking, sleep 가능 여부, latency 목표에 따라 다르다.
마지막에 driver가 ring index, counter, file 상태를 publish하고 wait queue를 깨우면 blocked task가 runnable해진다. scheduler가 그 task를 선택하고 epoll_wait가 반환한 뒤 userspace가 read를 호출해야 데이터 ownership이 실제로 이동한다. wake_up 자체가 userspace 함수를 호출하는 것은 아니다.
IRQ와 event는 일대일 대응하지 않는다
한 번의 interrupt가 여러 descriptor completion을 묶어 처리할 수 있고 interrupt coalescing을 쓰면 여러 packet이 IRQ 하나로 합쳐진다. 반대로 level-triggered interrupt는 원인이 제거될 때까지 다시 관찰될 수 있다. IRQ 횟수를 I/O operation 횟수로 해석하면 안 된다.
poll/epoll readiness도 사건 횟수가 아니다. readable은 지금 read가 진행되거나 EOF/error를 확인할 수 있다는 상태다. producer가 여러 번 buffer를 채워도 fd는 ready 상태 하나로 유지될 수 있으며, edge-triggered loop는 EAGAIN까지 drain해야 다음 edge를 받을 수 있다.
eventfd와 timerfd처럼 명시적 counter를 가진 API는 누적값을 보존하지만 counter 폭, overflow, read mode 규칙이 있다. 어떤 이벤트를 잃어도 되는지, 횟수가 필요한지, 최신 상태만 필요할지를 먼저 정해야 한다.
signal은 생성, pending, 선택, 전달 네 단계로 읽는다
producer가 signal number와 siginfo를 정하고 target을 찾는 시점이 생성이다. 그 다음 task 또는 shared signal pending bitmap/queue에 기록된다. standard signal은 같은 번호가 이미 pending이면 새 발생이 합쳐질 수 있고 realtime signal은 siginfo record를 순서대로 queue할 수 있다.
complete_signal은 signal을 받을 후보 thread를 찾고 TIF_SIGPENDING 등을 통해 signal work가 있음을 표시한다. get_signal은 대상 thread가 userspace로 돌아가려는 지점에서 mask와 disposition을 보고 실제로 처리할 signal을 고른다.
handler delivery에서는 architecture code가 user stack 또는 altstack에 rt_sigframe을 만들고 saved registers, old mask, siginfo/ucontext를 기록한 뒤 instruction pointer를 handler로 바꾼다. handler 종료 후 rt_sigreturn이 frame을 검증하고 문맥을 복원한다. 생성 시각과 handler instruction 실행 시각이 다른 이유다.
kill, tgkill, pidfd_send_signal은 target 선택에서 시작한다
kill(pid, sig)는 pid 값의 부호에 따라 한 process, caller의 process group, 특정 process group, 권한이 허용된 여러 process를 대상으로 할 수 있다. kernel은 namespace에서 PID를 해석하고 credential과 signal 종류에 따른 permission을 검사한 뒤 group_send_sig_info 계열로 들어간다.
tgkill(tgid, tid, sig)는 thread group과 thread ID를 함께 확인해 특정 thread-directed pending 상태를 만든다. pthread_kill은 이 syscall 계열을 사용하는 libc interface다. multithread 프로그램에서 process-directed signal은 block하지 않은 thread 중 하나가 받을 수 있으므로 target 의미를 구분해야 한다.
pidfd_send_signal은 숫자 PID 재사용 문제를 줄이기 위해 pidfd가 참조하는 process instance를 target으로 삼는다. 어떤 API를 사용해도 공통 signal enqueue 경로 이후 mask와 disposition 규칙은 동일하게 적용된다.
page fault와 illegal instruction은 동기 signal을 만든다
userspace instruction이 유효하지 않은 주소를 접근하면 CPU page-fault exception으로 kernel에 들어간다. kernel이 demand paging, COW, stack growth 같은 정상 fault로 해결하면 같은 instruction으로 돌아가며 signal은 생기지 않는다.
VMA가 없거나 permission이 맞지 않아 해결할 수 없으면 architecture fault code가 force_sig_fault 계열을 호출해 SIGSEGV와 SEGV_MAPERR 또는 SEGV_ACCERR, fault address를 기록한다. invalid opcode는 SIGILL, divide error는 SIGFPE처럼 exception 종류에 맞는 signal이 생성된다.
이 signal은 현재 faulting thread를 대상으로 하므로 일반 kill과 원인이 다르다. handler가 원인을 고치지 않은 채 return하면 같은 instruction이 다시 fault할 수 있고, default action이면 core dump와 process 종료 경로로 이어질 수 있다.
timer는 설정한 notification 방식에 따라 signal일 수도 fd event일 수도 있다
POSIX timer를 SIGEV_SIGNAL로 만들면 hrtimer callback이 만료 상태와 overrun 정보를 갱신하고 signal queue로 통지를 보낸다. signal이 pending인 동안 여러 만료가 생기면 timer overrun 값을 함께 해석해야 한다.
setitimer와 alarm도 만료 시 SIGALRM 계열을 생성하지만 standard signal coalescing 규칙 때문에 만료 횟수 counter로 쓰기 어렵다. timerfd는 signal pending 대신 file counter를 증가시키고 poll wait queue를 깨운다.
같은 clock source와 hrtimer 기반이라도 userspace ABI가 signal인지 fd인지에 따라 mask, queue, event loop 통합, 소비 방법이 달라진다. timer interrupt가 곧 SIGALRM이라는 식으로 연결하면 중간의 timer subsystem과 notification 선택을 놓친다.
Ctrl-C는 keyboard IRQ에서 곧바로 SIGINT가 되는 것이 아니다
키보드 또는 terminal emulator에서 입력 byte가 tty driver로 들어오고 line discipline이 termios 설정을 적용한다. canonical n_tty에서 ISIG가 켜져 있고 byte가 VINTR과 일치할 때 __isig가 foreground process group에 SIGINT를 보낸다.
따라서 물리 keyboard IRQ가 없는 pseudo terminal에서도 master 쪽에 control character를 쓰면 같은 tty 의미가 만들어질 수 있다. 반대로 raw mode 또는 ISIG 비활성화에서는 0x03이 일반 입력 byte로 read될 수 있다.
SIGINT target은 terminal foreground process group이지 화면에 보이는 process 하나라고 고정되지 않는다. shell이 pipeline을 하나의 process group으로 만들고 tcsetpgrp로 foreground ownership을 넘기는 이유가 여기에 있다.
SIGCHLD는 process lifecycle에서 생성되고 wait와 역할이 다르다
child가 exit, stop, continue 상태로 바뀌면 kernel은 parent가 정한 SIGCHLD disposition과 SA_NOCLDSTOP/SA_NOCLDWAIT 조건을 확인해 통지한다. siginfo에는 child PID, UID, CLD_EXITED 같은 code와 status가 들어갈 수 있다.
SIGCHLD는 상태 변화가 있다는 통지이고, exit status와 resource usage를 회수하는 동작은 waitpid/waitid가 담당한다. handler나 signalfd에서 SIGCHLD를 읽기만 하고 wait하지 않으면 zombie가 남을 수 있다.
여러 child가 비슷한 시각에 끝나면 standard SIGCHLD가 합쳐질 수 있으므로 signal 한 개당 wait 한 번으로 끝내지 않는다. WNOHANG loop로 더 이상 회수할 child가 없을 때까지 반복하는 패턴이 필요하다.
SIGIO와 fasync는 file event를 signal로 전달하는 선택적 경로다
application이 F_SETOWN과 O_ASYNC/FASYNC를 설정하면 file 또는 driver는 fasync_struct 목록에 subscriber를 등록할 수 있다. 상태 변화 시 kill_fasync가 owner에게 SIGIO 또는 F_SETSIG로 지정한 signal을 보낸다.
모든 fd가 fasync를 지원하는 것은 아니고 readiness와 signal queue 사이에는 race와 coalescing이 있다. 고속 I/O에서 매 사건을 standard SIGIO로 세려는 설계는 손실과 handler 비용 때문에 적합하지 않을 수 있다.
epoll, io_uring, blocking read, SIGIO 가운데 무엇을 쓸지는 장치 API와 workload에 따라 결정한다. 하드웨어 IRQ가 signal을 직접 쏜다기보다 file implementation이 fasync policy를 선택했을 때 signal producer가 되는 구조다.
signalfd는 signal의 발생 원인을 바꾸지 않는다
signalfd를 만들기 전에 대상 signal을 block해야 한다. 그렇지 않으면 일반 handler/default action과 fd 소비가 경쟁할 수 있다. process의 모든 thread에서 mask 정책을 일관되게 적용하는 것이 중요하다.
pending signal이 생기면 signalfd poll은 readable을 보고하고 read는 signalfd_siginfo로 하나 이상의 record를 가져온다. kill로 생긴 SIGUSR1, lifecycle에서 생긴 SIGCHLD가 같은 fd에서 보이지만 ssi_code, sender, status를 보고 원인을 구분할 수 있다.
SIGKILL과 SIGSTOP은 block하거나 signalfd로 소비할 수 없다. synchronous fault signal을 무조건 signalfd 전용으로 돌려 오류 instruction을 계속 재실행하게 만드는 설계도 피해야 한다.
문맥마다 허용되는 작업과 지연 원인이 다르다
hardirq context에서는 sleep할 수 없고 handler 시간을 짧게 유지해야 한다. softirq도 일반 process처럼 blocking mutex나 userspace 접근을 사용할 수 없으며 긴 부하는 ksoftirqd scheduling과 다른 IRQ latency에 영향을 준다. threaded IRQ와 workqueue는 sleep 가능한 process context를 제공하지만 scheduling delay가 추가된다.
wake_up에서 epoll_wait 반환까지의 시간에는 runnable queue, CPU affinity, priority, preemption, interrupt masking, softirq backlog가 들어간다. signal pending에서 handler 진입까지도 target thread가 kernel에서 돌아오지 않거나 signal을 block하면 늘어난다.
latency를 측정할 때 IRQ timestamp, state publish, wakeup, scheduler switch, syscall return, userspace consumption 시점을 따로 기록해야 병목 문맥을 찾을 수 있다. 평균 하나로 합치면 upper-tail 원인을 숨기기 쉽다.
OBJECTS
객체와 수명
| 대상 | 언제 생기고 없어지는가 | 확인할 값 |
|---|---|---|
irq_desc / irqaction | IRQ 등록과 해제 사이에 handler와 chip/domain 상태를 보관한다 | trigger type, disable depth, action flags, threaded handler |
wait_queue_head | subsystem object 또는 file 수명에 붙어 sleeper/callback 등록을 관리한다 | wait entry, wake function, lock, exclusive flag |
eventpoll / epitem | epoll_create와 EPOLL_CTL_ADD에서 생기고 DEL 또는 fd close에서 연결이 풀린다 | interest mask, ready list, user data |
signal_struct / sighand_struct | thread group이 shared pending state와 disposition을 공유하며 fork/exec/exit 규칙에 따라 복제·reset·해제된다 | shared_pending, action, flags |
task_struct.pending | thread-directed signal 생성부터 get_signal 소비까지 해당 task에 남는다 | signal bitmap, sigqueue list, TIF_SIGPENDING |
sigqueue / kernel_siginfo | queued signal에 record가 필요할 때 할당되고 dequeue 또는 target 종료에서 해제된다 | si_code, sender, si_addr, payload, overrun |
rt_sigframe | handler delivery 때 user stack에 생기고 rt_sigreturn에서 소비된다 | saved registers, old mask, siginfo, ucontext |
signalfd_ctx | signalfd 생성부터 file close까지 관찰할 mask를 보관한다 | mask, pending signal poll/read 결과 |
FAILURE PATH
실패 조건과 오해하기 쉬운 부분
| 겉으로 보이는 현상 | 실제 원인 후보 | 확인 방법 |
|---|---|---|
| IRQ 횟수와 packet 수가 다름 | interrupt coalescing, NAPI budget, 한 IRQ의 여러 completion | driver statistics, trace_irq_handler, ring producer/consumer index 비교 |
| epoll event 뒤 read가 EAGAIN | readiness와 소비 사이의 경쟁 또는 다른 thread가 먼저 drain | nonblocking 반환값과 fd owner 정책 확인 |
| signal을 여러 번 보냈는데 한 번만 수신 | standard signal pending bit coalescing | realtime signal, eventfd counter, application queue 검토 |
| signal handler가 늦게 실행 | target mask, long kernel section, scheduling delay, 다른 thread 선택 | /proc/PID/task/*/status와 sched/irq tracepoint 확인 |
| Ctrl-C가 process에 전달되지 않음 | foreground PGID 불일치, ISIG off, VINTR 변경, signal mask | tcgetpgrp, stty -a, /proc status 확인 |
| SIGCHLD 처리 후 zombie가 남음 | 통지만 소비하고 waitpid/waitid를 호출하지 않음 | ps state, /proc/PID/status, wait syscall trace |
| signalfd와 handler가 섞여 동작 | 일부 thread가 signal을 block하지 않음 | 모든 thread의 SigBlk과 thread 생성 순서 확인 |
| hardirq에서 sleep 경고 또는 lockup | handler가 blocking API나 과도한 작업 수행 | lockdep, irqsoff tracer, handler duration histogram 확인 |
LAB
직접 확인
- 예제를 실행해 SIGUSR1, SIGCHLD, eventfd, timerfd가 도착하는 순서를 기록한다. 순서는 매번 같다는 계약이 없지만 네 원인이 모두 같은 epoll_wait에서 관찰되는지 확인한다.
- kill(getpid(), SIGUSR1)을 세 번 연속 호출하고 signalfd record 수를 센다. SIGUSR1 coalescing을 본 뒤 SIGRTMIN과 sigqueue payload로 바꿔 queue 차이를 비교한다.
- worker의 eventfd write를 세 번으로 늘리고 main read를 늦춰 반환 counter가 3인지 확인한다. readiness 한 번과 사건 수 세 번을 구분한다.
- timer interval을 10 ms로 만들고 main을 200 ms sleep시켜 timerfd read의 expiration 누적값과 POSIX signal timer의 overrun 표현을 비교한다.
- strace -f 출력에서 kill은 signal_fd write syscall로 보이지 않으며, signalfd read가 별도 record를 반환하는지 확인한다. 생성 syscall과 소비 fd를 구분한다.
- stty -isig 상태와 stty isig 상태에서 Ctrl-C를 입력해 0x03 byte 입력과 SIGINT 생성의 차이를 확인한다. foreground process group도 tcgetpgrp로 함께 기록한다.
- perf trace 또는 ftrace의 irq_handler_entry/exit, sched_wakeup, sched_switch를 함께 수집해 IRQ, wakeup, userspace 실행 시각 사이의 구간을 나눈다.
- SIGUSR1을 block한 thread와 unblocked thread를 각각 만들고 process-directed kill과 pthread_kill의 수신 thread 차이를 signalfd/sigwaitinfo로 확인한다.
./event_sourcesstrace -f -e trace=epoll_wait,signalfd4,timerfd_create,timerfd_settime,eventfd2,kill,read,write,clone,clone3,wait4 ./event_sourcesPRIMARY REFERENCES