Threads and Signals

 

← Thread Stacks Section 33.2 · Threads & Signals Next: Process Control →

Chapter 33 · Section 33.2

Threads and Signals

Signal masks, pthread_kill, sigwait, and the recommended pattern for async signals

Per-threadSignal mask

Process-wideSignal disposition

sigwait()Recommended pattern

Key Terms

Signal Disposition Signal Mask (per-thread) pthread_sigmask pthread_kill pthread_sigqueue sigwait sigwaitinfo Process-directed signal Thread-directed signal Async-signal-safe Alternate signal stack

Why Signals and Threads Are a Difficult Combination

The Unix signal model was designed in the 1970s for single-threaded processes. Pthreads came much later. When they meet, the result is complex and full of traps:

  • Signal handlers are process-wide but threads are independent execution contexts.
  • None of the Pthreads API functions (pthread_mutex_lock, pthread_cond_signal, etc.) are async-signal-safe — you cannot call them from a signal handler.
  • A signal sent to the process can be delivered to any thread — you can’t predict which one.
  • Each thread has its own signal mask but all threads share signal dispositions.
Rule of thumb: Avoid mixing signals and threads whenever possible. When you must, use the sigwait() pattern described in Section 33.2.4.

What Is Process-Wide vs Per-Thread?

Signal Model: Process-Wide vs Per-Thread Attributes

PROCESS-WIDE (shared by all threads)
  • Signal dispositions (SIG_DFL, SIG_IGN, handler)
  • Signal actions (stop/terminate whole process)
  • Set of pending signals for the process

PER-THREAD (each thread independent)
  • Signal mask (which signals are blocked)
  • Set of pending signals for that thread
  • Alternate signal stack (sigaltstack)

Key implications:

  • If one thread calls sigaction(SIGINT, ...) to set a handler, that handler is used for all threads.
  • If one thread sets SIGPIPE to SIG_IGN, all threads ignore SIGPIPE.
  • But if one thread blocks SIGTERM in its mask, only that thread is blocking it — other threads can still receive it.
  • A newly created thread inherits a copy of its creator’s signal mask.

Process-Directed vs Thread-Directed Signals

When a signal arrives, the kernel needs to decide which thread gets it.

Thread-Directed Signal

Delivered to a specific thread. Caused by:

  • Hardware exception in that thread’s context: SIGSEGV, SIGFPE, SIGILL, SIGBUS
  • SIGPIPE when that thread writes to a broken pipe
  • Explicitly sent via pthread_kill() or pthread_sigqueue()

Process-Directed Signal

The kernel picks any thread that is not blocking the signal. Examples:

  • Signals sent via kill() from another process
  • Terminal signals: SIGINT, SIGQUIT, SIGTSTP
  • Timer signals: SIGALRM
  • Window resize: SIGWINCH
When a process-directed signal arrives, the kernel picks one — and only one — arbitrary thread that is not blocking the signal to invoke the handler. This preserves the traditional Unix semantic that a signal is handled once per delivery.

Manipulating Thread Signal Masks: pthread_sigmask()

pthread_sigmask() works identically to sigprocmask() but operates on the calling thread’s signal mask, not the whole process.

#include <signal.h>

int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
/* Returns 0 on success, positive error number on error */

/* 'how' values:
 *   SIG_BLOCK   — add signals in 'set' to the mask (block more)
 *   SIG_UNBLOCK — remove signals in 'set' from the mask (unblock)
 *   SIG_SETMASK — replace the mask entirely with 'set'
 */
sigprocmask() vs pthread_sigmask(): Calling sigprocmask() in a multithreaded program has unspecified behavior per SUSv3. Always use pthread_sigmask() in threaded code, even though on Linux they happen to be identical internally.

Sending a Signal to a Specific Thread: pthread_kill()

#include <signal.h>

int pthread_kill(pthread_t thread, int sig);
/* Returns 0 on success, positive error number on error */

/* NOTE: thread must be in the SAME process.
 * pthread_kill() is implemented via the Linux-specific tgkill() syscall,
 * which sends 'sig' to a specific thread within a thread group.
 * Use sig=0 to check if a thread exists (no signal sent, just error check).
 */

Also available: pthread_sigqueue(thread, sig, value) — sends a signal with accompanying data (combines pthread_kill + sigqueue). Added in glibc 2.11, requires Linux 2.6.31+.

The Recommended Pattern: sigwait()

The safest way to handle async signals in a multithreaded program is to not use signal handlers at all. Instead:

1
Block all async signals in the main thread before creating any other threads (using pthread_sigmask()). All newly created threads will inherit this mask.
2
Create a single dedicated signal-handling thread whose job is to call sigwait() in a loop, accepting signals one at a time.
3
In the signal thread: safely call mutex-protected code, non-async-signal-safe functions, send to condition variables — because you are in a normal thread context, not a signal handler.
#include <signal.h>

int sigwait(const sigset_t *set, int *sig);
/* Waits for one of the signals in 'set' to be pending for this thread.
 * Accepts the signal (removes it from pending set).
 * Stores the signal number in *sig.
 * Returns 0 on success, positive error number on error.
 *
 * Difference from sigwaitinfo():
 *   sigwait() → returns just signal number
 *   sigwaitinfo() → returns full siginfo_t structure
 */

Code Example 1 — pthread_sigmask and pthread_kill

This example shows a main thread blocking SIGUSR1, creating a worker thread, and then using pthread_kill() to send SIGUSR1 to the worker. The worker has SIGUSR1 unblocked and handles it.

/*
 * ep_thread_signal.c — pthread_sigmask + pthread_kill demo
 * Compile: gcc -o ep_thread_signal ep_thread_signal.c -lpthread
 */
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

/* Signal handler — invoked in the worker thread */
static void sigusr1_handler(int sig)
{
    /* Only async-signal-safe functions here!
     * write() is safe; printf() is NOT guaranteed safe. */
    const char *msg = "  [Worker] SIGUSR1 handler fired!\n";
    write(STDOUT_FILENO, msg, strlen(msg));
}

void *worker_thread(void *arg)
{
    sigset_t unblock;
    struct sigaction sa;

    /* Step 1: Install a handler for SIGUSR1 */
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = sigusr1_handler;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGUSR1, &sa, NULL);

    /* Step 2: Unblock SIGUSR1 in THIS thread */
    sigemptyset(&unblock);
    sigaddset(&unblock, SIGUSR1);
    pthread_sigmask(SIG_UNBLOCK, &unblock, NULL);

    printf("[Worker %lu] Waiting for signals (sleeping 5s)...\n",
           (unsigned long)pthread_self());
    sleep(5);
    printf("[Worker %lu] Finished waiting\n",
           (unsigned long)pthread_self());
    return NULL;
}

int main(void)
{
    pthread_t worker;
    sigset_t block_all;

    /* Block SIGUSR1 in the main thread before creating workers.
     * New threads inherit this mask. */
    sigemptyset(&block_all);
    sigaddset(&block_all, SIGUSR1);
    pthread_sigmask(SIG_BLOCK, &block_all, NULL);
    printf("[Main] SIGUSR1 blocked in main thread\n");

    pthread_create(&worker, NULL, worker_thread, NULL);

    /* Give worker time to unblock and set up handler */
    sleep(1);

    /* Send SIGUSR1 directly to the worker thread */
    printf("[Main] Sending SIGUSR1 to worker thread...\n");
    int ret = pthread_kill(worker, SIGUSR1);
    if (ret != 0)
        fprintf(stderr, "[Main] pthread_kill failed: %s\n", strerror(ret));

    pthread_join(worker, NULL);
    printf("[Main] Done.\n");
    return 0;
}

Expected output:

[Main] SIGUSR1 blocked in main thread
[Worker 140...] Waiting for signals (sleeping 5s)...
[Main] Sending SIGUSR1 to worker thread...
  [Worker] SIGUSR1 handler fired!
[Worker 140...] Finished waiting
[Main] Done.

Code Example 2 — The sigwait() Pattern for Safe Async Signal Handling

This is the recommended production pattern. All threads block all signals. A dedicated signal-handling thread calls sigwait() in a loop and handles each signal safely.

/*
 * ep_sigwait_pattern.c — The correct way to handle signals in multithreaded code
 * Compile: gcc -o ep_sigwait_pattern ep_sigwait_pattern.c -lpthread
 * Test:    ./ep_sigwait_pattern &
 *          kill -SIGINT  %1     # graceful shutdown
 *          kill -SIGUSR1 %1     # custom action
 *          kill -SIGTERM %1     # stop the process
 */
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

/* Shared flag for graceful shutdown */
static volatile int running = 1;
static pthread_mutex_t run_mutex = PTHREAD_MUTEX_INITIALIZER;

/* ---- Dedicated signal-handling thread ---- */
void *signal_handler_thread(void *arg)
{
    sigset_t *waitset = (sigset_t *)arg;
    int sig;

    printf("[SigThread] Started. Waiting for signals...\n");

    for (;;) {
        /* sigwait() atomically:
         *  1. Checks if a signal in 'waitset' is pending
         *  2. If yes, removes it from pending and returns it in 'sig'
         *  3. If no,  blocks until one arrives
         */
        int ret = sigwait(waitset, &sig);
        if (ret != 0) {
            fprintf(stderr, "[SigThread] sigwait error: %s\n", strerror(ret));
            continue;
        }

        printf("[SigThread] Received signal %d (%s)\n", sig, strsignal(sig));

        switch (sig) {
        case SIGINT:
        case SIGTERM:
            printf("[SigThread] Shutdown requested. Signalling workers...\n");
            /* Safe to use mutex here — we are in a normal thread */
            pthread_mutex_lock(&run_mutex);
            running = 0;
            pthread_mutex_unlock(&run_mutex);
            return NULL;  /* exit signal handler thread */

        case SIGUSR1:
            printf("[SigThread] SIGUSR1: performing custom action\n");
            /* Could signal a condition variable, update a counter, etc. */
            break;

        default:
            printf("[SigThread] Unhandled signal %d\n", sig);
            break;
        }
    }
}

/* ---- Worker thread (does real work) ---- */
void *worker_thread(void *arg)
{
    int id = *(int *)arg;
    printf("[Worker %d] Started\n", id);
    while (1) {
        pthread_mutex_lock(&run_mutex);
        int keep_running = running;
        pthread_mutex_unlock(&run_mutex);
        if (!keep_running) break;

        printf("[Worker %d] Working...\n", id);
        sleep(2);
    }
    printf("[Worker %d] Exiting cleanly\n", id);
    return NULL;
}

int main(void)
{
    pthread_t sig_tid, worker_tids[3];
    sigset_t block_all;
    int worker_ids[3] = {1, 2, 3};

    /* Step 1: Block ALL signals in main thread BEFORE creating any threads.
     * All threads will inherit this mask. */
    sigfillset(&block_all);
    pthread_sigmask(SIG_BLOCK, &block_all, NULL);
    printf("[Main] All signals blocked globally\n");

    /* Step 2: Create the dedicated signal-handling thread.
     * It will call sigwait() to synchronously receive signals. */
    sigset_t waitset;
    sigemptyset(&waitset);
    sigaddset(&waitset, SIGINT);
    sigaddset(&waitset, SIGTERM);
    sigaddset(&waitset, SIGUSR1);
    pthread_create(&sig_tid, NULL, signal_handler_thread, &waitset);

    /* Step 3: Create worker threads */
    for (int i = 0; i < 3; i++)
        pthread_create(&worker_tids[i], NULL, worker_thread, &worker_ids[i]);

    printf("[Main] All threads running. Send SIGINT or SIGTERM to stop.\n");

    /* Wait for signal handler thread to exit (triggered by SIGINT/SIGTERM) */
    pthread_join(sig_tid, NULL);

    /* Wait for workers to finish */
    for (int i = 0; i < 3; i++)
        pthread_join(worker_tids[i], NULL);

    printf("[Main] Clean shutdown complete.\n");
    return 0;
}
Why this pattern is safe: The signal-handling thread runs in normal thread context, not inside a signal handler. So it can safely call printf(), pthread_mutex_lock(), pthread_cond_signal(), or any other function — none of these are async-signal-safe but all are perfectly fine to call from a thread.

Alternate Signal Stacks Are Per-Thread

Each thread has its own alternate signal stack (set via sigaltstack()). A newly created thread starts with no alternate signal stack defined — it does not inherit the creator’s stack.

Historical note: LinuxThreads incorrectly made new threads inherit the creator’s alternate signal stack. This meant two threads could share the same alternate signal stack, leading to crashes if both handled signals simultaneously. NPTL (kernels < 2.6.16) had the same bug. It was fixed in Linux 2.6.16.

Other per-thread signal behaviours:

  • sigpending() returns the union of process-pending signals AND the calling thread’s pending signals.
  • A signal handler interrupted by a call to pthread_mutex_lock() — the lock call is automatically restarted.
  • A signal handler interrupted by pthread_cond_wait() — the wait either restarts automatically (Linux) or returns 0 as a spurious wakeup.

Interview Questions

Q1. What aspects of the signal model are process-wide and what aspects are per-thread?
Process-wide: Signal dispositions (handlers/SIG_IGN/SIG_DFL), signal actions (stop/terminate). All threads share these — if one thread changes the SIGINT handler, all threads use the new handler.

Per-thread: Signal mask (which signals are blocked), the per-thread pending signal set, and the alternate signal stack. A newly created thread inherits a copy of its creator’s signal mask, but after that they are independent.

Q2. What is the difference between a process-directed signal and a thread-directed signal?
A process-directed signal (e.g., sent via kill(), or generated by Ctrl+C) is delivered to the process as a whole — the kernel picks any thread that is not blocking the signal. A thread-directed signal targets a specific thread: hardware exceptions (SIGSEGV, SIGFPE, etc. in that thread’s context), SIGPIPE when that thread writes to a broken pipe, or signals sent explicitly via pthread_kill() or pthread_sigqueue().
Q3. Why should you not call pthread_mutex_lock() from a signal handler?
Pthreads functions are not async-signal-safe. A signal handler can interrupt any thread at any point — including a point where that thread already holds the mutex. If the signal handler then tries to acquire the same mutex, the program will deadlock. Even if a different mutex is used, the library’s internal state may be in an inconsistent state. The solution is to use the sigwait() pattern, which handles signals in a normal thread context where mutex operations are safe.
Q4. Explain the sigwait() pattern for multithreaded signal handling.
The recommended pattern is: (1) Block all async signals in the main thread before creating any other threads, so all threads inherit the block. (2) Create one dedicated signal-handling thread. (3) In that thread, call sigwait() in a loop — it synchronously waits for a signal from the blocked set, removes it from the pending queue, and returns the signal number. Because the signal is received in a normal thread context (not a signal handler), you can safely call any function — mutexes, condition variables, printf(), etc.
Q5. How does pthread_kill() differ from the regular kill() syscall?
kill(pid, sig) sends a process-directed signal to an entire process (or process group). pthread_kill(thread, sig) sends a signal to a specific thread within the same process. Because thread IDs are only unique within a process, you cannot use pthread_kill() to signal threads in other processes. Internally, NPTL implements pthread_kill() via the Linux-specific tgkill(tgid, tid, sig) syscall.
Q6. What happens if two threads both call sigwait() waiting for the same signal?
When the signal arrives, only one of the waiting threads will accept it — which one is indeterminate (implementation-defined). The other thread remains blocked in sigwait(). This is typically fine if both threads would handle the signal identically. For scenarios where exactly one specific thread must handle a signal, use pthread_kill() to target it directly, or design the system so only one thread calls sigwait() for that signal.
Q7. A new thread is created. What signal mask does it start with?
A new thread inherits a copy of the signal mask of the thread that called pthread_create(). So if the creating thread had SIGINT blocked, the new thread will also start with SIGINT blocked. However, this is just the starting point — the new thread can immediately change its own mask using pthread_sigmask() without affecting any other thread.

Section Summary

  • Signal dispositions are process-wide; signal masks are per-thread.
  • Use pthread_sigmask() (not sigprocmask) to manipulate thread signal masks.
  • Use pthread_kill() to send a signal to a specific thread in the same process.
  • No Pthreads functions are async-signal-safe — never call them from signal handlers.
  • The sigwait() pattern is the recommended approach: block all signals, dedicate one thread to call sigwait() and handle signals synchronously.

Keep Learning — It’s Free

EmbeddedPathashala — free Linux and embedded systems education.

Visit EmbeddedPathashala

Leave a Reply

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