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

SignalNumberDefault ActionTrigger
SIGINT2TerminateCtrl+C
SIGTERM15Terminatekill PID (polite shutdown)
SIGKILL9Terminate (uncatchable)kill -9 (force kill)
SIGSEGV11Core dumpInvalid memory access
SIGCHLD17IgnoreChild process exits
SIGPIPE13TerminateWrite to closed pipe/socket
SIGSTOP19Stop (uncatchable)kill -STOP or Ctrl+Z
SIGCONT18Continuefg or kill -CONT
SIGUSR110TerminateUser-defined
SIGALRM14Terminatealarm() 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");  // cleanup

Shared memory needs synchronization (semaphores, futexes) — the kernel doesn’t protect you from races.

IPC Comparison

MethodSpeedComplexityUse Case
PipeMediumLowParent-child, shell pipelines
Named pipe (FIFO)MediumLowUnrelated processes, simple streams
Shared memoryFastestHigh (need sync)High-throughput data sharing
Unix socketMediumMediumClient-server on same machine
Message queueMediumMediumDecoupled producers/consumers