Signal / Event / IPC · Linux userspace / kernel ABI

Where do interrupts, events, and signals originate?

Separates hardware IRQs, kernel state changes and wakeups, and process signals into distinct boundaries, then traces signal generation from kill, faults, timers, TTY input, and child termination.

Series
21 / 38
Build
cc -std=c17 -Wall -Wextra -O2 -pthread event_sources.c -o event_sources
Run
./event_sources
Kernel
Linux 6.18.37 LTS

Does a hardware interrupt immediately become a userspace signal or epoll event?

An interrupt is a change of execution context in which the CPU transfers the current flow to a kernel interrupt handler. An event is a broad term for the fact that state changed; Linux has no single universal object named event. A wait-queue wakeup, poll readiness, an eventfd counter, a completion, and an input event all have subsystem-specific storage formats and consumption rules.

A signal is an asynchronous notification protocol targeting a task or thread group. Signals can be generated without an IRQ by the kill syscall, a page fault, timer expiration, a terminal control character, or child termination. Conversely, a device IRQ may end entirely inside a driver. A common path has an IRQ put data in a ring buffer and wake a wait queue, after which userspace observes it through read or epoll; no signal is involved.

Structure diagram

Figure 1. Different signal sources converging on a common pending path
kill / tgkill userspace syscall · target/permission checks do_send_sig_info()
page fault / trap unresolvable userspace exception force_sig_fault()
POSIX timer SIGEV_SIGNAL timer expiration posix_timer_event()
terminal ISIG VINTR·VQUIT·VSUSP input n_tty_receive_signal_char()
child lifecycle notify parent of exit·stop·continue do_notify_parent()
file fasync notify O_ASYNC subscribers kill_fasync()
__send_signal_locked() record in task or thread-group pending bitmap/queue · standard-signal coalescing · realtime sigqueue
pending / wakeupcomplete_signal · TIF_SIGPENDING · candidate target-thread selection
selectionget_signal · mask · disposition · ptrace/default-action decision
delivery / consumert_sigframe+handler, or signalfd/sigwaitinfo consumes the pending record

A signal is not another name for a hardware IRQ. Signals originate in different contexts such as syscalls, synchronous CPU exceptions, timers, the terminal line discipline, process lifecycle, and fasync, then follow shared enqueue and delivery rules.

Call path

Figure 2. From userspace code to observable results
cause device IRQ · CPU fault · syscall · timer
kernel context hardirq · exception · process context
record state ring · counter · pending bit/queue
wake up wait queue · task wakeup · TIF_SIGPENDING
userspace observation read · epoll · signalfd · handler

An interrupt is a cause of kernel entry that executes code, an event is an observable state change, and a signal is a delivery protocol that leaves pending state on a task. They can be connected, but they are not automatically converted one-to-one.

Figure 3. Major points along the kernel-internal path
IRQ entry CPU enters through a vector and finds irq_desc
primary handler check device status, ACK, and do minimum work
deferred work softirq · threaded IRQ · workqueue
publish update buffer/counter, then wake_up
fd operation recheck poll readiness and consume with read

This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.

Linux 6.18.37 LTS source locations

Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.

FileFunction / structureWhat to inspect
kernel/irq/handle.c handle_irq_event(), handle_irq_event_percpu() The common path that calls an irqaction handler in hardirq context and passes IRQ_WAKE_THREAD to a threaded handler
kernel/softirq.c irq_exit_rcu(), invoke_softirq(), __do_softirq() The context boundary that runs pending softirqs after hardirq exit or hands them to ksoftirqd
kernel/sched/wait.c __wake_up_common(), __wake_up_common_lock() The common wakeup path through which a producer makes tasks on a wait queue runnable
fs/eventpoll.c ep_poll_callback(), ep_send_events() The path connecting a target file's wait-queue wakeup to an epoll ready list and a userspace events array
kernel/signal.c kill_something_info(), do_send_sig_info(), group_send_sig_info() The starting point where a kill-family syscall checks PID type and permission before creating a process-directed signal
kernel/signal.c __send_signal_locked(), complete_signal(), get_signal() The central path where several producers converge on shared pending bitmaps/queues and select a signal eligible for delivery
kernel/signal.c force_sig_fault(), force_sig_info_to_task() The path that converts a synchronous exception such as a page fault or illegal instruction into a signal carrying si_addr and si_code
kernel/time/posix-timers.c posix_timer_fn(), posix_timer_event() The starting point where POSIX timer expiration enters the signal queue according to a SIGEV_SIGNAL setting
drivers/tty/n_tty.c n_tty_receive_signal_char(), __isig() The line-discipline path that converts VINTR, VQUIT, and VSUSP into SIGINT, SIGQUIT, and SIGTSTP for the foreground process group
kernel/signal.c do_notify_parent() The process-lifecycle path that reports child exit/stop/continue state to the parent through SIGCHLD and wait status
fs/fcntl.c kill_fasync(), kill_fasync_rcu() The path through which a driver/file using O_ASYNC and fasync registration sends SIGIO or a selected signal
fs/signalfd.c signalfd_read(), signalfd_poll() The path that consumes already-generated pending signals as fd records and readiness instead of through a handler
arch/x86/kernel/signal.c arch_do_signal_or_restart(), setup_rt_frame() The path that saves register context and the signal mask in an rt_sigframe immediately before return to userspace and establishes the handler entry point

Complete runnable example

The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.

Buildcc -std=c17 -Wall -Wextra -O2 -pthread event_sources.c -o event_sources
01#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

Source line 55pthread_sigmask(SIG_BLOCK

Blocks SIGUSR1 and SIGCHLD before creating the fd. New threads inherit the mask, closing the path where an asynchronous handler runs unexpectedly in any thread and allowing signalfd to consume pending signals.

Source line 58signalfd(-1, &mask

signalfd does not generate new signals. When a signal in its mask enters task or thread-group pending state, it exposes the signal as a readable struct signalfd_siginfo record.

Source line 59timerfd_create(CLOCK_MONOTONIC

Represents expiration as a file containing an 8-byte expiration counter instead of a signal. CLOCK_MONOTONIC is used for elapsed time to avoid wall-clock adjustments.

Source line 61eventfd(0, EFD_CLOEXEC

Creates a credit counter with which the worker wakes the main loop. Do not equate one write with one epoll event; inspect the accumulated uint64_t value returned by read.

Source line 34epoll_ctl(epfd, EPOLL_CTL_ADD

Registers all three fds in one interest list. epoll does not unify the causes of signal, timer, and thread events; it only gathers poll readiness for their files into one array.

Source line 44write(event_fd, &credit

The worker increments the eventfd counter from userspace process context. This demonstrates that a complete fd event and wakeup can be created without a hardware interrupt.

Source line 75pid_t child = fork()

The child leaves termination status in _exit. The kernel's parent-notification path generates SIGCHLD, and the zombie is released when the parent collects the status with waitpid.

Source line 84kill(getpid(), SIGUSR1)

A purely software signal that begins at the kill syscall. Permission checks can create process-directed pending state without a device IRQ or timer.

Source line 90epoll_wait(epfd, events

While the main thread is not runnable, it sleeps on the epoll wait queue. A producer's wakeup does not immediately call a userspace callback; it makes the task runnable, and the scheduler decides when it actually executes.

Source line 100read(signal_fd, &info

A signal fd is a stream of fixed-size records. Because it is nonblocking, drain pending records until EAGAIN after one EPOLLIN.

Source line 106info.ssi_signo == SIGCHLD

A signalfd record is notification, not the reclamation of child resources. After reading SIGCHLD, waitpid or waitid must still collect the child exit status.

Source line 120read(timer_fd, &expirations

A timerfd read returns the number of expirations accumulated since the previous read. The value can exceed 1 when the event loop is late.

Source line 128read(event_fd, &credits

A normal eventfd read returns the entire current counter and resets it to 0. EFD_SEMAPHORE applies a different rule that consumes 1 per read.

Source line 138pthread_join(thread

Observing an event and ending the producer thread's lifetime are separate operations. Reclaim thread resources with join, then close the shared fd.

Detailed behavior

01

First separate interrupts, exceptions, events, and signals

A hardware interrupt is an asynchronous request made by a device independently of the CPU's current instruction flow. A CPU exception is a synchronous event tied to execution of the current instruction; page faults, divide errors, and invalid opcodes are examples. Linux documentation and code may group both broadly under interrupt/exception entry, but their causes and restart rules differ.

Event is a general name for a state change, not an ABI name. An epoll event is a readiness snapshot, an inotify event is a variable-length queued record, eventfd is a uint64 counter, and a completion is a kernel synchronization object. Sharing the word event does not make loss, ordering, and accumulation rules identical.

A signal is a task-notification protocol with a number, disposition, mask, pending state, siginfo, and default action. An interrupt-context routine is not called a signal handler, and a userspace signal handler does not run in hardirq context.

02

The path from a device IRQ to a userspace read crosses several boundaries

When a device completes a DMA descriptor and raises an interrupt line or MSI, the CPU passes through architecture entry code to find irq_desc and irqaction. The primary handler reads device status, ACKs or masks the cause, and preserves the minimum state required for later processing.

Work that may take longer, such as packet parsing or follow-up after block completion, is deferred to a softirq, NAPI, threaded IRQ, tasklet, workqueue, or similar mechanism. The choice depends on the subsystem and driver's locking, ability to sleep, and latency goals.

Finally, the driver publishes a ring index, counter, or file state and wakes a wait queue, making a blocked task runnable. Data ownership actually moves only after the scheduler selects the task, epoll_wait returns, and userspace calls read. wake_up itself does not invoke a userspace function.

03

IRQs and events are not one-to-one

One interrupt can process several descriptor completions, and interrupt coalescing combines several packets behind one IRQ. Conversely, a level-triggered interrupt can be observed repeatedly until its cause is removed. Do not interpret IRQ count as I/O-operation count.

poll/epoll readiness is not an event count either. Readable means that read can currently make progress or discover EOF/error. Even if a producer fills a buffer several times, the fd can remain in one ready state; an edge-triggered loop must drain to EAGAIN before it can receive another edge.

APIs with explicit counters, such as eventfd and timerfd, retain accumulated values, but still have counter width, overflow, and read-mode rules. Decide first whether events may be lost, counts are required, or only the latest state matters.

04

Read a signal in four stages: generation, pending, selection, and delivery

Generation is when a producer chooses the signal number and siginfo and identifies the target. The signal is then recorded in the task or shared-signal pending bitmap/queue. If a standard signal of the same number is already pending, new occurrences can coalesce; realtime signals can queue siginfo records in order.

complete_signal finds a candidate receiving thread and marks signal work through TIF_SIGPENDING and related state. As the target thread prepares to return to userspace, get_signal consults the mask and disposition and selects the signal that will actually be handled.

During handler delivery, architecture code creates an rt_sigframe on the user stack or altstack, records saved registers, the old mask, and siginfo/ucontext, then changes the instruction pointer to the handler. After the handler ends, rt_sigreturn validates the frame and restores context. This is why generation time and handler-instruction execution time differ.

05

kill, tgkill, and pidfd_send_signal begin with target selection

Depending on the sign of pid, kill(pid, sig) can target one process, the caller's process group, a specified process group, or multiple permitted processes. The kernel resolves the PID in a namespace, checks permission according to credentials and signal type, then enters the group_send_sig_info family.

tgkill(tgid, tid, sig) validates both the thread group and thread ID and creates thread-directed pending state. pthread_kill is a libc interface over this syscall family. A process-directed signal in a multithreaded program can be received by any thread that does not block it, so distinguish the target semantics.

pidfd_send_signal targets the process instance referenced by a pidfd, reducing numeric-PID reuse problems. Whichever API is used, the same mask and disposition rules apply after the common signal-enqueue path.

06

Page faults and illegal instructions create synchronous signals

When a userspace instruction accesses an invalid address, a CPU page-fault exception enters the kernel. If the kernel resolves it as normal demand paging, COW, or stack growth, execution returns to the same instruction and no signal is generated.

If no VMA exists or permissions do not match and the fault cannot be resolved, architecture fault code calls the force_sig_fault family to record SIGSEGV with SEGV_MAPERR or SEGV_ACCERR and the fault address. Other exception types generate corresponding signals, such as SIGILL for an invalid opcode and SIGFPE for a divide error.

This signal targets the current faulting thread, so its cause differs from a normal kill. If a handler returns without correcting the cause, the same instruction may fault again; the default action can lead to a core dump and process termination.

07

A timer can be a signal or an fd event, depending on its configured notification method

When a POSIX timer uses SIGEV_SIGNAL, its hrtimer callback updates expiration state and overrun information and sends a notification to the signal queue. If several expirations occur while the signal is pending, interpret the timer overrun value as well.

setitimer and alarm also generate SIGALRM-family signals on expiration, but standard-signal coalescing makes them unsuitable as expiration counters. timerfd increments a file counter and wakes a poll wait queue instead of creating signal-pending state.

Even with the same clock source and hrtimer foundation, choosing a signal or fd userspace ABI changes masks, queues, event-loop integration, and consumption. Equating a timer interrupt directly with SIGALRM skips the timer subsystem and the notification choice in between.

08

Ctrl-C does not turn directly from a keyboard IRQ into SIGINT

Input bytes from a keyboard or terminal emulator enter the tty driver, where the line discipline applies termios settings. In canonical n_tty, when ISIG is enabled and a byte matches VINTR, __isig sends SIGINT to the foreground process group.

Consequently, writing a control character to the master side of a pseudoterminal can produce the same tty semantics without a physical keyboard IRQ. Conversely, in raw mode or with ISIG disabled, 0x03 can be read as an ordinary input byte.

The SIGINT target is the terminal foreground process group, not necessarily one process visible on the screen. This is why a shell places a pipeline in one process group and transfers foreground ownership with tcsetpgrp.

09

SIGCHLD is generated by the process lifecycle and serves a different role from wait

When a child enters exit, stop, or continue state, the kernel checks the parent's SIGCHLD disposition and SA_NOCLDSTOP/SA_NOCLDWAIT settings before notification. siginfo can contain the child PID, UID, a code such as CLD_EXITED, and status.

SIGCHLD notifies the parent of a state change; waitpid/waitid performs the operation that collects exit status and resource usage. A zombie can remain if code only reads SIGCHLD in a handler or signalfd and never waits.

When several children finish at nearly the same time, standard SIGCHLD signals can coalesce, so do not perform exactly one wait per signal. Repeat a WNOHANG loop until no more children can be collected.

10

SIGIO and fasync are an optional path for delivering file events as signals

When an application sets F_SETOWN and O_ASYNC/FASYNC, a file or driver can register subscribers in a fasync_struct list. On a state change, kill_fasync sends the owner SIGIO or the signal selected with F_SETSIG.

Not every fd supports fasync, and readiness, the signal queue, races, and coalescing interact. Counting every occurrence with standard SIGIO for high-rate I/O may be unsuitable because of loss and handler cost.

The device API and workload determine whether to use epoll, io_uring, blocking read, or SIGIO. The hardware IRQ does not directly fire a signal; rather, the file implementation becomes a signal producer when it chooses an fasync policy.

11

signalfd does not change the cause that generated a signal

Target signals must be blocked before signalfd is created. Otherwise, normal handler/default-action delivery races with fd consumption. Apply a consistent mask policy in every thread of the process.

When a signal becomes pending, signalfd poll reports readable and read obtains one or more signalfd_siginfo records. SIGUSR1 generated by kill and SIGCHLD generated by the lifecycle appear on the same fd, but ssi_code, sender, and status distinguish their causes.

SIGKILL and SIGSTOP cannot be blocked or consumed through signalfd. Also avoid designs that route every synchronous fault signal exclusively to signalfd and repeatedly execute the faulty instruction.

12

Allowed work and sources of delay differ by context

hardirq context cannot sleep, and its handler must remain short. A softirq likewise cannot use blocking mutexes or userspace access like an ordinary process; prolonged load affects ksoftirqd scheduling and other IRQ latency. Threaded IRQs and workqueues provide sleepable process context but add scheduling delay.

Time from wake_up to epoll_wait returning includes the runnable queue, CPU affinity, priority, preemption, interrupt masking, and softirq backlog. Time from signal pending to handler entry also grows if the target thread does not return from the kernel or blocks the signal.

To locate the bottleneck context, measure IRQ timestamp, state publication, wakeup, scheduler switch, syscall return, and userspace consumption separately. Combining them into one average easily hides upper-tail causes.

Objects and lifetimes

ObjectCreation and releaseValues to inspect
irq_desc / irqactionRetains handler and chip/domain state between IRQ registration and removaltrigger type, disable depth, action flags, threaded handler
wait_queue_headAttached to a subsystem object or file lifetime and manages sleeper/callback registrationwait entry, wake function, lock, exclusive flag
eventpoll / epitemCreated by epoll_create and EPOLL_CTL_ADD; detached by DEL or fd closeinterest mask, ready list, user data
signal_struct / sighand_structA thread group shares pending state and dispositions, which are copied, reset, or released according to fork/exec/exit rulesshared_pending, action, flags
task_struct.pendingRemains on the task from creation of a thread-directed signal until get_signal consumes itsignal bitmap, sigqueue list, TIF_SIGPENDING
sigqueue / kernel_siginfoAllocated when a queued signal needs a record and released on dequeue or target exitsi_code, sender, si_addr, payload, overrun
rt_sigframeCreated on the user stack at handler delivery and consumed by rt_sigreturnsaved registers, old mask, siginfo, ucontext
signalfd_ctxRetains the observation mask from signalfd creation until file closeMask and pending-signal poll/read results

Failure conditions and common misconceptions

Observed symptomLikely causesHow to verify
IRQ count differs from packet countInterrupt coalescing, NAPI budget, or several completions per IRQCompare driver statistics, trace_irq_handler, and ring producer/consumer indexes
read returns EAGAIN after an epoll eventA race between readiness and consumption, or another thread drained firstCheck the nonblocking return value and fd-ownership policy
Several signals were sent but only one was receivedstandard signal pending bit coalescingConsider realtime signals, an eventfd counter, or an application queue
A signal handler executes lateThe target mask, a long kernel section, scheduling delay, or selection of another threadInspect /proc/PID/task/*/status and sched/irq tracepoints
Ctrl-C is not delivered to the processForeground PGID mismatch, ISIG off, changed VINTR, or the signal maskInspect tcgetpgrp, stty -a, and /proc status
A zombie remains after SIGCHLD handlingOnly the notification was consumed; waitpid/waitid was not calledps state, /proc/PID/status, wait syscall trace
signalfd and a handler both operateSome threads do not block the signalCheck SigBlk in every thread and the thread-creation order
A sleep warning or lockup occurs in hardirqThe handler uses a blocking API or performs excessive workInspect lockdep, the irqsoff tracer, and a handler-duration histogram

Verify it yourself

  1. Run the example and record the arrival order of SIGUSR1, SIGCHLD, eventfd, and timerfd. The order is not guaranteed to be identical each time; verify that all four causes are observed through the same epoll_wait.
  2. Call kill(getpid(), SIGUSR1) three times in a row and count signalfd records. After observing SIGUSR1 coalescing, switch to SIGRTMIN with a sigqueue payload and compare queue behavior.
  3. Increase the worker's eventfd writes to three and delay the main read; verify that the returned counter is 3. Distinguish one readiness notification from three occurrences.
  4. Set the timer interval to 10 ms and make main sleep for 200 ms, then compare accumulated timerfd expirations with POSIX signal-timer overrun reporting.
  5. In strace -f output, confirm that kill does not appear as a write syscall to signal_fd and that signalfd read returns a separate record. Distinguish the generating syscall from the consuming fd.
  6. Press Ctrl-C under stty -isig and stty isig and compare a 0x03 input byte with SIGINT generation. Record the foreground process group with tcgetpgrp as well.
  7. Collect irq_handler_entry/exit, sched_wakeup, and sched_switch with perf trace or ftrace to separate the intervals between IRQ, wakeup, and userspace execution.
  8. Create one thread that blocks SIGUSR1 and one that does not, then use signalfd/sigwaitinfo to compare the receiving thread for process-directed kill and pthread_kill.
Run./event_sources
Tracestrace -f -e trace=epoll_wait,signalfd4,timerfd_create,timerfd_settime,eventfd2,kill,read,write,clone,clone3,wait4 ./event_sources

Primary sources