Linux Signals – We Peek into the Kernel
So far in this series, we’ve followed signals from user-space handlers to the kernel’s signal queues.
Now, let’s go further.
In this post, we’ll trace the exact kernel functions that send, queue, and deliver signals — using both code walkthroughs and real tracing tools.
You’ll see how the kernel translates a call like sigqueue() or an event like SIGALRM into a precise chain of internal functions — and how we can watch it live.
Step 1: Meet the Core Kernel Functions
When any signal is sent (via kill(), sigqueue(), or internally from a timer or fault), the kernel runs a consistent set of routines.
Below is the conceptual call hierarchy (simplified for clarity).
send_signal()
└── __send_signal()
├── prepare_signal()
├── complete_signal()
└── signal_wake_up()
And when the process resumes execution, signal delivery happens in:
do_signal()
└── get_signal()
└── handle_signal()
└── setup_rt_frame()
Finally, when the handler returns, the kernel restores everything through:
rt_sigreturn()
└── sys_rt_sigreturn()
send_signal() – Entry Point
Defined in kernel/signal.c, this function handles permissions, queuing, and waking the target process.
Key actions:
- Validate the signal number and target process.
- Build a
siginfo_tif data is passed (fromsigqueue()). - Call
__send_signal()to enqueue it.
For example:
int send_signal(int sig, struct siginfo *info, struct task_struct *t, int group)
{
return __send_signal(sig, info, t, group);
}
__send_signal() – Core Enqueuing
This is where the heavy lifting happens.
It checks:
- Whether the signal is already pending.
- Whether it can be coalesced (non-RT) or must be queued (RT).
- Whether the target has the signal blocked.
If unblocked, it calls complete_signal().
complete_signal() – Prepare for Delivery
It decides which thread will get the signal in a thread group (important in multi-threaded programs).
It then triggers signal_wake_up() to make sure that if the thread is sleeping, it wakes up soon for signal delivery.
do_signal() and get_signal() – Delivery in Action
These functions live in arch/<arch>/kernel/signal.c (for example, arch/arm64/kernel/signal.c).
When a thread is returning from a system call or being scheduled, the kernel checks if any unblocked pending signals exist.
get_signal() picks the next pending signal, sets up the handler call, and calls:
setup_rt_frame()
This function pushes the signal frame onto the user stack — including:
- Register context (
pt_regs) - Signal number
siginfo_tucontext_tfor restoration- Return address to
rt_sigreturn
rt_sigreturn() – Back to User Space
Once the handler completes, the process invokes the special syscall:
rt_sigreturn()
The kernel:
- Pops the saved state from the signal frame.
- Restores register values and signal mask.
- Returns control to the instruction that was interrupted.
That’s the elegant cycle — from generation to handling to restoration.
Step 2: Tracing It Live with ftrace
ftrace lets us see these functions execute on a live system.
🔧 Setup Commands
# Enable function tracing
sudo su
cd /sys/kernel/debug/tracing
# Select function tracer
echo function > current_tracer
# Filter only signal-related functions
echo send_signal > set_ftrace_filter
echo __send_signal >> set_ftrace_filter
echo complete_signal >> set_ftrace_filter
echo do_signal >> set_ftrace_filter
echo get_signal >> set_ftrace_filter
echo setup_rt_frame >> set_ftrace_filter
echo sys_rt_sigreturn >> set_ftrace_filter
# Start tracing
echo 1 > tracing_on
# Run a test process that triggers a signal
kill -SIGUSR1 <pid>
# Stop tracing and read results
echo 0 > tracing_on
cat trace | grep signal
Expected Output
<...>-1234 [000] .... send_signal -> __send_signal
<...>-1234 [000] .... complete_signal -> signal_wake_up
<...>-1234 [000] .... do_signal -> get_signal
<...>-1234 [000] .... setup_rt_frame
<...>-1234 [000] .... sys_rt_sigreturn
⚡ Step 3: Tracing Timer-based Signals with perf
Timer-generated signals (SIGALRM, SIGRTMIN+n) can also be traced.
sudo perf record -e signal:signal_generate,signal:signal_deliver ./your_app
sudo perf script | grep signal:
This shows both generation (signal_generate) and delivery (signal_deliver) events, along with process name, PID, and signal number.
Step 4: Visualizing Signal Flow
You can visualize this pipeline as:
sigqueue()/timer → send_signal() → __send_signal()
↓
pending queue (sigpending/sigqueue)
↓
get_signal() → setup_rt_frame() → handler()
↓
sigreturn()
Step 5: What You’ve Learned
- How kernel code builds and enqueues signals.
- The real kernel functions in play.
- How to trace them live using
ftraceandperf. - The full circle: generation → delivery → return.
Now you know not just that signals work — but how they travel through the kernel.
🔮 Next in the Series – “Signals Meet Scheduling”
The next post will explore:
- How Linux scheduler cooperates with signal delivery.
- How the kernel decides when to deliver signals to a running vs. sleeping process.
- Why signals sometimes appear “delayed” — and what really happens behind it.
Stay tuned. That’s where signals meet CPU scheduling, and the timing gets even more fascinating.
Build Your Linux Foundations
If you want to go from user-space to kernel-level mastery:
- Linux Rapid Mastery → Learn Linux Fundamentals, Applications, Driver Basics & Kernel Internals
👉 https://embitude.in/lrm
Join the community of passionate embedded engineers:
👉 https://embitudeinfotech.graphy.com/s/community