In Part 1 of this series, we explored how user-space programs handle Linux signals using sigaction(), masks, and handlers.
But have you ever wondered what happens before that?
🤔 When you press Ctrl + C on your terminal…
- How does that event become a signal?
- How does the kernel know which process should receive it?
- What exact kernel functions handle this delivery?
Let’s decode the complete journey — from keyboard interrupt ➜ TTY subsystem ➜ kernel signal mechanism ➜ target process delivery.
🧩 Q1: When I Press “Ctrl + C”, What Actually Happens?
When you press Ctrl + C, the keyboard hardware generates an interrupt (IRQ).
This interrupt is handled by the keyboard driver (through the input subsystem).
The kernel maps Ctrl + C (ASCII 0x03) to a special control character called INTR — short for interrupt.
This key combination is interpreted by the TTY (teletypewriter) driver, which is responsible for managing terminal I/O.
So the event sequence looks like this:
Keyboard IRQ → Input Driver → TTY Line Discipline → Kernel Converts to SIGINT
🧠 Q2: How Does the Kernel Know Which Process to Signal?
Each terminal session is associated with a foreground process group.
When you run a command like:
$ ./myapp
your shell creates a process group for that command and assigns it as the foreground process group for the TTY.
When the user hits Ctrl + C, the TTY subsystem sends a signal (SIGINT) to every process in that foreground group.
Internally, this is done by a kernel function:
kill_pgrp(fg_pgrp, SIGINT, 1);
So the signal is not sent to just one process — it’s broadcast to all processes currently interacting with that terminal.
⚙️ Q3: What Function Actually Sends the Signal?
The kill_pgrp() function eventually calls group_send_sig_info(), which is responsible for sending the signal to all tasks in a process group.
At the core of signal delivery lies:
send_sig_info(int sig, struct siginfo *info, struct task_struct *t)
This is the heart of Linux signal delivery.
It inserts the signal into the target process’s pending signal queue (t->pending) and sets the appropriate flag so that the process will handle it when scheduled next.
🔍 Q4: Where Is the Signal Stored?
Each process in Linux (represented by task_struct) contains:
- A signal handler table (
struct sighand_struct) - A pending signal list (
struct sigpending) - A blocked signal mask
When a signal is sent, it’s added to the pending queue.
If the signal is not blocked by the process’s mask (sigprocmask), it’s delivered the next time the process runs in user mode.
🧠 Q5: How Does the Kernel Decide When to Deliver the Signal?
During a context switch, the scheduler checks whether the target process has any pending unblocked signals.
If yes, before returning to user mode, the kernel modifies the process’s instruction pointer so that execution jumps to the signal handler defined by sigaction().
That’s how the magic happens:
→ Signal arrives while in kernel mode
→ Process context is modified
→ On return to user mode, the signal handler executes
🧩 Q6: What Happens if the Process Ignores or Blocks the Signal?
If the signal is blocked (via sigprocmask) or temporarily masked using sa_mask, it stays queued.
The kernel maintains pending signals until they are unblocked or discarded.
If ignored (SIG_IGN), the kernel simply discards it.
For uncatchable signals like SIGKILL and SIGSTOP, delivery is immediate and unconditional — they bypass all checks.
⚡ Q7: Is Every Signal Delivered Instantly?
No — and that’s an important subtlety.
Signals are asynchronous but deferred.
That means the kernel only delivers them when the target process is about to return to user space or enters a state where it can handle them safely.
For instance, if a process is in kernel mode executing a system call, the signal waits until it’s safe to deliver.
🔬 Q8: What Kernel Functions Drive the Delivery Mechanism?
Here’s the simplified internal call chain:
kill_pgrp()
→ group_send_sig_info()
→ send_sig_info()
→ __send_signal()
→ complete_signal()
→ signal_wake_up()
These functions:
- Locate the target process (or group)
- Queue the signal
- Wake up the process if it’s sleeping
Once awake, the process checks its signal queue and runs the appropriate handler in user space.
🧩 Q9: Where Does the Signal Meet User Space?
When the process resumes from kernel mode, the architecture-specific signal trampoline (e.g., do_signal() or get_signal()) sets up the user stack frame for the handler:
- It stores the process’s current register context
- Points the instruction pointer to the signal handler’s address
- And ensures that when the handler finishes, a special trampoline code (
sigreturn) will restore everything.
That’s how user space gets control back — executing your custom handler function that you registered using sigaction() in Part 1.
So the entire journey looks like this:
Ctrl + C (keyboard)
↓
Keyboard Driver (input event)
↓
TTY Line Discipline
↓
kill_pgrp()
↓
send_sig_info()
↓
__send_signal() → pending queue
↓
Scheduler context switch
↓
get_signal() → jump to handler
🧩 Q10: How Does the Process Return to Where It Was Interrupted?
When your signal handler finishes, control doesn’t automatically jump back.
Instead, the kernel has prepared a user-space trampoline function, usually invoked through the sigreturn system call.
Here’s what happens under the hood:
- Signal handler executes.
- The handler ends normally or calls
return. - The kernel-provided sigreturn code runs.
- It restores:
- Program Counter (PC)
- Stack Pointer (SP)
- CPU registers and flags
- Previous signal mask
- Execution continues exactly where it left off — as if the signal never happened.
It’s like a context save and restore mechanism — except triggered by a software interrupt instead of a hardware one.
This is how signals remain non-destructive and maintain application flow integrity.
🧭 Q11: Can We Trace Signal Delivery in Real Systems?
Absolutely!
You can use tracing tools like:
sudo strace -e signal=all ./app
sudo perf trace -e signal:* ./app
These commands let you observe when signals are generated, queued, and handled — invaluable when debugging complex Linux systems.
🧭 The Life of a Signal — Ctrl+C Edition
| Stage | Component | Function/Action |
|---|---|---|
| Keyboard Input | Input Subsystem | Generates IRQ |
| TTY Layer | Line Discipline | Maps Ctrl+C to SIGINT |
| Kernel Core | kill_pgrp() | Sends SIGINT to foreground group |
| Signal Subsystem | send_sig_info() | Queues signal in task_struct |
| Scheduler | Context Switch | Checks pending unblocked signals |
| User Space | Signal Handler | Executes registered function |
| sigreturn() | Kernel trampoline | Restores context to original point |
So every time you press Ctrl + C, Linux orchestrates this intricate dance between hardware, kernel, and user space — and all of it happens in microseconds ⚡
🔮 Coming Up Next – The Signals We Don’t Expect!
So far, we explored a user-generated signal (SIGINT) from a terminal.
But what about the signals generated by the kernel or by other processes?
💭 What happens when your program segfaults (SIGSEGV)?
💭 Or when a timer expires (SIGALRM)?
💭 Or when one process sends a custom signal (SIGUSR1) to another?
In Part 3 of this series — “Signals from the System: SIGSEGV, SIGALRM & SIGUSR1” — we’ll trace these cases from fault detection and timers to inter-process signaling and queued delivery.
Stay tuned — this is where things get even deeper! 🔥
🚀 Want to Master Linux from the Ground Up?
If this behind-the-scenes exploration excited you, it’s time to build the foundation that makes such understanding possible.
🎯 Check out Linux Rapid Mastery— a launchpad to Linux Driver Development, built for engineers who want to move fast.
💡 You’ll Learn
- Linux Fundamentals
- Linux Application Development
- Linux Driver Fundamentals
- Linux Kernel Internals
✅ Weekly Live Meetups
✅ Assignment Reviews
✅ Career Guidance & Mentorship
Your journey from Linux user → Linux driver developer starts here.
👉 Enroll in Linux Rapid Mastery