Embitude Infotech1

Linux Signals Deep Dive Part-4

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:

  1. Validate the signal number and target process.
  2. Build a siginfo_t if data is passed (from sigqueue()).
  3. 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_t
  • ucontext_t for 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 ftrace and perf.
  • 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

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top