Loading learning content...
Signals aren't just received—they're sent. Every time you press Ctrl+C, run kill, or a parent process manages its children, signals are being transmitted. Understanding signal transmission mechanisms is essential for building process management tools, implementing graceful shutdown, and coordinating multi-process applications.
This page explores the complete landscape of signal sending: from the fundamental kill() system call to the data-carrying capabilities of sigqueue(), from command-line tools to process group broadcasts. By the end, you'll understand not just how to send signals, but when to use each mechanism.
By the end of this page, you will understand: the kill() and raise() system calls, killing process groups, signal permissions and security, sigqueue() for real-time signals with payloads, pthread_kill() for thread-directed signals, and practical command-line signal tools.
Despite its name, kill() doesn't always kill. It's the general-purpose signal sending function, capable of delivering any signal to any process you have permission to signal.
#include <signal.h>
int kill(pid_t pid, int sig);
Parameters:
pid: Target specification (see below for variations)sig: Signal number to send (0 for permission check only)Returns: 0 on success, -1 on error (sets errno)
The pid parameter has four distinct modes:
| pid value | Effect |
|---|---|
> 0 | Send to process with that exact PID |
0 | Send to all processes in caller's process group |
-1 | Send to all processes caller has permission to signal (except PID 1 and self) |
< -1 | Send to all processes in process group with ID -pid |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
#include <signal.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h>#include <errno.h> /* * Demonstrates various kill() targeting modes. */ void send_to_specific_process(pid_t target) { printf("Sending SIGTERM to PID %d\n", target); if (kill(target, SIGTERM) == -1) { switch (errno) { case ESRCH: printf(" Error: Process %d doesn't exist\n", target); break; case EPERM: printf(" Error: Permission denied\n"); break; default: perror(" kill"); } } else { printf(" Signal sent successfully\n"); }} void check_process_exists(pid_t target) { /* * Special use: signal 0 doesn't send anything, but performs * permission check. Useful to test if process exists and * you have permission to signal it. */ if (kill(target, 0) == 0) { printf("Process %d exists and is accessible\n", target); } else if (errno == ESRCH) { printf("Process %d doesn't exist\n", target); } else if (errno == EPERM) { printf("Process %d exists but not accessible\n", target); }} void signal_process_group(pid_t pgid) { printf("Sending SIGTERM to process group %d\n", pgid); /* Negative pid = process group ID (absolute value) */ if (kill(-pgid, SIGTERM) == -1) { perror(" kill"); } else { printf(" Signal sent to all processes in group\n"); }} int main() { pid_t child; /* Create a child process to demonstrate */ child = fork(); if (child == 0) { /* Child process: just sleep */ printf("Child PID %d started, waiting...\n", getpid()); while (1) sleep(1); exit(0); } sleep(1); /* Let child start */ /* Check if child exists */ check_process_exists(child); /* Send SIGTERM to child */ send_to_specific_process(child); /* Wait for child to terminate */ waitpid(child, NULL, 0); printf("Child terminated\n"); /* Check again - now it doesn't exist */ check_process_exists(child); return 0;}| errno | Cause | Solution |
|---|---|---|
ESRCH | Process doesn't exist | Handle gracefully—process may have exited |
EPERM | Permission denied | Check permissions (see next section) |
EINVAL | Invalid signal number | Use valid signal constants |
Sending signal 0 is a common idiom for checking if a process exists:
if (kill(pid, 0) == 0) {
/* Process exists and is accessible */
} else if (errno == ESRCH) {
/* Process doesn't exist */
} else if (errno == EPERM) {
/* Process exists but we can't signal it */
}
This is used in process monitoring, lock file validation, and daemon management scripts.
Notably, kill() is one of the functions you CAN safely call from a signal handler. This enables patterns like: catch SIGTERM, send SIGTERM to child processes, then exit. Signal-based chain reactions are valid.
Not every process can signal every other process. Permission checks protect system integrity by preventing unprivileged users from disrupting system processes or other users' applications.
For a process to send a signal to another process:
Rule 1: Root Can Signal Anyone
Processes with CAP_KILL capability (typically root) can send any signal to any process.
Rule 2: Same User
A process can signal another process if the sender's real or effective UID matches the target's real or saved-set UID.
/* Simplified permission check logic */
if (sender_euid == 0 || sender_has_cap_kill) {
/* Permitted - root or capability */
} else if (sender_ruid == target_ruid ||
sender_ruid == target_suid ||
sender_euid == target_ruid ||
sender_euid == target_suid) {
/* Permitted - same user */
} else {
errno = EPERM;
return -1;
}
Rule 3: SIGCONT Exception
A process can always send SIGCONT to another process in the same session, regardless of UID. This allows job control to work across setuid boundaries.
| Scenario | Permitted? | Why |
|---|---|---|
| User A signals User A's process | Yes | Same user |
| User A signals User B's process | No | Different user, no privilege |
| Root signals any process | Yes | Root has CAP_KILL |
| User A signals root's process | No | Cannot signal privileged user |
| Setuid program signals parent | Depends | Compares real/effective UIDs |
| Any process → SIGCONT in session | Yes | Session exception for job control |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
#include <signal.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <errno.h>#include <pwd.h> /* * Safely attempt to signal a process with proper error handling. */int safe_kill(pid_t pid, int sig, const char *purpose) { printf("Attempting to send signal %d to PID %d (%s)\n", sig, pid, purpose); if (kill(pid, sig) == 0) { printf(" Success!\n"); return 0; } switch (errno) { case EPERM: fprintf(stderr, " Permission denied.\n"); fprintf(stderr, " You can only signal processes you own,\n"); fprintf(stderr, " or you need root/CAP_KILL privilege.\n"); break; case ESRCH: fprintf(stderr, " Process %d not found.\n", pid); fprintf(stderr, " It may have already terminated.\n"); break; case EINVAL: fprintf(stderr, " Invalid signal number: %d\n", sig); break; default: perror(" kill"); } return -1;} int main(int argc, char *argv[]) { if (argc != 3) { fprintf(stderr, "Usage: %s <pid> <signal>\n", argv[0]); fprintf(stderr, "Example: %s 1234 15 (send SIGTERM to PID 1234)\n", argv[0]); return 1; } pid_t target = atoi(argv[1]); int signal = atoi(argv[2]); printf("Running as UID %d (EUID %d)\n", getuid(), geteuid()); /* First check if process exists and is accessible */ if (kill(target, 0) == -1) { if (errno == ESRCH) { fprintf(stderr, "Target process %d doesn't exist\n", target); return 1; } else if (errno == EPERM) { fprintf(stderr, "Target process %d exists but you can't access it\n", target); return 1; } } /* Now send the actual signal */ return safe_kill(target, signal, "user request") == 0 ? 0 : 1;}Never assume a signal will be delivered. The target may exit, you may lack permission, or the signal could be blocked. Always check kill() return value and handle errors. Critical systems should not rely solely on signals for coordination—use additional confirmation mechanisms.
Sometimes a process needs to signal itself. The raise() function provides a clean interface for self-signaling, while abort() provides a specific pattern for abnormal termination.
#include <signal.h>
int raise(int sig);
raise(sig) is equivalent to kill(getpid(), sig) in single-threaded programs, or pthread_kill(pthread_self(), sig) in multi-threaded programs. It sends the signal to the calling thread.
Common uses:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
#include <signal.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h> /* * Pattern: Custom handler that runs, then invokes default action. * Useful for cleanup before termination with proper exit status. */ volatile sig_atomic_t shutdown_started = 0; void sigterm_handler(int sig) { if (shutdown_started) { /* Already cleaning up - re-raise for default termination */ signal(SIGTERM, SIG_DFL); raise(SIGTERM); /* Won't return - default kills us */ return; } shutdown_started = 1; const char msg[] = "Caught SIGTERM, will exit after handler\n"; write(STDERR_FILENO, msg, sizeof(msg) - 1);} void cleanup() { printf("Cleaning up resources...\n"); sleep(1); printf("Cleanup complete.\n");} int main() { struct sigaction sa; sa.sa_handler = sigterm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGTERM, &sa, NULL); printf("PID %d - send SIGTERM to test\n", getpid()); while (!shutdown_started) { sleep(1); } cleanup(); /* * Re-raise with default handler to: * 1. Report correct exit status to parent (killed by SIGTERM) * 2. Allow wait() to see proper signal death */ printf("Re-raising SIGTERM with default handler...\n"); signal(SIGTERM, SIG_DFL); raise(SIGTERM); /* Never reached */ return 0;}#include <stdlib.h>
void abort(void);
abort() sends SIGABRT to the calling process. It's used for unrecoverable error conditions where you want:
Key behaviors:
| Scenario | Use |
|---|---|
| Testing handlers | raise(SIGTERM) |
| Intentional pause | raise(SIGSTOP) |
| Unrecoverable error | abort() |
| Failed assertion | assert() (calls abort internally) |
| Clean exit after signal cleanup | raise(original_signal) with SIG_DFL |
If you catch SIGTERM for cleanup, re-raise it with default handler afterward. Parent processes (and shell scripts) expect specific exit codes: exit status 128+N means 'killed by signal N'. Using exit(0) after catching SIGTERM confuses process monitors that expect termination-by-signal semantics.
Signals can target entire process groups—collections of related processes. This is fundamental to job control and is how Ctrl+C stops an entire pipeline.
A process group is a collection of processes with the same process group ID (PGID). Key points:
cat file | grep pattern | wc -l#include <signal.h>
int killpg(pid_t pgrp, int sig);
Equivalent to kill(-pgrp, sig) but more readable.
Use cases:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
#include <signal.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h> /* * Demonstrates process group signaling. * Parent creates children in a new process group, then kills the group. */ void child_handler(int sig) { const char msg[] = "Child received SIGTERM\n"; write(STDERR_FILENO, msg, sizeof(msg) - 1); _exit(0);} void spawn_children_in_group(pid_t pgrp, int count) { for (int i = 0; i < count; i++) { pid_t pid = fork(); if (pid == 0) { /* Child process */ setpgid(0, pgrp); /* Join the process group */ struct sigaction sa; sa.sa_handler = child_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGTERM, &sa, NULL); printf("Child %d (PID %d, PGID %d) ready\n", i, getpid(), getpgrp()); while (1) pause(); exit(0); } /* Parent: also set child's group (race avoidance) */ setpgid(pid, pgrp); }} int main() { pid_t pgrp; int child_count = 3; /* Create first child - becomes process group leader */ pid_t leader = fork(); if (leader == 0) { setpgid(0, 0); /* Create new process group with self as leader */ struct sigaction sa; sa.sa_handler = child_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGTERM, &sa, NULL); printf("Group leader (PID %d, PGID %d) ready\n", getpid(), getpgrp()); while (1) pause(); exit(0); } setpgid(leader, leader); /* Parent's view: set leader's PGID */ pgrp = leader; /* Create more children in the same group */ spawn_children_in_group(pgrp, child_count - 1); sleep(1); /* Let children start */ printf("\nParent: Sending SIGTERM to process group %d\n", pgrp); if (killpg(pgrp, SIGTERM) == -1) { perror("killpg"); exit(1); } /* Wait for all children */ for (int i = 0; i < child_count; i++) { wait(NULL); } printf("All children terminated\n"); return 0;}When you press Ctrl+C in a terminal, it sends SIGINT to the entire foreground process group. This is how shell job control works:
$ cat file | sort | uniq # All three in same process group
^C # SIGINT to all three
The shell manages process groups:
Session: A collection of process groups, typically one per login. The session leader is usually the login shell.
SIGHUP propagation on terminal close:
disown command)nohupsetsid())Daemons call setsid() to create a new session, becoming immune to terminal hangups and job control. This is part of the standard daemonization process: fork, setsid, fork again, redirect stdio to /dev/null.
Standard signals carry no data—they only indicate that an event occurred. For cases requiring data transmission along with the notification, POSIX provides sigqueue() with real-time signals.
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
Parameters:
pid: Target process ID (must be specific, not group/broadcast)sig: Signal number (typically SIGRTMIN to SIGRTMAX)value: Payload data (integer or pointer)union sigval {
int sival_int; /* Integer value to pass */
void *sival_ptr; /* Pointer value to pass (same address space only!) */
};
Real-time signals (SIGRTMIN through SIGRTMAX) have special properties:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
#include <signal.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <string.h> /* * Server process: receives real-time signals with data. */ void rt_signal_handler(int sig, siginfo_t *info, void *context) { /* * With SA_SIGINFO, we get siginfo_t containing: * - si_signo: signal number * - si_code: SI_QUEUE if from sigqueue() * - si_pid: sender's PID * - si_uid: sender's UID * - si_value: the payload we sent */ printf("\n=== Real-time Signal Received ===\n"); printf("Signal: %d (SIGRTMIN+%d)\n", sig, sig - SIGRTMIN); printf("Sender PID: %d\n", info->si_pid); printf("Sender UID: %d\n", info->si_uid); if (info->si_code == SI_QUEUE) { printf("Payload (integer): %d\n", info->si_value.sival_int); printf("Source: sigqueue()\n"); } else { printf("Source: kill() or raise() (no payload)\n"); } printf("================================\n\n");} void run_server() { struct sigaction sa; /* Install handler for first few real-time signals */ memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = rt_signal_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_SIGINFO; /* Required for si_value access */ for (int i = 0; i < 5; i++) { sigaction(SIGRTMIN + i, &sa, NULL); } printf("Server running (PID %d)\n", getpid()); printf("Waiting for real-time signals SIGRTMIN through SIGRTMIN+4...\n"); printf("Use: ./client %d <message_id>\n", getpid()); while (1) { pause(); /* Wait for signals */ }} /* * Client function: sends real-time signal with data. */void run_client(pid_t server_pid, int message_id) { union sigval value; value.sival_int = message_id; printf("Sending SIGRTMIN with payload %d to PID %d\n", message_id, server_pid); if (sigqueue(server_pid, SIGRTMIN, value) == -1) { perror("sigqueue"); exit(1); } printf("Signal sent successfully\n");} int main(int argc, char *argv[]) { if (argc == 1) { /* No arguments: run as server */ run_server(); } else if (argc == 3) { /* Two arguments: run as client */ pid_t server_pid = atoi(argv[1]); int message_id = atoi(argv[2]); run_client(server_pid, message_id); } else { fprintf(stderr, "Usage:\n"); fprintf(stderr, " Server: %s\n", argv[0]); fprintf(stderr, " Client: %s <server_pid> <message_id>\n", argv[0]); return 1; } return 0;}Good use cases:
Limitations:
If you send faster than the receiver processes, the queue may fill:
if (sigqueue(pid, sig, val) == -1 && errno == EAGAIN) {
/* Queue full - signal pending limit reached */
/* Options: retry later, drop, use different IPC */
}
The system limit is typically in /proc/sys/kernel/rtsig-max on Linux.
sigqueue() is most useful for lightweight notifications with small payloads. For transferring larger data, use sigqueue() as a notification mechanism alongside shared memory or pipes for the actual data: 'New data in slot 5' rather than embedding the data in the signal.
In multi-threaded programs, signals can be directed to specific threads using pthread_kill(). This enables thread-level control and notification.
#include <signal.h>
int pthread_kill(pthread_t thread, int sig);
Parameters:
thread: Thread ID (from pthread_create or pthread_self)sig: Signal number (0 to check thread existence)Returns: 0 on success, error code (not -1!) on failure
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
#include <pthread.h>#include <signal.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <string.h> /* Thread-local variable to identify which thread */static __thread int thread_id = -1; void sigusr1_handler(int sig) { /* Note: printf not safe, but for demo purposes */ printf("Thread %d received SIGUSR1\n", thread_id);} void *worker_thread(void *arg) { thread_id = *(int *)arg; printf("Worker %d started (pthread_t: %lu)\n", thread_id, pthread_self()); /* Worker loop */ for (int i = 0; i < 10; i++) { sleep(1); printf("Worker %d working...\n", thread_id); } return NULL;} int main() { pthread_t threads[3]; int ids[3] = {1, 2, 3}; /* Install signal handler (shared by all threads) */ struct sigaction sa; sa.sa_handler = sigusr1_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGUSR1, &sa, NULL); thread_id = 0; /* Main thread ID */ /* Create worker threads */ for (int i = 0; i < 3; i++) { pthread_create(&threads[i], NULL, worker_thread, &ids[i]); } sleep(2); /* Let workers start */ /* Send signal to specific threads */ printf("\nMain: Sending SIGUSR1 to thread 2 only\n"); int err = pthread_kill(threads[1], SIGUSR1); if (err != 0) { fprintf(stderr, "pthread_kill: %s\n", strerror(err)); } sleep(1); printf("\nMain: Sending SIGUSR1 to all threads\n"); for (int i = 0; i < 3; i++) { pthread_kill(threads[i], SIGUSR1); } /* Wait for threads to finish */ for (int i = 0; i < 3; i++) { pthread_join(threads[i], NULL); } return 0;}A common robust pattern for handling signals in multi-threaded programs:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
#include <pthread.h>#include <signal.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h> volatile sig_atomic_t shutdown_requested = 0; void *signal_thread(void *arg) { sigset_t *mask = (sigset_t *)arg; int sig; printf("Signal thread running\n"); while (1) { /* Synchronously wait for signals */ int err = sigwait(mask, &sig); if (err != 0) { perror("sigwait"); continue; } printf("Signal thread received signal %d\n", sig); switch (sig) { case SIGINT: case SIGTERM: printf("Shutdown requested\n"); shutdown_requested = 1; return NULL; case SIGHUP: printf("Config reload requested\n"); /* Handle config reload safely */ break; } } return NULL;} void *worker_thread(void *arg) { int id = *(int *)arg; printf("Worker %d started\n", id); while (!shutdown_requested) { sleep(1); printf("Worker %d working\n", id); } printf("Worker %d shutting down\n", id); return NULL;} int main() { pthread_t sig_thread, worker; sigset_t mask; int worker_id = 1; /* Block SIGINT, SIGTERM, SIGHUP in main thread */ sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGTERM); sigaddset(&mask, SIGHUP); pthread_sigmask(SIG_BLOCK, &mask, NULL); /* pthread version */ /* Create signal handler thread */ pthread_create(&sig_thread, NULL, signal_thread, &mask); /* Create worker threads (inherit blocked mask) */ pthread_create(&worker, NULL, worker_thread, &worker_id); printf("Main: PID %d, send SIGTERM to test\n", getpid()); /* Wait for threads */ pthread_join(sig_thread, NULL); pthread_cancel(worker); /* Force worker shutdown */ pthread_join(worker, NULL); printf("Shutdown complete\n"); return 0;}The dedicated signal thread pattern eliminates async-signal-safety concerns entirely. Signals are received synchronously via sigwait()—no handler runs, no async interruption. You can use any function, any library, any complex logic in response to signals. This is the gold standard for multi-threaded servers.
While you'll often send signals programmatically, command-line tools are essential for system administration, debugging, and scripting.
kill [-signal] pid...
kill -l # List all signals
kill -l signum # Name for signal number
Examples:
kill 1234 # Send SIGTERM (default)
kill -TERM 1234 # Explicit SIGTERM
kill -9 1234 # SIGKILL (forced)
kill -HUP 1234 # SIGHUP (reload config)
kill -0 1234 # Check if process exists
kill -STOP 1234 # Suspend process
kill -CONT 1234 # Resume process
kill -- -1234 # Signal process group 1234
killall [-signal] name
Examples:
killall nginx # SIGTERM to all nginx processes
killall -9 python # SIGKILL all python processes
killall -HUP sshd # Reload sshd config
killall -w nginx # Wait for processes to die
⚠️ Warning: On Solaris, killall kills ALL processes. Use pkill for portable name-based killing.
pgrep [options] pattern # Find PIDs matching pattern
pkill [options] pattern # Signal processes matching pattern
Examples:
pgrep -u root sshd # Find sshd owned by root
pgrep -f 'python.*app' # Match full command line
pkill -HUP -u www nginx # HUP to nginx owned by www
pkill -KILL -t pts/1 # Kill processes on terminal
| Tool | Target By | Signal Syntax | Notes |
|---|---|---|---|
kill | PID | -SIG or -N | POSIX standard, always available |
killall | Process name | -SIG | Not portable (different on Solaris) |
pkill | Pattern/regex | -SIG | Flexible matching, recommended |
xkill | Window click | SIGKILL | X11 only, click to kill GUI app |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
#!/bin/bash## graceful_shutdown.sh - Proper SIGTERM → SIGKILL pattern# PIDFILE="/var/run/myapp.pid"TIMEOUT=30 shutdown_app() { if [[ ! -f "$PIDFILE" ]]; then echo "PID file not found" return 1 fi local pid=$(cat "$PIDFILE") # Check if process exists if ! kill -0 "$pid" 2>/dev/null; then echo "Process $pid not running" rm -f "$PIDFILE" return 0 fi echo "Sending SIGTERM to $pid..." kill -TERM "$pid" # Wait for graceful shutdown local count=0 while kill -0 "$pid" 2>/dev/null; do if [[ $count -ge $TIMEOUT ]]; then echo "Timeout! Sending SIGKILL..." kill -KILL "$pid" break fi sleep 1 ((count++)) done # Final check if kill -0 "$pid" 2>/dev/null; then echo "ERROR: Process still running!" return 1 fi echo "Process terminated" rm -f "$PIDFILE" return 0} shutdown_appAlways use kill -0 to check process existence before and after sending signals. Always use the SIGTERM → wait → SIGKILL pattern for clean shutdown. Store PIDs in pid files for easy management. Use trap in scripts to handle signals sent to the script itself.
Signal sending is as important as signal handling. You now have the complete toolkit for process coordination via signals. Let's consolidate the key points:
kill(pid, 0) tests if process exists and is accessible without sending a signal.What's Next:
The final page in this module addresses signal reliability—the historical issues, remaining edge cases, and modern solutions. Understanding reliability completes your mastery of the signal mechanism.
You now understand the complete signal sending landscape—from basic kill() to advanced sigqueue(), from single processes to process groups to individual threads. Combined with handler knowledge from the previous page, you can build sophisticated signal-based coordination systems.