Chapter 64: Sending Signals: kill()
and raise()
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the fundamental role of signals as an Inter-Process Communication (IPC) mechanism in Linux.
- Explain the operational differences and use cases for the
kill()
andraise()
system calls. - Implement C programs that programmatically send signals to other processes using their Process IDs (PIDs).
- Configure and write applications that manage process groups and send signals to entire groups of related processes.
- Debug common permission errors and logical flaws in signal-sending applications.
- Apply signal-sending techniques to control and manage multi-process applications on an embedded Raspberry Pi 5 system.
Introduction
In the intricate dance of a modern Linux system, processes rarely operate in isolation. They must communicate, coordinate, and control one another to perform complex tasks. While previous chapters may have explored more structured forms of Inter-Process Communication (IPC) like pipes or shared memory, this chapter delves into one of the oldest and most fundamental mechanisms: signals. Signals are the system’s lightweight, asynchronous notifications, serving as the software equivalent of a hardware interrupt. They report exceptional events, from user actions like pressing Ctrl+C
to catastrophic errors like an illegal memory access.
Understanding how to programmatically send signals is not merely an academic exercise; it is a critical skill for any embedded Linux developer. In an embedded environment, robust process management is paramount. Imagine a system with a central management process responsible for monitoring the health of various peripheral drivers or application services. If a service becomes unresponsive, the manager must be able to terminate it cleanly and perhaps restart it. This is often accomplished by sending signals. From shutting down a data logging service gracefully with SIGTERM
to forcing a misbehaving driver to stop with SIGKILL
, the ability to send signals gives your applications the power to act as supervisors, ensuring system stability and reliability. This chapter will equip you with the knowledge to wield this power effectively using the standard kill()
and raise()
system calls on your Raspberry Pi 5.
Technical Background
The Nature of Signals in UNIX and Linux
To truly appreciate the function of kill()
and raise()
, one must first understand the nature of signals themselves. Signals are a form of asynchronous Inter-Process Communication (IPC) in UNIX-like operating systems, including Linux. Their history dates back to the earliest days of UNIX research at Bell Labs. They were designed as a simple, efficient way for the kernel to notify a process that a specific event has occurred. This event could be a hardware exception, such as a division-by-zero error, or a software-initiated event, such as a user pressing an interrupt key or one process needing to communicate with another.
A signal is often described as a “software interrupt” because it alters the receiving process’s flow of execution. When a signal is delivered to a process, the kernel interrupts the process’s normal execution. The process must then handle the signal. For every signal, a process can take one of three possible actions:
- Default Action: Every signal has a default action associated with it. For many signals, like
SIGSEGV
(segmentation fault), the default action is to terminate the process. For others, likeSIGCHLD
(a child process has terminated), the default action is to be ignored. - Catch the Signal: A process can register a custom function, known as a signal handler, to be executed when the signal is delivered. This allows the application to perform custom logic, such as cleaning up resources before exiting.
- Ignore the Signal: A process can explicitly choose to ignore a signal.
However, not all signals can be caught or ignored. The SIGKILL
and SIGSTOP
signals are special; they are handled directly by the kernel and cannot be intercepted by the process. This provides a guaranteed mechanism for the system administrator (and the kernel) to terminate or stop any non-essential process.
Signals are identified by small, positive integers, but in programming, we always refer to them by their symbolic names defined in the <signal.h>
header, such as SIGHUP
, SIGINT
, SIGTERM
, and so on. The command kill -l
on a Linux system will list all the available signals.
Sending Signals: The Role of kill()
While the kernel sends signals to report hardware events, and the terminal driver sends signals like SIGINT
in response to user input, the most common way for one process to send a signal to another is via the kill()
system call. The name is somewhat of a misnomer; while it is frequently used to terminate processes, its actual function is simply to send any signal to a process or a group of processes.
The prototype for the kill()
function is defined in <sys/types.h>
and <signal.h>
:
int kill(pid_t pid, int sig);
The function takes two arguments. The sig
argument is the signal number (e.g., SIGTERM
, SIGUSR1
) to be sent. The pid
argument determines the target of the signal, and its interpretation is crucial:
pid > 0
: The signal is sent to the process whose Process ID (PID) is equal topid
. This is the most straightforward use case: targeting a single, specific process.pid == 0
: The signal is sent to every process in the process group of the calling process. A process group is a collection of related processes, often a shell and the commands it has launched. This is useful for tasks like ensuring all related background jobs are terminated together.pid == -1
: The signal is sent to every process for which the calling process has permission to send signals, except for process 1 (init
) and the calling process itself. This is a powerful but potentially dangerous option, often used by system shutdown scripts.pid < -1
: The signal is sent to every process in the process group whose ID is-pid
(the absolute value ofpid
). This allows a process to signal a specific process group other than its own.
flowchart TD subgraph "kill(pid, sig) Logic" direction TB A["Start: kill(pid, sig) called"]:::primary A --> B{Check value of 'pid'}:::decision B --> C[pid > 0] C --> D["Send 'sig' to the process<br>with PID == <b>pid</b>"]:::process B --> E[pid == 0] E --> F["Send 'sig' to every process in the<br>caller's process group"]:::process B --> G[pid < -1] G --> H["Send 'sig' to every process in the<br>process group with PGID == <b>-pid</b>"]:::process B --> I[pid == -1] I --> J["Send 'sig' to all processes the<br>caller has permission for<br>(except init and self)"]:::process end D --> K[End]:::success F --> K H --> K J --> K classDef primary fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff; classDef success fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff; classDef decision fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff; classDef process fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff;
For a call to kill()
to succeed, the sending process must have the necessary permissions. The general rule is that a process can send a signal to another process if the real or effective user ID of the sender matches the real or effective user ID of the receiver. The exception is the superuser (root), which can send signals to any process. If the sender lacks permission, the call fails, and errno
is set to EPERM
. If the target PID does not exist, the call fails, and errno
is set to ESRCH
(No such process).
Self-Signaling: The raise()
Function
Sometimes, a process needs to send a signal to itself. This might seem strange, but it’s a useful technique for invoking the process’s own signal handling logic. For example, a complex application might detect an internal, non-recoverable error and could raise(SIGTERM)
to initiate its own graceful shutdown procedure, which is defined in its SIGTERM
handler. This centralizes the shutdown logic, ensuring it’s executed regardless of whether the termination request came from an external process or from the application itself.
While a process could achieve this with kill(getpid(), sig)
, the C standard library provides a more direct and portable function for this purpose: raise()
.
The raise()
function is defined in <signal.h>
:
int raise(int sig);
It takes a single argument: the signal to be sent. The call raise(sig)
is functionally equivalent to kill(getpid(), sig)
. It sends the specified signal to the calling process. Its primary benefit is simplicity and clarity of intent. When you see raise()
in code, you know immediately that the process is signaling itself, which makes the code more self-documenting.
sequenceDiagram participant P as Process (main loop) participant H as Signal Handler (handle_sigterm) P->>P: 1. Application logic runs... Note right of P: An internal error is detected<br>or a shutdown condition is met. P->>P: 2. Calls raise(SIGTERM) activate P Note over P: Kernel delivers SIGTERM<br>to the process itself. P-->>H: 3. Execution jumps to signal handler deactivate P activate H H->>P: 4. Sets global flag:<br>shutdown_requested = 1 H-->>P: 5. Handler returns deactivate H activate P P->>P: 6. Main code resumes after raise() call P->>P: 7. Checks flag: if (shutdown_requested) P->>P: 8. Calls cleanup_resources() deactivate P Note right of P: Process exits gracefully.
Process and Process Group IDs
A firm grasp of Process IDs (PIDs) and Process Group IDs (PGIDs) is essential for using kill()
effectively. Every process in a Linux system is assigned a unique PID when it is created. A process can get its own PID using the getpid()
system call and its parent’s PID using getppid()
.
Processes are also organized into process groups. Each process group has a unique Process Group ID (PGID). The PGID is always the PID of a specific process, known as the process group leader. By default, when a new process is created via fork()
, it becomes a member of its parent’s process group. A process can find its PGID using the getpgrp()
call.
Process groups are the basis for job control in shells. When you run a command pipeline like cat data.txt | grep "error" | wc -l
, the shell typically creates a new process group for this pipeline. The PGID for this group would be the PID of the cat
process. This organization allows the shell to manage the entire pipeline as a single “job.” If you then press Ctrl+C
, the shell sends a SIGINT
signal to the entire process group, ensuring that cat
, grep
, and wc
all terminate. This is an example of kill()
being used with a negative pid
argument to target a whole group.
In embedded systems, you might design a multi-process application where a primary “manager” process forks several “worker” processes. By placing all these workers in the same process group, the manager can efficiently signal all of them at once—for instance, sending a SIGUSR1
to tell them all to reload their configuration or a SIGTERM
to initiate a coordinated shutdown.
graph TD subgraph Terminal A(Shell Process: bash) end subgraph "Job: Pipeline (PGID = 1001)" direction LR B[Process 1001<br><b>cat data.txt</b>]:::process C["Process 1002<br><b>grep <i>error</i></b>"]:::process D[Process 1003<br><b>wc -l</b>]:::process B -- "pipe" --> C -- "pipe" --> D end subgraph Kernel E{User presses Ctrl+C}:::decision end E --> A A -- "Sends kill(-1001, SIGINT)" --> B A -- " " --> C A -- " " --> D style B fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff style C fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff style D fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff classDef decision fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff; classDef process fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff;
Practical Examples
This section provides hands-on examples for the Raspberry Pi 5. We will assume you have a standard Raspberry Pi OS (or a similar Debian-based distribution) running and are comfortable with compiling and running C programs from the command line.
Example 1: Basic Signal Sending with kill()
Our first example demonstrates the fundamental use of kill()
: one process sending a signal to another. We will create two programs: a “target” process that waits for a signal and a “sender” process that sends it.
The Target Program (target.c
)
The target program will simply print its PID and then enter an infinite loop, pausing execution with the pause()
system call. The pause()
function causes the process to sleep until any signal is caught. We will also set up a simple signal handler for SIGUSR1
to confirm that our signal was received.
// target.c: A process that waits to receive a signal.
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
// A simple signal handler for SIGUSR1
void handle_sigusr1(int sig) {
// Note: printf is not async-signal-safe, but used here for simple demonstration.
// In production code, use write() or other safe functions.
printf("\nTarget: Received SIGUSR1! Resuming work...\n");
}
int main() {
// Register the signal handler for SIGUSR1
signal(SIGUSR1, handle_sigusr1);
// Get and print the Process ID (PID)
pid_t my_pid = getpid();
printf("Target process running with PID: %d\n", my_pid);
printf("Target: Waiting for a signal...\n");
// Loop indefinitely, pausing to wait for signals
while(1) {
pause(); // Suspend execution until a signal is caught
printf("Target: Woke up from pause(). Waiting again.\n");
}
// This part is unreachable in this simple example
return 0;
}
Code Explanation:
handle_sigusr1
: This function is our custom signal handler. When the process receivesSIGUSR1
, this function will execute.signal(SIGUSR1, handle_sigusr1)
: This line registershandle_sigusr1
as the handler for theSIGUSR1
signal.getpid()
: This call retrieves the current process’s PID, which we need to provide to the sender program.pause()
: This is the key to our waiting mechanism. The process will block on this line until a signal handler has been executed.
The Sender Program (sender.c
)
The sender program will take a PID and a signal number as command-line arguments and use kill()
to send the specified signal to the target process.
// sender.c: A process that sends a signal to another process.
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <errno.h>
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <pid> <signal_number>\n", argv[0]);
return 1;
}
// Convert command-line arguments to integers
pid_t target_pid = atoi(argv[1]);
int signal_to_send = atoi(argv[2]);
printf("Sender: Attempting to send signal %d to PID %d\n", signal_to_send, target_pid);
// Use kill() to send the signal
if (kill(target_pid, signal_to_send) == -1) {
// Handle potential errors
perror("Sender: kill failed");
if (errno == ESRCH) {
fprintf(stderr, "Sender: Error - No such process with PID %d.\n", target_pid);
} else if (errno == EPERM) {
fprintf(stderr, "Sender: Error - Permission denied. Are you running as the same user?\n");
}
return 1;
}
printf("Sender: Signal sent successfully!\n");
return 0;
}
Code Explanation:
argc
,argv
: We use standard command-line arguments to get the target PID and signal number.atoi()
: This function converts the string arguments from the command line into integers.kill(target_pid, signal_to_send)
: This is the core of the program. It sends the signal.- Error Handling: We check the return value of
kill()
. If it is-1
, an error occurred. We useperror()
to print a system error message and also checkerrno
to provide more specific feedback to the user.
Build and Execution Steps
1. Open two terminals on your Raspberry Pi 5. One will be for the target
, the other for the sender
.
2. Compile the programs in each terminal:
# In Terminal 1
gcc -o target target.c
# In Terminal 2
gcc -o sender sender.c
3. Run the target program in Terminal 1:
# In Terminal 1
./target
The program will print its PID and wait. Note the PID. For example:
Target process running with PID: 12345
Target: Waiting for a signal...
4. Run the sender program in Terminal 2. Use the PID from the target and send signal 10
(which corresponds to SIGUSR1
on most Linux systems).
# In Terminal 2 (replace 12345 with the actual PID)
./sender 12345 10
The sender’s output will be:
Sender: Attempting to send signal 10 to PID 12345
Sender: Signal sent successfully!
5. Observe the output in Terminal 1. The target will wake up, execute its signal handler, and then go back to waiting.
Target process running with PID: 12345
Target: Waiting for a signal...
Target: Received SIGUSR1! Resuming work...
Target: Woke up from pause(). Waiting again.
6. Experiment further. Try sending other signals. For example, send SIGTERM
(signal 15) to terminate the target gracefully.
# In Terminal 2
./sender 12345 15
The target
process in Terminal 1 will now terminate and the shell prompt will return.
Example 2: Self-Signaling with raise()
for Graceful Shutdown
This example demonstrates how a process can use raise()
to trigger its own cleanup logic. This is a common pattern in embedded applications that need to shut down gracefully.
The Self-Terminating Program (self_term.c
)
// self_term.c: A program that uses raise() to trigger its own shutdown.
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
// A flag to control the main loop, modified by the signal handler
volatile sig_atomic_t shutdown_requested = 0;
// Signal handler for SIGTERM
void handle_sigterm(int sig) {
// This handler will be triggered by raise()
printf("\nSIGTERM caught! Initiating graceful shutdown...\n");
shutdown_requested = 1;
}
void cleanup_resources() {
printf("Cleaning up resources (e.g., closing files, releasing hardware)...\n");
// In a real application, you would close file descriptors,
// deallocate memory, shut down hardware interfaces, etc.
printf("Cleanup complete.\n");
}
int main() {
// Register the handler for SIGTERM
signal(SIGTERM, handle_sigterm);
printf("Application running with PID: %d\n", getpid());
printf("Simulating work for 5 seconds before triggering shutdown.\n");
// Simulate some work
sleep(5);
// An internal condition has been met, time to shut down.
printf("\nInternal condition met. Raising SIGTERM to self.\n");
if (raise(SIGTERM) != 0) {
perror("raise failed");
exit(1);
}
// The signal handler will execute here, setting shutdown_requested to 1.
// The program flow continues after the signal handler returns.
// Check the flag set by the signal handler
if (shutdown_requested) {
cleanup_resources();
}
printf("Exiting gracefully.\n");
return 0;
}
Code Explanation:
volatile sig_atomic_t shutdown_requested
: This global variable acts as a flag. It is declaredvolatile
to prevent the compiler from making optimizations that might break its usage in a signal handler.sig_atomic_t
guarantees that reads and writes to it are atomic, which is essential for variables shared between a signal handler and the main program logic.handle_sigterm
: This handler simply sets theshutdown_requested
flag to 1. It avoids doing complex work inside the handler itself, which is a best practice.raise(SIGTERM)
: After a simulated work period, the program callsraise()
to sendSIGTERM
to itself. This immediately invokeshandle_sigterm
.cleanup_resources()
: After the signal handler returns, the main program logic checks the flag and, if it’s set, proceeds to call the main cleanup function. This pattern separates the immediate signal notification from the more complex shutdown logic.
Build and Execution Steps
1. Compile the program:
gcc -o self_term self_term.c
2. Run the program:
./self_term
3. Observe the output: The program will run for 5 seconds, then print messages indicating it is raising the signal, handling it, cleaning up, and exiting.
Application running with PID: 1543534
Simulating work for 5 seconds before triggering shutdown.
Internal condition met. Raising SIGTERM to self.
SIGTERM caught! Initiating graceful shutdown...
Cleaning up resources (e.g., closing files, releasing hardware)...
Cleanup complete.
Exiting gracefully.
This example clearly shows how raise()
can be used to cleanly integrate signal handling logic into a program’s own control flow, a powerful technique for creating robust, self-managing applications.
Common Mistakes & Troubleshooting
When working with signals, developers often encounter a few common pitfalls. Understanding these can save significant debugging time.
Exercises
- The Watchdog and the Worker:
- Objective: Create a two-process system. A
worker
process creates a file/tmp/worker.pid
containing its PID and then enters a loop, printing a “working…” message every second. Awatchdog
process reads the PID from the file and, after 10 seconds, sends aSIGTERM
signal to the worker to terminate it. - Guidance: The
worker
should install aSIGTERM
handler that prints “SIGTERM received, shutting down…” and then callsexit()
. Thewatchdog
will need to read the PID from the file,sleep(10)
, and then usekill()
. - Verification: When you run the
worker
, it should start printing messages. When you run thewatchdog
, theworker
should stop after 10 seconds, printing its shutdown message before it exits.
- Objective: Create a two-process system. A
sequenceDiagram participant W as Worker Process participant FS as Filesystem (/tmp/worker.pid) participant D as Watchdog Process W->>W: 1. Starts up, gets own PID W->>FS: 2. Creates & writes PID to file activate FS FS-->>W: 3. File created deactivate FS loop Work Loop W->>W: 4. Prints "working..." W->>W: 5. sleep(1) end D->>D: 6. Starts up D->>FS: 7. Reads PID from file activate FS FS-->>D: 8. Returns PID of Worker deactivate FS D->>D: 9. sleep(10) Note over D,W: Watchdog sends SIGTERM D-->>W: 10. kill(worker_pid, SIGTERM) W->>W: 11. Catches SIGTERM, runs handler W->>W: 12. Prints "shutting down..." W-->>D: 13. Exits gracefully
- Configuration Reload:
- Objective: Modify the
target.c
program from our first example. In addition to handlingSIGUSR1
, make it also handleSIGHUP
. TheSIGHUP
handler should print a message like “SIGHUP received! Reloading configuration…”. - Guidance: You will need to register a second signal handler for
SIGHUP
. Use thesender
program to send bothSIGUSR1
(10) andSIGHUP
(1) to the target and observe the different outputs. - Verification: The target process should respond differently depending on which signal is sent, demonstrating the ability to handle multiple, distinct commands via signals.
- Objective: Modify the
- Process Group Termination:
- Objective: Write a program that forks three child processes. Each child should print its PID and PGID and then enter an infinite
sleep()
loop. The parent process should wait for 5 seconds and then send aSIGKILL
signal to the entire process group. - Guidance: Use
fork()
three times in the parent. The children can usegetpid()
andgetpgrp()
to print their information. The parent can get the process group ID withgetpgrp()
and then usekill(-pgid, SIGKILL)
to signal the group. - Verification: The parent and all three children should start. After 5 seconds, all three children should be terminated simultaneously by the single
kill()
call from the parent.
- Objective: Write a program that forks three child processes. Each child should print its PID and PGID and then enter an infinite
- Error Handling Robustness:
- Objective: Modify the
sender.c
program to be more robust. It should first check if the target PID exists before trying to send a signal. - Guidance: A common way to check if a process exists is to use
kill(pid, 0)
. This special call performs error checking (permissions, existence) but doesn’t actually send a signal. If this call succeeds, the process exists. If it fails withESRCH
, the process does not exist. - Verification: Run the modified sender with a non-existent PID. It should print a clear error message “Process does not exist” instead of the more generic one from
perror
. Then run it with a valid PID to ensure it still works correctly.
- Objective: Modify the
Summary
- Signals are Asynchronous Notifications: They are a fundamental IPC mechanism in Linux for notifying processes of hardware or software events.
kill()
Sends Signals: Thekill(pid, sig)
system call is the primary tool for a process to send a signal to another process or process group. The interpretation of thepid
argument allows for precise targeting.raise()
Signals Self: Theraise(sig)
function is a convenient and portable way for a process to send a signal to itself, which is useful for triggering its own signal handlers.- Permissions are Key: A process must have appropriate permissions (typically, matching user IDs) to send a signal to another process. The superuser is the exception.
- Process Groups Enable Job Control: Organizing processes into groups allows signals to be sent to multiple related processes at once, a crucial feature for managing complex applications and shell jobs.
- Signal Handlers Must Be Safe: Functions called from within a signal handler must be async-signal-safe. The best practice is to keep handlers minimal, often just setting a flag for the main program loop to act upon.
Further Reading
- The Linux Programming Interface by Michael Kerrisk – Chapters 20-22 provide an exhaustive and authoritative treatment of signals.
- Advanced Programming in the UNIX Environment by W. Richard Stevens and Stephen A. Rago – A classic text with deep insights into process control and signals.
signal(7)
Linux Manual Page – Provides a comprehensive overview of Linux signals. Access viaman 7 signal
.kill(2)
Linux Manual Page – The definitive documentation for thekill
system call. Access viaman 2 kill
.- Raspberry Pi Foundation Documentation – Official hardware and software documentation for the Raspberry Pi platform. https://www.raspberrypi.com/documentation/
- GNU C Library (glibc) Manual – Section on Signal Handling provides details on the C library’s implementation and wrappers. https://www.gnu.org/software/libc/manual/html_node/Signal-Handling.html