Skip to content

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 with SIGCHLD, 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 through execve() image replacement to waitpid().

  • 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_SHARED and msync().

  • POSIX Shared Memory Lab


    Engineer a high-throughput, zero-copy IPC engine connecting independent processes via /dev/shm and 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>):

~/c_ipc_lab/zombie_reaper.c
#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;
}
Testing Zombie Reaping
gcc -std=c17 -Wall -Wextra zombie_reaper.c -o zombie_reaper
./zombie_reaper

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:

~/c_ipc_lab/safe_signal.c
#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):

Demonstrating pipe() and dup2() Redirection
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.
Reading Large Files via mmap
#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:

~/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:

~/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:

~/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;
}
Compiling & Running IPC Lab
# 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.

    Network epoll Guide

  • Part 2.3: Debugging, Sanitizers & Profiling


    Review GDB core dumps, AddressSanitizer (ASan), Valgrind Massif, and compiler hardening flags.

    Debugging Guide

  • Linux Process Management & System Calls


    Inspect running ELF processes, signals (SIGTERM/SIGKILL), strace syscall traces, and /proc internals.

    Process Management