Zum Inhalt

C Linux IPC & Prozess-Architektur: fork, Signale, mmap & POSIX Shared Memory

Erstellt: 07.11.2026 Aktualisiert: 07.11.2026

Management Summary: Threads innerhalb eines Prozesses teilen sich denselben Arbeitsspeicher – doch ein einziger Speicherfehler oder Absturz reißt die gesamte Anwendung mit in den Abgrund. In hochkritischen Systemarchitekturen (wie PostgreSQL, Nginx oder Chromium) setzt man daher auf Multi-Prozess-Architekturen. Durch getrennte Prozesse, die über Inter-Process Communication (IPC) Daten austauschen, erreicht man vollständige Fehlertrennung, unabhängige Berechtigungen und unterbrechungsfreie Rolling Updates. Dieser Leitfaden bildet Teil 3.2 (Experten & Deep Dive) unseres C-Curriculums: Er beleuchtet den Linux-fork()-Mechanismus mit Copy-On-Write (COW), eliminiert Zombie-Prozesse mit SIGCHLD, baut signalsichere Event-Handler und implementiert eine extrem schnelle Zero-Copy-Pipeline mit POSIX Shared Memory (shm_open) und Semaphoren.

  • Prozess-Lebenszyklus & COW


    Prozess-Erzeugung von fork() und Kernel-Copy-On-Write (COW) über execve()-Image-Ersetzung bis hin zu waitpid() nachverfolgen.

  • Async-Signal-Safe Handler


    Signale im Kernel verstehen, zuverlässige sigaction()-Handler installieren und Deadlocks durch strikte Beschränkung auf sichere Syscalls meiden.

  • Memory-Mapped I/O (mmap)


    Pufferkopien im User Space umgehen, indem Dateiinhalte über MAP_SHARED und msync() direkt in virtuelle Adressräume eingeblendet werden.

  • POSIX Shared Memory Lab


    Eine High-Throughput-IPC-Engine konstruieren, die getrennte Prozesse über /dev/shm und POSIX-Semaphoren mit Zero-Copy-Geschwindigkeit verbindet.


1. Der Linux-Prozess-Lebenszyklus: fork(), execve() & Copy-On-Write (COW)

Jeder Linux-Prozess besitzt eine eindeutige Prozess-ID (PID). Neue Prozesse entstehen über das klassische Unix-Fork-Exec-Prinzip:

%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
    P["Elternprozess (PID: 2040)<br/>Speicher: Page-Table-Mapping"]
    FORK["⚡ fork() Systemaufruf<br/>Kernel dupliziert Page-Tables<br/>Physische Frames werden READ-ONLY!"]
    COW["🛡️ Copy-On-Write (COW)<br/>Bei Schreibzugriff löst MMU Page-Fault aus<br/>Kernel dupliziert 4KB-Frame"]
    EXEC["🚀 execve() Systemaufruf<br/>Ersetzt Prozess-Image mit neuem ELF-Binary<br/>(Setzt Heap & Stack zurück; FDs bleiben)"]

    P --> FORK
    FORK --> COW
    COW --> EXEC

Funktionsweise von Copy-On-Write (COW)

Ruft ein Prozess fork() auf, kopiert der Linux-Kernel nicht sofort den gesamten physikalischen Arbeitsspeicher. Bei einem 16-GB-Datenbankprozess würde dies hunderte Millisekunden dauern und den RAM sprengen.

Stattdessen dupliziert der Kernel nur die Seitentabellen (Page Tables): 1. Eltern- und Kindprozess zeigen auf exakt dieselben physikalischen RAM-Frames. 2. Alle gemeinsamen Seiten werden als Read-Only (PAGE_READONLY) markiert. 3. Versucht einer der beiden Prozesse, in eine Seite zu schreiben, fängt die Hardware-Memory-Management-Unit (MMU) den Schreibzugriff ab und löst einen Page Fault aus. 4. Der Kernel allokiert nun transparent einen neuen 4-KB-Frame, kopiert die Daten dieser einen Seite, markiert sie als beschreibbar und setzt die Ausführung fort.


Zombie-Prozesse & Asynchrones Aufräumen (SIGCHLD)

Beendet sich ein Kindprozess, gibt der Kernel dessen Speicher und Dateideskriptoren sofort frei. Der Eintrag in der Prozess-Tabelle muss jedoch bestehen bleiben, bis der Elternprozess den Exit-Status (WIFEXITED, WEXITSTATUS) abfragt. Einen beendeten Prozess vor der Abfrage nennt man 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>

// Asynchroner Signal-Handler für SIGCHLD (Räumt Zombies automatisch ab!)
void handle_sigchld(int sig) {
    (void)sig;
    int gemerkter_errno = errno; // errno über Signal-Handler hinweg bewahren!

    // WNOHANG: Alle beendeten Kinder einsammeln, ohne den Elternprozess zu blockieren
    pid_t pid;
    int status;
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status)) {
            // Direkt über signalsicheren Syscall schreiben (write)
            const char nachricht[] = "Zombie-Kindprozess asynchron abgeräumt.\n";
            write(STDOUT_FILENO, nachricht, sizeof(nachricht) - 1);
        }
    }

    errno = gemerkter_errno;
}

int main(void) {
    // 1. SIGCHLD-Handler mit SA_RESTART installieren
    struct sigaction sa;
    sa.sa_handler = handle_sigchld;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
    sigaction(SIGCHLD, &sa, NULL);

    printf("[Elternprozess PID: %d] Erzeuge Kindprozess...\n", getpid());
    pid_t pid = fork();

    if (pid == 0) {
        // Kindprozess
        printf("  [Kindprozess PID: %d] Arbeitet für 1 Sekunde...\n", getpid());
        sleep(1);
        printf("  [Kindprozess PID: %d] Beendet sich.\n", getpid());
        exit(42);
    }

    // Elternprozess läuft ungestört in seiner Hauptschleife weiter
    for (int i = 0; i < 3; i++) {
        printf("[Elternprozess] Verarbeite Geschäftslogik Schritt %d...\n", i + 1);
        sleep(1);
    }

    return EXIT_SUCCESS;
}
Zombie-Reaping testen
gcc -std=c17 -Wall -Wextra zombie_reaper.c -o zombie_reaper
./zombie_reaper

2. Linux-Signale & Async-Signal-Safety

Signale sind asynchrone Benachrichtigungen des Kernels an einen Prozess (z. B. SIGINT durch Strg+C, SIGTERM von systemd oder SIGSEGV bei Speicherfehlern).

Trifft ein Signal ein, unterbricht der Kernel den Programmablauf an beliebiger Stelle, sichert die CPU-Register und springt in den registrierten Signal-Handler.

Das Gesetz der Async-Signal-Safety

Weil ein Signal zu jedem beliebigen Zeitpunkt eintreffen kann (mitten in einem malloc()- oder printf()-Aufruf), führt der Aufruf nicht-reentranter Funktionen im Signal-Handler unweigerlich zu Deadlocks oder Datenkorruption:

// ❌ SCHWERER FEHLER: NIEMALS IM SIGNAL-HANDLER AUSFÜHREN!
void gefaehrlicher_handler(int sig) {
    printf("Signal %d empfangen\n", sig); // DEADLOCK-GEFAHR: printf sperrt internen Mutex!
    free(g_puffer);                       // KORRUPTIONS-GEFAHR: Heap-Metadaten können inkonsistent sein!
}

POSIX definiert eine strenge Liste von Async-Signal-Safe Funktionen (einsehbar über man 7 signal-safety), darunter write(), _exit() und sigprocmask().


Die saubere Praxis: volatile sig_atomic_t

Die industriell erprobte Best Practice besteht darin, im Signal-Handler keinerlei schwere Arbeit zu verrichten, sondern lediglich ein atomares Flag zu setzen, das die Hauptschleife des Programms sicher auswertet:

~/c_ipc_lab/safe_signal.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

// Garantiert atomares Lesen und Schreiben in einem einzigen CPU-Befehl
static volatile sig_atomic_t g_shutdown_angefordert = 0;

void handle_signal(int sig) {
    (void)sig;
    g_shutdown_angefordert = 1; // 100% signalsichere Flag-Änderung!
}

int main(void) {
    struct sigaction sa = { .sa_handler = handle_signal };
    sigemptyset(&sa.sa_mask);
    sigaction(SIGTERM, &sa, NULL);
    sigaction(SIGINT, &sa, NULL);

    printf("Daemon läuft (PID: %d). Strg+C zum Beenden senden.\n", getpid());

    while (!g_shutdown_angefordert) {
        printf("Hauptschleife führt Aufgaben aus...\n");
        sleep(1);
    }

    printf("\nShutdown-Flag erkannt! Geordnetes Herunterfahren...\n");
    return EXIT_SUCCESS;
}

3. Inter-Process Communication (IPC): Pipes vs. FIFOs

%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart LR
    subgraph Pipe ["Anonyme Pipe (pipe())"]
        P1["Elternprozess"] ==>|"write(fd[1])"| KBUF["Kernel Pipe-Ringpuffer (64 KB)"]
        KBUF ==>|"read(fd[0])"| C1["Kindprozess"]
    end

Anonyme Pipes (pipe())

Eine anonyme Pipe ist ein unidirektionaler Datenstrom im Kernel-Speicher (64-KB-Ringpuffer) zur Verbindung eng verwandter Prozesse (Eltern $\leftrightarrow$ Kind):

Demonstration von pipe() und dup2()-Umleitung
int pipefd[2];
pipe(pipefd); // pipefd[0] ist Lese-Ende; pipefd[1] ist Schreib-Ende

if (fork() == 0) {
    // Kind: stdout auf das Schreibende der Pipe umleiten
    close(pipefd[0]);
    dup2(pipefd[1], STDOUT_FILENO);
    close(pipefd[1]);
    execlp("ls", "ls", "-la", NULL);
}

// Elternprozess: Ausgabe des Kindes über das Leseende empfangen
close(pipefd[1]);
char puffer[1024];
ssize_t n = read(pipefd[0], puffer, sizeof(puffer) - 1);
puffer[n] = '\0';
printf("Vom Kindprozess empfangen:\n%s\n", puffer);
close(pipefd[0]);

4. Memory-Mapped Files (mmap())

Der mmap()-Syscall blendet Dateiinhalte direkt in den virtuellen Adressraum des Prozesses ein:

  • MAP_SHARED: Schreibzugriffe verändern die physische Datei und sind für alle anderen Prozesse sichtbar, die dieselbe Datei mappen.
  • MAP_PRIVATE: Erzeugt eine Copy-On-Write-Kopie. Änderungen bleiben lokal im Prozess und verändern nicht die Datei auf der Festplatte.
Große Dateien blitzschnell via mmap verarbeiten
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

int fd = open("große_daten.bin", O_RDONLY);
off_t dateigroesse = lseek(fd, 0, SEEK_END);

// Datei direkt in den virtuellen Adressraum einblenden
void *daten = mmap(NULL, dateigroesse, PROT_READ, MAP_SHARED, fd, 0);

// Daten lassen sich nun direkt wie ein normales C-Array ansprechen!
// Null Overhead durch read()-Pufferkopien!

munmap(daten, dateigroesse);
close(fd);

5. Hands-On Lab: Sub-Mikrosekunden IPC mit POSIX Shared Memory & Semaphoren

Müssen zwei getrennte Prozesse hochfrequente Telemetriedaten mit maximaler Performance austauschen, erzeugen Netzwerk-Sockets oder Pipes spürbare Latenzen.

POSIX Shared Memory (shm_open) legt einen gemeinsamen RAM-Bereich im Linux-tmpfs-Dateisystem (/dev/shm) an. In Kombination mit POSIX Named Semaphores (sem_open) kommunizieren die Prozesse mit Nanosekunden-Latenz.

%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
    P1["Producer-Prozess (Sensor-Simulator)"]
    P2["Consumer-Prozess (Telemetrie-Logger)"]

    subgraph RAM ["/dev/shm (Linux tmpfs RAM-Bereich)"]
        SHM["🗄️ Shared-Memory-Bereich: 'telemetry_shm'<br/>Direkter Zugriff auf gemeinsame C-Struktur"]
        S_MUTEX[("🔒 /sem_mutex (Mutex-Schutz)")]
        S_FULL[("📦 /sem_full (Zähler für fertige Pakete)")]
    end

    P1 ==>|"Schreibt Struktur direkt in RAM"| SHM
    P1 -.->|"sem_post()"| S_FULL
    P2 -.->|"sem_wait()"| S_FULL
    P2 ==>|"Liest Struktur direkt aus RAM"| SHM

Gemeinsame Schnittstellen-Header (shm_common.h)

Erstelle ~/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 temperatur;
    double spannung;
    char status_label[32];
} TelemetriePaket;

typedef struct {
    TelemetriePaket paket;
    int ist_beendet;
} GemeinsamerSpeicher;

Der Producer (ipc_producer.c)

Erstelle ~/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. Shared Memory in /dev/shm anlegen
    int shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    if (shm_fd < 0) { perror("shm_open"); exit(1); }

    // 2. Größe des Speicherbereichs festlegen
    ftruncate(shm_fd, sizeof(GemeinsamerSpeicher));

    // 3. In den virtuellen Adressraum mappen
    GemeinsamerSpeicher *shm = mmap(NULL, sizeof(GemeinsamerSpeicher), 
                                    PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);

    // 4. Benannte POSIX-Semaphoren erzeugen
    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 GESTARTET ===\n");

    for (int i = 1; i <= 5; i++) {
        // Mutex anfordern
        sem_wait(sem_mutex);

        // Telemetriedaten direkt in das gemeinsame RAM schreiben
        shm->paket.sensor_id = 101;
        shm->paket.temperatur = 24.5 + (i * 0.8);
        shm->paket.spannung = 3.3;
        shm->paket.timestamp_ns = (uint64_t)time(NULL);
        snprintf(shm->paket.status_label, sizeof(shm->paket.status_label), 
                 "BATCH_TICK_%d", i);
        shm->ist_beendet = 0;

        printf("[Producer] Datensatz #%d geschrieben (Temp: %.2f C)\n", 
               i, shm->paket.temperatur);

        // Mutex freigeben und Item-Semaphore erhöhen
        sem_post(sem_mutex);
        sem_post(sem_items);

        usleep(150000); // 150ms Intervall
    }

    // Beendigungs-Signal senden
    sem_wait(sem_mutex);
    shm->ist_beendet = 1;
    sem_post(sem_mutex);
    sem_post(sem_items);

    munmap(shm, sizeof(GemeinsamerSpeicher));
    close(shm_fd);
    sem_close(sem_mutex);
    sem_close(sem_items);

    printf("Producer sauber beendet.\n");
    return 0;
}

Der Consumer (ipc_consumer.c)

Erstelle ~/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. Bestehenden Shared Memory öffnen
    int shm_fd = shm_open(SHM_NAME, O_RDWR, 0666);
    if (shm_fd < 0) { 
        perror("shm_open (Starte zuerst den Producer!)"); 
        exit(1); 
    }

    GemeinsamerSpeicher *shm = mmap(NULL, sizeof(GemeinsamerSpeicher), 
                                    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 GESTARTET ===\n");

    while (1) {
        // Warten, bis ein Datensatz bereitliegt
        sem_wait(sem_items);
        sem_wait(sem_mutex);

        if (shm->ist_beendet) {
            printf("[Consumer] Beendigungs-Signal empfangen.\n");
            sem_post(sem_mutex);
            break;
        }

        // Zero-Copy-Auslesen direkt aus dem RAM
        printf("📊 [Consumer EMPFANGEN] Sensor: %u | Temp: %.2f C | Status: %s\n",
               shm->paket.sensor_id,
               shm->paket.temperatur,
               shm->paket.status_label);

        sem_post(sem_mutex);
    }

    // Aufräumen und Shared-Memory-Dateien aus /dev/shm entfernen
    munmap(shm, sizeof(GemeinsamerSpeicher));
    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("Shared Memory und Semaphoren sauber aus /dev/shm entfernt.\n");
    return 0;
}
IPC-Labor kompilieren und ausführen
# Mit -lrt (Real-Time Library für shm) und -pthread kompilieren
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: Consumer starten (wartet auf Daten)
./ipc_consumer

# Terminal 2: Producer starten
./ipc_producer

Nächste Schritte & Empfehlungen

Setze deinen Weg durch das C-Curriculum fort:

  • Teil 3.3: High-Performance-Netzwerk & epoll


    Nicht-blockierende Berkeley-Sockets, C10K I/O-Multiplexing, Edge-Triggered epoll und Zero-Copy sendfile meistern.

    Zum epoll-Netzwerk-Guide

  • Teil 2.3: Debugging, Sanitizer & Profiling


    GDB Core Dumps, AddressSanitizer (ASan), Valgrind Massif und Compiler-Härtungsflags vertiefen.

    Zum Debugging-Guide

  • Linux Prozessverwaltung & Syscalls


    Laufende ELF-Prozesse, Signale (SIGTERM/SIGKILL), strace-Syscall-Traces und /proc-Dateisysteme analysieren.

    Zum Prozessmanagement