C Linux IPC & Process Architecture: fork, Signals, mmap & POSIX Shared Memory
Created: 2026-11-07 Last updated: 2026-11-07
Executive Summary: Threads within a single process share memory effortlessly, but a single memory corruption or segmentation fault crashes the entire application. In mission-critical systems architecture (such as PostgreSQL, Nginx, or Chromium), stability is achieved through Multi-Process Architecture. By running isolated processes coordinated through Inter-Process Communication (IPC), software achieves fault isolation, independent privileges, and zero-downtime rolling updates. This guide forms Part 3.2 (Advanced & Deep Dive) of our C curriculum: exploring kernel
fork()and Copy-On-Write (COW), eliminating zombie processes withSIGCHLD, authoring async-signal-safe handlers, and engineering sub-microsecond zero-copy pipelines with POSIX Shared Memory (shm_open) and named semaphores.
-
Process Lifecycle & COW
Trace process instantiation from
fork()and kernel Copy-On-Write (COW) memory paging throughexecve()image replacement towaitpid(). -
Async-Signal-Safe Handlers
Understand kernel signal frame delivery, install reliable
sigaction()handlers, and avoid deadlock traps by restricting to async-safe syscalls. -
Memory-Mapped I/O (mmap)
Bypass user-space buffer copying by mapping file contents directly into process virtual memory pages with
MAP_SHAREDandmsync(). -
POSIX Shared Memory Lab
Engineer a high-throughput, zero-copy IPC engine connecting independent processes via
/dev/shmand POSIX named semaphores.
1. The Linux Process Lifecycle: fork(), execve() & Copy-On-Write (COW)
Every process in Linux is assigned a unique Process ID (PID). Process creation follows the classic Unix fork-exec paradigm:
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
P["Parent Process (PID: 2040)<br/>Memory: Page Table Mapping"]
FORK["⚡ fork() System Call<br/>Kernel duplicates Page Tables<br/>Physical frames marked READ-ONLY!"]
COW["🛡️ Copy-On-Write (COW)<br/>When either process writes,<br/>MMU triggers page fault & duplicates frame"]
EXEC["🚀 execve() System Call<br/>Replaces process image with new ELF binary<br/>(Clears heap & stack; preserves open FDs)"]
P --> FORK
FORK --> COW
COW --> EXEC
Understanding Kernel Copy-On-Write (COW)
When a process calls fork(), the kernel does not duplicate the process's physical RAM contents. Doing so for a 16 GB database daemon would take hundreds of milliseconds and exhaust memory.
Instead, the kernel duplicates only the Page Table Directory:
1. Both parent and child page table entries point to the exact same physical memory frames.
2. All shared pages are marked as Read-Only (PAGE_READONLY).
3. When either process attempts to write to a page, the CPU Memory Management Unit (MMU) catches the violation and triggers a Page Fault.
4. The Linux kernel's fault handler transparently allocates a new physical frame, copies the 4 KB page content, marks it writable, and resumes execution.
Zombie Processes & Asynchronous Reaping (SIGCHLD)
When a child process terminates, its memory space and file descriptors are immediately reclaimed by the kernel. However, its entry in the Process Table must remain until the parent process reads its exit status code (WIFEXITED, WEXITSTATUS). A terminated process awaiting reap is called a Zombie (<defunct>):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>
// Asynchronous signal handler for SIGCHLD (Reaps zombies automatically!)
void handle_sigchld(int sig) {
(void)sig;
int saved_errno = errno; // Preserve errno across signal handler!
// WNOHANG: Reap all terminated children without blocking the parent
pid_t pid;
int status;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if (WIFEXITED(status)) {
// Write directly with async-signal-safe syscall (dprintf/write)
const char msg[] = "Reaped zombie child process asynchronously.\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1);
}
}
errno = saved_errno;
}
int main(void) {
// 1. Install SIGCHLD handler with SA_RESTART
struct sigaction sa;
sa.sa_handler = handle_sigchld;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);
printf("[Parent PID: %d] Spawning child...\n", getpid());
pid_t pid = fork();
if (pid == 0) {
// Child Process
printf(" [Child PID: %d] Working for 1 second...\n", getpid());
sleep(1);
printf(" [Child PID: %d] Terminating.\n", getpid());
exit(42);
}
// Parent continues its normal event loop
for (int i = 0; i < 3; i++) {
printf("[Parent] Serving business logic tick %d...\n", i + 1);
sleep(1);
}
return EXIT_SUCCESS;
}
2. Linux Signals & Async-Signal-Safety
Signals are asynchronous software interrupts generated by the kernel (e.g. SIGINT from Ctrl+C, SIGTERM from systemd, SIGSEGV on bad pointer).
When a signal arrives, the kernel interrupts whatever the thread was doing, saves the thread's CPU register context on the stack, and redirects instruction execution to the registered signal handler.
The Async-Signal-Safe Golden Rule
Because a signal can interrupt execution at any instruction boundary (even in the middle of a malloc() or printf() call), calling non-reentrant functions inside a signal handler leads to deadlocks or memory corruption:
// ❌ CRITICAL BUG: NEVER DO THIS IN A SIGNAL HANDLER!
void dangerous_handler(int sig) {
printf("Received signal %d\n", sig); // DEADLOCK RISK: printf acquires internal glibc mutex!
free(g_buffer); // CORRUPTION RISK: Heap allocator structures can be in mid-mutation!
}
POSIX defines a strict list of Async-Signal-Safe functions (documented in man 7 signal-safety). These include write(), _exit(), and sigprocmask().
The Clean Pattern: volatile sig_atomic_t
The industry standard approach is to perform zero heavy work in the signal handler—simply mutate an atomic flag and allow the main application event loop to process the event cleanly:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
// Lock-free atomic integer type guaranteed to read/write in a single CPU instruction
static volatile sig_atomic_t g_shutdown_requested = 0;
void handle_signal(int sig) {
(void)sig;
g_shutdown_requested = 1; // Atomic flag mutation (100% Signal-Safe!)
}
int main(void) {
struct sigaction sa = { .sa_handler = handle_signal };
sigemptyset(&sa.sa_mask);
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
printf("Daemon running (PID: %d). Send SIGINT (Ctrl+C) to stop.\n", getpid());
while (!g_shutdown_requested) {
printf("Doing work in main loop...\n");
sleep(1);
}
printf("\nGraceful shutdown flag detected! Cleaning up state...\n");
return EXIT_SUCCESS;
}
3. Inter-Process Communication (IPC): Pipes vs. FIFOs
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart LR
subgraph Pipe ["Anonymous Pipe (pipe())"]
P1["Parent Process"] ==>|"write(fd[1])"| KBUF["Kernel Pipe Ring Buffer (64 KB)"]
KBUF ==>|"read(fd[0])"| C1["Child Process"]
end
Anonymous Pipes (pipe())
An Anonymous Pipe is a unidirectional byte stream managed by a 64 KB kernel ring buffer. It connects related processes (typically parent and child):
int pipefd[2];
pipe(pipefd); // pipefd[0] is Read End; pipefd[1] is Write End
if (fork() == 0) {
// Child: Redirect stdout to the write end of the pipe
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
execlp("ls", "ls", "-la", NULL);
}
// Parent: Read output from child
close(pipefd[1]);
char buffer[1024];
ssize_t n = read(pipefd[0], buffer, sizeof(buffer) - 1);
buffer[n] = '\0';
printf("Captured from child process:\n%s\n", buffer);
close(pipefd[0]);
4. Memory-Mapped Files (mmap())
The mmap() system call asks the kernel's virtual memory subsystem to map a file directly into process address space.
MAP_SHARED: Writes to the memory buffer modify the underlying physical file and are visible to all other processes mapping the same file.MAP_PRIVATE: Creates a Copy-On-Write private mapping. Writes do not alter the physical file on disk.
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
int fd = open("large_dataset.bin", O_RDONLY);
off_t file_size = lseek(fd, 0, SEEK_END);
// Map file directly into virtual address space
void *data = mmap(NULL, file_size, PROT_READ, MAP_SHARED, fd, 0);
// Data is now accessible directly as a normal memory pointer!
// Zero read() buffer copying overhead!
munmap(data, file_size);
close(fd);
5. Hands-On Lab: Sub-Microsecond IPC Engine with POSIX Shared Memory & Semaphores
When two distinct processes must exchange high-frequency telemetry records (e.g. trading engines, sensor collectors, or database write-ahead logs), network sockets or pipes introduce unacceptable latency.
POSIX Shared Memory (shm_open) creates an in-memory file backed by the Linux tmpfs RAM filesystem (/dev/shm). Paired with POSIX Named Semaphores (sem_open), processes communicate with sub-microsecond latency.
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
P1["Producer Process (Telemetry Generator)"]
P2["Consumer Process (Telemetry Logger)"]
subgraph RAM ["/dev/shm (Linux tmpfs RAM Space)"]
SHM["🗄️ Shared Memory Region: 'telemetry_shm'<br/>Direct zero-copy struct memory"]
S_MUTEX[("🔒 /sem_mutex (Mutual Exclusion)")]
S_FULL[("📦 /sem_full (Available Items Counter)")]
end
P1 ==>|"Writes struct directly"| SHM
P1 -.->|"sem_post()"| S_FULL
P2 -.->|"sem_wait()"| S_FULL
P2 ==>|"Reads struct directly"| SHM
Common Data Header (shm_common.h)
Create ~/c_ipc_lab/shm_common.h:
#pragma once
#include <stdint.h>
#define SHM_NAME "/nhdoc_telemetry_shm"
#define SEM_MUTEX_NAME "/nhdoc_sem_mutex"
#define SEM_ITEMS_NAME "/nhdoc_sem_items"
typedef struct {
uint64_t timestamp_ns;
uint32_t sensor_id;
double temperature;
double voltage;
char status_label[32];
} TelemetryPacket;
typedef struct {
TelemetryPacket packet;
int is_shutdown;
} SharedDataArea;
The Producer (ipc_producer.c)
Create ~/c_ipc_lab/ipc_producer.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <semaphore.h>
#include <time.h>
#include "shm_common.h"
int main(void) {
// 1. Create shared memory region in /dev/shm
int shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
if (shm_fd < 0) { perror("shm_open"); exit(1); }
// 2. Set physical size of shared region
ftruncate(shm_fd, sizeof(SharedDataArea));
// 3. Map shared memory into virtual address space
SharedDataArea *shm = mmap(NULL, sizeof(SharedDataArea),
PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
// 4. Create named semaphores
sem_t *sem_mutex = sem_open(SEM_MUTEX_NAME, O_CREAT, 0666, 1);
sem_t *sem_items = sem_open(SEM_ITEMS_NAME, O_CREAT, 0666, 0);
printf("=== IPC PRODUCER RUNNING ===\n");
for (int i = 1; i <= 5; i++) {
// Wait on mutex
sem_wait(sem_mutex);
// Write telemetry packet directly into shared RAM
shm->packet.sensor_id = 101;
shm->packet.temperature = 24.5 + (i * 0.8);
shm->packet.voltage = 3.3;
shm->packet.timestamp_ns = (uint64_t)time(NULL);
snprintf(shm->packet.status_label, sizeof(shm->packet.status_label),
"BATCH_TICK_%d", i);
shm->is_shutdown = 0;
printf("[Producer] Emitted record #%d (Temp: %.2f C)\n",
i, shm->packet.temperature);
// Release mutex and increment available items semaphore
sem_post(sem_mutex);
sem_post(sem_items);
usleep(150000); // 150ms interval
}
// Send termination signal
sem_wait(sem_mutex);
shm->is_shutdown = 1;
sem_post(sem_mutex);
sem_post(sem_items);
munmap(shm, sizeof(SharedDataArea));
close(shm_fd);
sem_close(sem_mutex);
sem_close(sem_items);
printf("Producer finished.\n");
return 0;
}
The Consumer (ipc_consumer.c)
Create ~/c_ipc_lab/ipc_consumer.c:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <semaphore.h>
#include "shm_common.h"
int main(void) {
// 1. Open existing shared memory region
int shm_fd = shm_open(SHM_NAME, O_RDWR, 0666);
if (shm_fd < 0) {
perror("shm_open (Run producer first!)");
exit(1);
}
SharedDataArea *shm = mmap(NULL, sizeof(SharedDataArea),
PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
sem_t *sem_mutex = sem_open(SEM_MUTEX_NAME, 0);
sem_t *sem_items = sem_open(SEM_ITEMS_NAME, 0);
printf("=== IPC CONSUMER RUNNING ===\n");
while (1) {
// Wait until an item is available
sem_wait(sem_items);
sem_wait(sem_mutex);
if (shm->is_shutdown) {
printf("[Consumer] Received shutdown signal.\n");
sem_post(sem_mutex);
break;
}
// Read zero-copy struct from shared memory
printf("📊 [Consumer RECEIVED] Sensor: %u | Temp: %.2f C | Status: %s\n",
shm->packet.sensor_id,
shm->packet.temperature,
shm->packet.status_label);
sem_post(sem_mutex);
}
// Clean up resources and remove shared files from system
munmap(shm, sizeof(SharedDataArea));
close(shm_fd);
sem_close(sem_mutex);
sem_close(sem_items);
shm_unlink(SHM_NAME);
sem_unlink(SEM_MUTEX_NAME);
sem_unlink(SEM_ITEMS_NAME);
printf("Cleaned up shared memory and semaphores from /dev/shm.\n");
return 0;
}
# Compile both applications (linking with -lrt and -pthread for POSIX shm)
gcc -std=c17 -Wall -Wextra ipc_producer.c -lrt -pthread -o ipc_producer
gcc -std=c17 -Wall -Wextra ipc_consumer.c -lrt -pthread -o ipc_consumer
# Terminal 1: Launch Consumer first
./ipc_consumer
# Terminal 2: Launch Producer
./ipc_producer
Where to Go Next
Continue your journey through the C Systems Programming curriculum:
-
Part 3.3: High-Performance Network & epoll
Master non-blocking Berkeley sockets, C10K I/O multiplexing, edge-triggered epoll, and zero-copy sendfile.
-
Part 2.3: Debugging, Sanitizers & Profiling
Review GDB core dumps, AddressSanitizer (ASan), Valgrind Massif, and compiler hardening flags.
-
Linux Process Management & System Calls
Inspect running ELF processes, signals (
SIGTERM/SIGKILL),stracesyscall traces, and/procinternals.