Embitude Infotech1

Linux Signals Deep Dive Part-3

🧭 Introduction — Why these three Linux Signals?

Linux Signals come from different sources. Some are generated by hardware faults. Some come from timers. Some come from other processes.

In this post we ask three simple but important questions:

  • What happens when your program segfaults (SIGSEGV)?
  • How do timer signals like SIGALRM work?
  • How can one process send data to another with SIGUSR1?

We answer them with short explanations, examples, and practical tips.


❓ Q1 — What exactly causes SIGSEGV and how is it delivered?

Short answer: SIGSEGV is generated by the kernel when a process accesses invalid memory. The kernel detects the fault and sends SIGSEGV to the offending process.

Details, step by step:

  1. The CPU executes an instruction that reads or writes an invalid address.
  2. The MMU raises a page fault.
  3. The kernel handles the fault and decides the cause.
  4. If the access is invalid, the kernel sends SIGSEGV to that process (using internal signal send functions).
  5. If the process has a handler, the kernel arranges to deliver the signal. Otherwise the default action is to terminate and (often) produce a core dump.

Key point: You can catch SIGSEGV with sigaction(). However, catching it requires care. The process state may be inconsistent. Use sigaltstack() to ensure the handler runs on a safe stack.


🔧 Example — Catching SIGSEGV safely (SA_SIGINFO + sigaltstack)

/* sigsegv_handler.c */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ucontext.h>

static void segv_handler(int sig, siginfo_t *si, void *arg) {
(void)arg;
printf("Caught SIGSEGV: addr=%p, si_code=%d\n", si->si_addr, si->si_code);
_exit(1); // safest action: exit or attempt limited recovery
}

int main(void) {
struct sigaction sa;
stack_t ss;
// set up alternate stack
ss.ss_sp = malloc(SIGSTKSZ);
if (!ss.ss_sp) { perror("malloc"); return 1; }
ss.ss_size = SIGSTKSZ;
ss.ss_flags = 0;
if (sigaltstack(&ss, NULL) == -1) { perror("sigaltstack"); return 1; }

memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = segv_handler;
sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGSEGV, &sa, NULL) == -1) { perror("sigaction"); return 1; }

// trigger segfault
int *p = NULL;
*p = 42; // deliberate crash

return 0;
}

Notes:

  • SA_SIGINFO gives siginfo_t (contains faulting address in si->si_addr).
  • sigaltstack() avoids stack overflow if the fault happened on the normal stack.
  • Prefer to log and exit safely rather than attempt complex recovery.

❓ Q2 — How do timer-driven signals like SIGALRM work?

Short answer: The kernel or a user-space timer triggers SIGALRM. The kernel posts the signal to the process. The usual way to set a timer is alarm() or setitimer(). For higher precision, use POSIX timers (timer_create() + timer_settime()), which can deliver real-time signals with data.

Behavior highlights:

  • alarm() schedules SIGALRM after N seconds.
  • setitimer() can set one-shot or periodic timers.
  • Timers are tracked by the kernel. When time expires, the kernel queues the signal.
  • If SIGALRM is blocked, it is queued until unblocked.

🔧 Example — SIGALRM with sigaction() and setitimer()

/* sigalrm_example.c */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <signal.h>
#include <sys/time.h>
#include <unistd.h>
#include <string.h>

void alarm_handler(int sig) {
(void)sig;
static int count = 0;
printf("SIGALRM fired: count=%d\n", ++count);
if (count >= 3) _exit(0);
}

int main(void) {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = alarm_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGALRM, &sa, NULL);

struct itimerval tv;
tv.it_value.tv_sec = 1; // first expire after 1s
tv.it_value.tv_usec = 0;
tv.it_interval.tv_sec = 1; // then every 1s
tv.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &tv, NULL);

while (1) pause();
return 0;
}

Notes:

  • Use sigaction() for robust behavior.
  • For per-thread or high-resolution needs, prefer POSIX timers.
  • If you need data with a timer, create a timer with timer_create() using SIGEV_SIGNAL or SIGEV_THREAD_ID and deliver a queued signal with value.

❓ Q3 — How can one process send SIGUSR1 (with data) to another?

Short answer: Use kill() for simple signals. Use sigqueue() to send a signal plus a small integer or pointer-sized value (union sigval). The receiving process can read that value from siginfo_t in a SA_SIGINFO handler.

Why use sigqueue()?

  • It allows queued delivery for real-time signals.
  • It attaches a small payload (sigval) to the signal.
  • It is useful for lightweight IPC or notifications.

🔧 Example — Sender and Receiver with sigqueue() and SA_SIGINFO

Receiver (prints extra data):

/* usr1_receiver.c */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>

void usr1_handler(int sig, siginfo_t *si, void *u) {
(void)u;
printf("Received signal %d from pid=%ld, value=%d\n",
sig, (long)si->si_pid, si->si_value.sival_int);
}

int main(void) {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = usr1_handler;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
sigaction(SIGUSR1, &sa, NULL);

printf("Receiver PID: %ld\n", (long)getpid());
while (1) pause();
return 0;
}

Sender (sends integer payload):

/* usr1_sender.c */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
if (argc < 3) {
fprintf(stderr, "Usage: %s <pid> <value>\n", argv[0]);
return 1;
}
pid_t pid = (pid_t)atoi(argv[1]);
int val = atoi(argv[2]);

union sigval sv;
sv.sival_int = val;
if (sigqueue(pid, SIGUSR1, sv) == -1) {
perror("sigqueue");
return 1;
}
return 0;
}

Notes:

  • sigqueue() requires appropriate permissions (same UID or CAP_SYS_ADMIN).
  • Use real-time signals (SIGRTMIN+n) if you need queuing guarantees and ordering.
  • siginfo_t also contains the sender PID in si_pid.

⚡ Practical Tips and Pitfalls

  • Do not assume safety after SIGSEGV. The process state may be corrupted. Prefer to log and exit.
  • Always use sigaction() rather than signal() for portability, predictable semantics, and SA_SIGINFO support.
  • Use sigaltstack() when handling signals that may be caused by stack overflows or corrupted stacks (e.g., SIGSEGV).
  • Real-time signals are preferable for queued delivery and for carrying payloads.
  • Avoid heavy work in signal handlers. Keep handlers minimal: set flags, write to a pipe, or call async-safe functions only. Use the main loop to perform lengthy actions.

🔎 Tracing and Debugging Signals

  • strace -e signal=all ./app shows delivered signals to a process.
  • gdb can catch signals and show backtraces on SIGSEGV. Use handle SIGSEGV stop in gdb.
  • For inter-process signaling issues, verify permissions and target PID.

🔮 Next in the Series

We covered:

  • Kernel-generated faults (SIGSEGV),
  • Timer signals (SIGALRM), and
  • Inter-process signals with payload (SIGUSR1 + sigqueue()).

Next, we will dig into kernel internals:

  • How the kernel assembles siginfo_t for each case,
  • Where the kernel stores queued signals, and
  • The exact kernel call paths that implement sigqueue() and timer-based signal delivery.

🚀 Build Your Linux Skills Faster

If you want structured learning that takes you from fundamentals to kernel internals, check out these programs:

Leave a Comment

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

Scroll to Top