Linux System Programming · beginner · ~6 min

raise() — sending a signal to yourself

- By the end you can call raise(signo) to deliver a signal to the calling process (or, when threads are involved, the calling thread). - By the end you can explain why raise(signo) is equivalent to kill(getpid(), signo) for a single-threaded program, and where that equivalence breaks down. - By the end you can implement the cleanup-then-re-raise idiom so a parent sees the correct WIFSIGNALED/WTERMSIG cause of death. - By the end you can use raise() to fire your own handler on demand for testing, and know why raise(SIGKILL) or raise(SIGSTOP) cannot be caught. - By the end you can read raise()'s return value and avoid unsafe work inside a signal handler.

Overview

You already met the common signals (SIGINT, SIGTERM, SIGSEGV, SIGUSR1/2, SIGKILL, SIGSTOP) and what their default actions are. This lesson gives you the simplest way to generate a signal from inside your own program: raise(). Where installing a handler with sigaction decides what happens when a signal arrives, raise() is the trigger that makes one arrive right now, aimed at yourself.

Think of raise(signo) as a one-line shorthand for kill(getpid(), signo): it asks the kernel to deliver signo to the current process. Because delivery of a synchronous, unblocked signal happens before raise() returns, it is the cleanest way to test a handler you just wrote, or to end your process with a signal rather than a plain exit() so that whoever launched you learns the real reason it stopped.

Why it matters

Real programs use raise() to die honestly. When a daemon or CLI tool catches a fatal signal, does its cleanup, and then wants to terminate, the correct pattern is to restore the default disposition and raise() the same signal — that way its parent (a shell, systemd, a supervisor) reads the exit status and sees "killed by SIGTERM" instead of a misleading normal exit code. Shells rely on exactly this: $? becomes 128+signo only when a child actually died from a signal. raise() also drives test suites for signal handling and lets you convert an internal invariant failure into raise(SIGABRT) (which is what abort() does under the hood), producing a core dump for post-mortem debugging.

Core concepts

raise() is "signal myself"

raise(signo) asks the kernel to deliver signal number signo to the calling process. For an ordinary single-threaded program it behaves exactly like kill(getpid(), signo). What happens next depends entirely on the disposition you learned about with the common signals:

Current disposition of signo Effect of raise(signo)
Default (terminating signal) Process terminates, possibly with a core dump
Default (SIGCHLD, SIGURG, …) Signal is ignored by default; nothing visible
A handler installed Your handler runs, then raise() returns
SIG_IGN Signal is discarded; raise() returns
Blocked (in the signal mask) Signal stays pending until unblocked

Because the signal is delivered synchronously to the caller, an unblocked, handled signal has already run its handler by the time raise() returns.

   raise(SIGUSR1)                kernel                  your code
   ------------------>  mark SIGUSR1 pending for me
                        deliver now (unblocked)
                        run handler on_usr1() -------->  handler executes
                        handler returns
   <------------------  raise() returns 0
   next line runs

raise() vs kill() vs the thread twist

In a multi-threaded program raise(signo) is not kill(getpid(), signo). POSIX defines raise() in a threaded process as pthread_kill(pthread_self(), signo) — it targets the calling thread specifically. kill(getpid(), signo) instead targets the process, and the kernel is free to deliver a process-directed signal to any one thread that has it unblocked. This matters when you have a dedicated signal-handling thread: use kill/pthread_kill deliberately, and remember raise() keeps the signal on the thread that called it.

Call Target Multi-thread meaning
raise(sig) Yourself The calling thread (pthread_kill(pthread_self(), sig))
kill(getpid(), sig) Your process Some thread with sig unblocked, chosen by the kernel
pthread_kill(tid, sig) One thread The exact thread tid

Knowledge check: In a program with three threads, thread B calls raise(SIGUSR1). Which thread's handler runs?

Thread B's. raise() inside a threaded process is pthread_kill(pthread_self(), SIGUSR1), so the signal is directed at the calling thread. If B has SIGUSR1 unblocked, B runs the handler. Had B instead called kill(getpid(), SIGUSR1), the kernel could have picked any of the three threads that had SIGUSR1 unblocked.

The cleanup-then-re-raise idiom

The most important production use of raise() is dying with the right cause. Suppose you catch SIGTERM to flush buffers and remove a lock file. After cleanup you should not just exit(0) — that hides the fact you were terminated. Instead:

  1. In the handler, do only async-signal-safe cleanup.
  2. Restore the default disposition: signal(signo, SIG_DFL);.
  3. raise(signo); — now the default (fatal) action runs, and your parent's WIFSIGNALED(status) is true with WTERMSIG(status) == signo.

This is the same mechanism abort() uses for SIGABRT. It ensures shells report 128 + signo and supervisors restart you for the right reason.

Signals you cannot raise-and-catch

raise(SIGKILL) and raise(SIGSTOP) do exactly what any other source of those signals does: SIGKILL terminates you unconditionally and SIGSTOP suspends you. They cannot be caught, blocked, or ignored, so installing a handler for them is silently useless and raise(SIGKILL) will end the process before the next line runs. Use catchable signals (SIGINT, SIGTERM, SIGUSR1, SIGABRT, …) when you actually want a handler to run.

Syntax notes

#include <signal.h>
int raise(int sig);
  • sig — the signal number to deliver to the caller (e.g. SIGUSR1, SIGTERM, SIGABRT). Use the named constants, never magic numbers.
  • Return value — 0 on success, non-zero on failure (an invalid sig). Note this differs from kill(), which returns -1 and sets errno. raise() rarely fails; the common failure is an out-of-range signal number.
  • Delivery timing — for an unblocked signal, delivery (and any handler) completes before raise() returns. For a blocked signal, raise() returns and the signal stays pending until unblocked.

Related signatures you build on:

int kill(pid_t pid, int sig);              /* kill(getpid(), sig) ~= raise(sig) single-threaded */
int pthread_kill(pthread_t thread, int sig);/* what raise() maps to in a threaded process       */
void (*signal(int sig, void (*handler)(int)))(int); /* quick disposition change; SIG_DFL / SIG_IGN */
int sigaction(int sig, const struct sigaction *act, struct sigaction *old); /* preferred handler setup */

Inside a handler you may call only async-signal-safe functions — write(), _exit(), signal(), raise() are safe; printf(), malloc(), free() are not. No memory needs freeing or closing for raise() itself; the caution is entirely about what runs inside the handler it triggers.

Lesson

What raise() does

raise(signo) sends the named signal to the program that calls it.

It is shorthand for kill(getpid(), signo). In a multi-threaded program, the signal goes to the calling thread, not the whole process.

Common uses

  • Re-raise a fatal signal after cleanup. This lets the parent process see the correct cause of death.
  • Trigger your own handler for testing. For example, fire your SIGINT handler on demand.
  • Report the right exit status to a parent that inspects the result with WIFSIGNALED.

Code examples

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

static volatile sig_atomic_t got_usr1 = 0;

/* Async-signal-safe handler: only set a flag and write() a fixed string. */
static void on_usr1(int signo) {
    (void)signo;
    got_usr1 = 1;
    const char msg[] = "  [handler] caught SIGUSR1 in the calling thread\n";
    write(STDOUT_FILENO, msg, sizeof msg - 1);
}

/* Demonstrates the cleanup-then-re-raise pattern. */
static void on_term(int signo) {
    const char msg[] = "  [child] cleaning up, then re-raising with default action\n";
    write(STDOUT_FILENO, msg, sizeof msg - 1);
    signal(signo, SIG_DFL);   /* restore default so the re-raise is fatal */
    raise(signo);             /* die the "right" way; parent sees SIGTERM  */
}

int main(void) {
    /* Part 1: raise() to fire our own handler on demand (for testing). */
    struct sigaction sa;
    memset(&sa, 0, sizeof sa);
    sa.sa_handler = on_usr1;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGUSR1, &sa, NULL);

    printf("parent: raising SIGUSR1 to test the handler\n");
    raise(SIGUSR1);                 /* == kill(getpid(), SIGUSR1) */
    printf("parent: back from raise, got_usr1 = %d\n", (int)got_usr1);

    /* Part 2: child uses cleanup-then-re-raise so the parent learns the cause. */
    pid_t pid = fork();
    if (pid == 0) {
        struct sigaction sc;
        memset(&sc, 0, sizeof sc);
        sc.sa_handler = on_term;
        sigemptyset(&sc.sa_mask);
        sigaction(SIGTERM, &sc, NULL);
        printf("child:  raising SIGTERM on myself\n");
        raise(SIGTERM);
        _exit(0);                   /* not reached */
    }

    int status;
    waitpid(pid, &status, 0);
    if (WIFSIGNALED(status))
        printf("parent: child died from signal %d (%s)\n",
               WTERMSIG(status), strsignal(WTERMSIG(status)));
    else
        printf("parent: child exited normally with %d\n", WEXITSTATUS(status));

    return 0;
}

Line by line

  • static volatile sig_atomic_t got_usr1 — the one variable a handler and main code share. volatile sig_atomic_t is the only type guaranteed safe to read/write across a handler boundary; ordinary int may be optimized or torn.
  • on_usr1() — the SIGUSR1 handler. It sets the flag and uses write() (async-signal-safe) instead of printf() (not safe). sizeof msg - 1 writes the string without the trailing NUL.
  • on_term() — the cleanup-then-re-raise handler. signal(signo, SIG_DFL) restores the default (fatal) disposition so the following raise(signo) actually terminates instead of re-entering the handler forever. raise(signo) then delivers SIGTERM with its default action — the child dies from the signal.
  • sigaction(SIGUSR1, &sa, NULL) — installs the handler. memset(&sa, 0, sizeof sa) zeroes the struct first (portable, clears flags), and sigemptyset(&sa.sa_mask) means no extra signals are blocked while the handler runs.
  • raise(SIGUSR1) in the parent — sends SIGUSR1 to itself. Because it is unblocked and handled, on_usr1 runs to completion before raise() returns, which is why the next line already sees got_usr1 = 1.
  • Note the print order: the handler's write() is unbuffered and appears immediately, while the parent's printf lines are stdio-buffered and flushed later — that is why the handler line can print before the "parent: raising" line.
  • fork() — makes a child that inherits the parent's handlers. The child installs its own SIGTERM handler, then raise(SIGTERM) triggers the cleanup-then-re-raise sequence.
  • waitpid + WIFSIGNALED/WTERMSIG — the parent inspects how the child ended. Because the child re-raised SIGTERM with the default disposition, WIFSIGNALED is true and WTERMSIG is 15 (SIGTERM) — the whole point of the idiom.
  • return 0 in the parent — the parent itself exits cleanly; only the child died by signal.

Common mistakes

1. Re-raising without restoring the default → infinite loop.

void handler(int s) { /* cleanup */ raise(s); }   /* WRONG: handler re-enters */

The handler is still installed, so raise(s) just calls the handler again (or the signal is blocked during the handler and stays pending, then re-fires). Fix:

void handler(int s) { /* cleanup */ signal(s, SIG_DFL); raise(s); }

2. Expecting to catch SIGKILL/SIGSTOP.

signal(SIGKILL, handler);
raise(SIGKILL);            /* WRONG: process dies, handler never runs */

SIGKILL and SIGSTOP cannot be caught, blocked, or ignored. Fix: choose a catchable signal.

signal(SIGTERM, handler);
raise(SIGTERM);            /* handler runs */

3. Doing unsafe work in the handler that raise() triggers.

void handler(int s){ printf("got %d\n", s); malloc(64); }  /* WRONG: not async-signal-safe */

printf/malloc can deadlock or corrupt state if the signal arrived mid-call. Fix: set a flag and/or write() a fixed buffer.

volatile sig_atomic_t flag;
void handler(int s){ (void)s; flag = 1; write(1, "got\n", 4); }

4. Assuming raise() reports errors like kill().

if (raise(sig) == -1) perror("raise");   /* WRONG: raise returns non-zero, not -1, and does not set errno reliably */

Fix: check for non-zero.

if (raise(sig) != 0) fprintf(stderr, "raise failed (bad signal %d)\n", sig);

Debugging tips

  • See the real cause of death. Run under a shell and print echo $?: a process killed by signal N reports 128 + N (SIGTERM = 15 → 143). This confirms your re-raise idiom worked.
  • strace/dtruss. On Linux strace -e trace=signal ./prog shows the rt_sigaction, kill/tgkill (raise maps here), and the delivered signal, e.g. --- SIGTERM {si_signo=SIGTERM, si_code=SI_TKILL} ---. On macOS use sudo dtruss.
  • gdb. Set handle SIGUSR1 stop print to break when the signal is delivered, then bt to see you are inside raise. Use info signals to view each signal's stop/print/pass settings.
  • Handler not firing? Check three things: is the disposition actually a handler (not SIG_IGN/SIG_DFL), is the signal blocked in your mask (sigprocmask), and did you install the handler before the raise().
  • printf ordering surprises. If handler output and normal output interleave oddly, it is stdio buffering. Call fflush(stdout) before raise(), or use write() in the handler (as the example does).

Memory safety

  • Only volatile sig_atomic_t crosses the handler boundary safely. A handler that raise() triggers can interrupt your main code between any two machine instructions; sharing an ordinary variable is a data race / undefined behaviour. Use volatile sig_atomic_t flags and read them in the main flow.
  • Async-signal-safety. Inside the handler call only async-signal-safe functions (write, _exit, raise, signal, sigaction). Calling printf, malloc, or touching a lock that main code may hold can deadlock or corrupt the heap — a classic reentrancy bug.
  • No leaks from raise() itself, but a re-raise that terminates the process skips your normal cleanup path: make sure critical flushes (or their async-signal-safe equivalents) happen before the fatal raise, since atexit handlers and stdio buffers are not flushed when you die by signal.
  • Threading. raise() targets the calling thread. If you rely on a single dedicated signal thread, a stray raise() on the wrong thread can run a handler where you did not expect it. Keep signal masks explicit (pthread_sigmask).
  • Infinite recursion. Forgetting signal(s, SIG_DFL) before re-raising can loop the handler and blow the stack; always restore the default disposition first.

Real-world uses

  • abort() is essentially raise(SIGABRT) after unblocking SIGABRT — used by assert, allocator corruption detectors, and __stack_chk_fail to dump core and stop immediately.
  • Graceful shutdown in daemons/CLIs. Catch SIGTERM/SIGINT, flush and release resources, then signal(sig, SIG_DFL); raise(sig); so systemd, Docker, and shells record the correct termination cause and $? is 128+sig.
  • Test harnesses. Unit tests fire raise(SIGUSR1)/raise(SIGSEGV) to exercise handlers deterministically without an external kill.
  • Self-restart / self-suspend tooling. Interactive tools re-raise SIGTSTP after saving terminal state so Ctrl-Z suspends correctly, then restore state on SIGCONT.
  • Best practice: prefer sigaction over signal for installing handlers (defined semantics, no reset-on-delivery surprises), keep handlers tiny, and use the cleanup-then-re-raise idiom rather than exit() for signal-caused termination.

Practice tasks

  1. Write a program that installs a SIGUSR1 handler which prints via write(), then calls raise(SIGUSR1). Confirm the handler line appears before the line after raise().

  2. Remove the handler from task 1 (leave SIGUSR1 at its default) and raise(SIGUSR1). Predict and then verify the exit status with echo $?.

  3. Implement the cleanup-then-re-raise idiom for SIGTERM: in the handler, write() a cleanup message, restore SIG_DFL, and raise(SIGTERM). In a parent that forks this child, use WIFSIGNALED/WTERMSIG to prove the child died from SIGTERM.

  4. Block SIGUSR1 with sigprocmask before calling raise(SIGUSR1), print a line, then unblock it. Show that the handler runs only after you unblock (the signal was pending).

  5. Write a two-thread program where one thread blocks all signals and the other calls raise(SIGUSR1); verify the handler runs on the raising thread only, demonstrating that raise() targets the calling thread rather than the whole process.

Summary

  • raise(signo) sends signo to the caller; single-threaded it equals kill(getpid(), signo), multi-threaded it equals pthread_kill(pthread_self(), signo) — the calling thread.
  • For an unblocked, handled signal, the handler runs and completes before raise() returns; a blocked signal stays pending.
  • raise() returns 0 on success and non-zero on failure — unlike kill(), it does not use the -1/errno convention.
  • The key idiom: catch a fatal signal, do async-signal-safe cleanup, signal(sig, SIG_DFL), then raise(sig) so the parent sees the true cause via WIFSIGNALED/WTERMSIG.
  • SIGKILL and SIGSTOP cannot be caught, blocked, or ignored, so raise()-ing them just kills or stops you.
  • Only touch volatile sig_atomic_t and async-signal-safe functions inside the handler; forgetting SIG_DFL before a re-raise causes infinite handler recursion.

Practice with these exercises