Signals and IPC
Signals are asynchronous notifications delivered to a process by the kernel. IPC (Inter-Process Communication) mechanisms let separate processes exchange data — pipes, shared memory, message queues, and sockets.
Why It Matters
Every daemon handles SIGTERM for graceful shutdown. Every shell uses pipes. Every high-performance service uses shared memory or sockets. Understanding signals and IPC is how you build systems where multiple processes cooperate.
Signals
A signal interrupts normal execution and invokes a handler (or takes a default action).
Signal Lifecycle
Source (kill, kernel, hardware)
→ Signal generated
→ Added to target process's pending set
→ When process is scheduled: signal delivered
→ Handler runs (or default action: terminate, core dump, ignore)
Common Signals
| Signal | Number | Default Action | Trigger |
|---|---|---|---|
SIGINT | 2 | Terminate | Ctrl+C |
SIGTERM | 15 | Terminate | kill PID (polite shutdown) |
SIGKILL | 9 | Terminate (uncatchable) | kill -9 (force kill) |
SIGSEGV | 11 | Core dump | Invalid memory access |
SIGCHLD | 17 | Ignore | Child process exits |
SIGPIPE | 13 | Terminate | Write to closed pipe/socket |
SIGSTOP | 19 | Stop (uncatchable) | kill -STOP or Ctrl+Z |
SIGCONT | 18 | Continue | fg or kill -CONT |
SIGUSR1 | 10 | Terminate | User-defined |
SIGALRM | 14 | Terminate | alarm() timer expires |
sigaction (Proper Signal Handling)
Use sigaction instead of signal() — it has well-defined semantics across platforms:
#include <signal.h>
#include <unistd.h>
volatile sig_atomic_t got_signal = 0; // only safe type in handlers
void handler(int sig) {
got_signal = 1; // set flag — do minimal work in handlers
// Only call async-signal-safe functions here
// (write, _exit — NOT printf, malloc, mutex operations)
}
int main(void) {
struct sigaction sa = {
.sa_handler = handler,
.sa_flags = SA_RESTART, // restart interrupted syscalls
};
sigemptyset(&sa.sa_mask);
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
while (!got_signal) {
// main work loop
pause(); // sleep until signal
}
// cleanup and exit
return 0;
}Rule: signal handlers should set a flag and return. Do real work in the main loop.
IPC Mechanisms
Pipes (Unidirectional)
int fds[2];
pipe(fds); // fds[0] = read end, fds[1] = write end
if (fork() == 0) { // child writes
close(fds[0]);
write(fds[1], "hello", 5);
close(fds[1]);
_exit(0);
} else { // parent reads
close(fds[1]);
char buf[16];
ssize_t n = read(fds[0], buf, sizeof(buf));
buf[n] = '\0';
printf("got: %s\n", buf);
close(fds[0]);
}Named pipes (FIFOs) persist in the filesystem: mkfifo("/tmp/myfifo", 0644).
Shared Memory (Fastest IPC)
Zero-copy — both processes access the same physical pages:
#include <sys/mman.h>
#include <fcntl.h>
// Process 1: create and write
int fd = shm_open("/myshm", O_CREAT | O_RDWR, 0644);
ftruncate(fd, 4096);
int *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
*ptr = 42; // write to shared memory
close(fd);
// Process 2: open and read
int fd = shm_open("/myshm", O_RDONLY, 0);
int *ptr = mmap(NULL, 4096, PROT_READ, MAP_SHARED, fd, 0);
printf("value: %d\n", *ptr); // 42
munmap(ptr, 4096);
close(fd);
shm_unlink("/myshm"); // cleanupShared memory needs synchronization (semaphores, futexes) — the kernel doesn’t protect you from races.
IPC Comparison
| Method | Speed | Complexity | Use Case |
|---|---|---|---|
| Pipe | Medium | Low | Parent-child, shell pipelines |
| Named pipe (FIFO) | Medium | Low | Unrelated processes, simple streams |
| Shared memory | Fastest | High (need sync) | High-throughput data sharing |
| Unix socket | Medium | Medium | Client-server on same machine |
| Message queue | Medium | Medium | Decoupled producers/consumers |
Related
- Processes and Threads — signals target processes, IPC connects them
- File IO in C — pipes are file descriptors
- Concurrency and Synchronization — shared memory needs locks
- Socket Programming — Unix domain sockets for local IPC