Chapter 33 · Section 33.2
Threads and Signals
Signal masks, pthread_kill, sigwait, and the recommended pattern for async signals
Key Terms
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.
What Is Process-Wide vs Per-Thread?
Signal Model: Process-Wide vs Per-Thread Attributes
- Signal dispositions (SIG_DFL, SIG_IGN, handler)
- Signal actions (stop/terminate whole process)
- Set of pending signals for the process
- 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()orpthread_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
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() 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:
pthread_sigmask()). All newly created threads will inherit this mask.sigwait() in a loop, accepting signals one at a time.#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;
}
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.
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
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.
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().sigwait() pattern, which handles signals in a normal thread context where mutex operations are safe.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.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.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.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.
