Stoff laut Register: Praktische Leistungsüberprüfung · Praxistest IPC
Quellen: Classroom 2026/27 4AT TP, Thema „IPC und Nebenläufige Programmierung“: Material „IPC (Wiederholung)“ (fork, Pipe, FIFO, Message Queues, Shared Memory, Signale), Übungsblatt docs.fallout.school/4at (Sensorüberwachung, Produktionsstraße), „Folien und Übungen zu Threads in C“ (Threads in C.pdf, mutex.c, semaphore.c, deadlock.c, deadlockSolved.c, deadlockSharedMem.c, deadlockSharedMemorySolved.c); Obsidian Prozessumgebung, Übung 0, Übung 1, Übung 2; lokale Skripte in Technologie und Planung/Skripte (shared_memory, Threads, Sensorueberwachung, Produktionsstrasse).
So lernst du damit
Teil 1–3 durchlesen und die Syntaxkästen auswendig können (Funktionsnamen, Parameter, Header)
Teil 4 (typische Fehler) genau anschauen
Teil 5 selbst programmieren, kompilieren und laufen lassen, erst dann die Lösung aufklappen
Am Schluss den Spickzettel (Teil 6) ohne Hilfe nachschreiben
Es ist ein Praxistest
Du musst Code schreiben, der kompiliert und läuft. Die Aufgaben auf docs.fallout.school (Sensorüberwachung, Produktionsstraße) zeigen, wie so eine Aufgabe aussieht: IPC-Methode wählen, im Kommentar begründen, jeder Prozess protokolliert seine Aktionen und beendet sich sauber.
fork()kopiert den Prozess. Ab der Stelle laufen beide weiter.
Rückgabe: 0 im Kind, PID des Kindes im Elternprozess, −1 bei Fehler
Nach fork() hat jeder Prozess eigene Kopien aller Variablen → Änderungen sieht der andere nicht (darum braucht man IPC).
getpid() = eigene PID, getppid() = PID des Elternprozesses
wait(NULL) wartet auf irgendein Kind, waitpid(pid, NULL, 0) auf ein bestimmtes
Ohne wait() → beendetes Kind bleibt als Zombie stehen
Stirbt der Elternprozess zuerst → Kind wird Waise und von init/systemd adoptiert
n aufeinanderfolgende fork() → 2n Prozesse
Ausgabe vor fork()
Steht ein printf ohne \n/fflush(stdout) vor fork(), liegt der Text noch im Puffer und wird doppelt ausgegeben. Deshalb in deinem Sensorüberwachungs-Code: fflush(stdout); vor fork().
1.2 IPC-Methoden im Überblick
Methode
Richtung
Für wen?
Wichtige Funktionen
Wann wählen?
Anonyme Pipe
unidirektional
nur verwandte Prozesse (fork)
pipe, read, write, close
Datenstrom Eltern → Kind (Produktionsstraße)
FIFO (Named Pipe)
unidirektional
auch fremde Prozesse (Datei im Dateisystem)
mkfifo, open, read, write, unlink
zwei getrennt gestartete Programme
Message Queue
beliebig, nachrichtenweise
beliebige Prozesse
msgget, msgsnd, msgrcv, msgctl
einzelne Nachrichten mit Typ, Reihenfolge bleibt
Shared Memory
gemeinsamer Speicher
beliebige Prozesse
shmget/shmat/shmdt/shmctl oder shm_open/mmap
schnellste Methode, große Datenmengen, aktueller Zustand (Sensorwerte)
Signale
Benachrichtigung, keine Daten
Prozess mit bekannter PID
signal, kill, pause
Ereignis melden (Warnung, Ende)
Begründung im Kommentar (so wie in deiner produktionsstrasse.c)
// Anonyme Pipes, weil die Prozesse verwandt sind (Parent -> Child -> Child) und die Daten nur in eine Richtung fließen. // SIGUSR1, weil es ein benutzerdefiniertes Signal ist und den Prozess nicht wie SIGTERM/SIGKILL beendet.
Jeder Prozess schließt das Ende, das er nicht braucht.
read() liefert 0 (EOF) erst, wenn alle Schreibenden geschlossen sind → vergessenes close(fd[1]) = Leser hängt für immer.
Kette mit 3 Prozessen (Produktionsstraße): pipe1 vor dem ersten fork, pipe2 im Kind 1 vor dem zweiten fork.
1.4 FIFO (Named Pipe)
#include <sys/stat.h>#include <fcntl.h>mkfifo("/tmp/mein_fifo", 0666); // Datei vom Typ FIFO anlegenint fd = open("/tmp/mein_fifo", O_WRONLY); // bzw. O_RDONLY beim Leserwrite(fd, msg, strlen(msg) + 1);close(fd);unlink("/tmp/mein_fifo"); // am Ende löschen
open()blockiert, bis die Gegenseite die FIFO auch geöffnet hat.
Funktioniert zwischen zwei getrennt gestarteten Programmen (z. B. ./writer und ./reader in zwei Terminals).
1.5 Message Queue (System V)
#include <sys/ipc.h>#include <sys/msg.h>struct nachricht { long mtype; char text[64]; }; // mtype > 0, steht an erster Stelle!int qid = msgget(key, IPC_CREAT | 0666);msgsnd(qid, &m, sizeof(m.text), 0); // Größe OHNE mtypemsgrcv(qid, &m, sizeof(m.text), 1, 0); // 1 = nur Nachrichten vom Typ 1, 0 = beliebiger Typmsgctl(qid, IPC_RMID, NULL); // Queue löschen
Nachrichten bleiben einzeln getrennt (keine Bytes vermischt wie bei Pipes).
msgrcv blockiert, bis eine passende Nachricht da ist.
Shared Memory ist die schnellste IPC, hat aber keine eigene Synchronisation → gleichzeitiger Zugriff braucht Semaphore (siehe 3.3).
Daten als struct anlegen, z. B. typedef struct { int werte[3]; int status[3]; int ende; } SharedData;
Shared Memory nach fork(): Wurde vor dem fork() eingeblendet, sehen Eltern und Kind denselben Speicher.
1.7 Signale
#include <signal.h>volatile sig_atomic_t signal_caught = 0;static void handler(int sig) { signal_caught = sig; } // im Handler nur Flag setzen!signal(SIGUSR1, handler); // registrieren, VOR fork(), damit das Kind ihn erbtkill(pid, SIGUSR1); // Signal an Prozess pid sendenpause(); // schlafen, bis ein Signal kommt
Signal
Bedeutung
abfangbar?
SIGUSR1, SIGUSR2
frei für eigene Zwecke
ja
SIGINT
Strg+C
ja
SIGTERM
höflich beenden (Standard bei kill)
ja
SIGKILL
sofort beenden
nein
SIGSTOP
anhalten
nein
SIGCHLD
ein Kind hat sich beendet
ja
Signale übertragen keine Daten, nur „Ereignis X ist passiert“.
Flag-Variable muss volatile sig_atomic_t sein.
Die Nummer ist systemabhängig (SIGUSR1 = 10 unter Linux, 30 unter macOS) → immer den Namen verwenden.
Teil 2 – Threads
2.1 Thread vs. Prozess (Folien „Threads in C“)
Prozess (fork)
Thread (pthread_create)
Adressraum
eigener (Kopie)
gemeinsamer
Globale Variablen
getrennt
geteilt
Eigener Stack / Befehlszähler
ja
ja (jeder Thread)
Code, Daten, Heap, offene Dateien
Kopie
geteilt
Kommunikation
braucht IPC
einfach über gemeinsame Variablen
Absturz
nur der Prozess
reißt alle Threads mit (kein Speicherschutz)
Aufwand
„schwergewichtig“
„leichtgewichtig“
Jeder Prozess hat mindestens einen Thread, den Main-Thread.
exit() in irgendeinem Thread beendet den ganzen Prozess.
Endet main mit return, sterben auch alle anderen Threads (Übung 1: Endlos-Threads enden nach sleep(20) mit dem Main-Thread).
Thread-Funktion hat immer die Form void *name(void *arg).
Nach pthread_create läuft der neue Thread sofort asynchron los.
Ohne pthread_join wird ein beendeter Thread nicht freigegeben (wie ein Zombie) → jeden Thread joinen oder detachen.
2.3 Argumente übergeben
// Variante 1: Zahl direkt in den Zeiger packen (Übung 0, Übung 2)pthread_create(&t, NULL, f, (void *)(intptr_t)5);int x = (int)(intptr_t)arg;// Variante 2: Zeiger auf eigene Variable (Uebung_3.c)int ids[3] = {1, 2, 3};pthread_create(&t[i], NULL, f, &ids[i]); // jeder Thread eigenes Element!int id = *(int *)arg;// Variante 3: struct für mehrere Werte und Rückgabetypedef struct { long long von, bis, summe; } Bereich;
Nicht &i aus der Schleife übergeben
pthread_create(&t[i], NULL, f, &i); → alle Threads lesen dieselbe Variable i, die sich schon weiter verändert hat → falsche IDs.
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;pthread_mutex_lock(&mutex);counter++; // kritischer Abschnittpthread_mutex_unlock(&mutex);pthread_mutex_destroy(&mutex); // am Ende
Ein Mutex ist wie ein Schlüssel: nur wer ihn hat, darf rein. lock blockiert, bis er frei ist.
Wer sperrt, muss auch entsperren (auch vor break/return!).
Zählender Semaphor (Startwert n): höchstens n gleichzeitig (semaphore.c: 5 Threads, 3 Ressourcen → erst 3, nach 2 s die restlichen 2).
Binärer Semaphor (Startwert 1) wirkt wie ein Mutex, funktioniert aber auch zwischen Prozessen: Semaphor in den Shared Memory legen und sem_init(&shm->sem, 1, 1).
Shared Memory für Eltern und Kind ohne Schlüssel: mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0)vorfork().
macOS
sem_init ist auf macOS veraltet und schon das Kompilieren eines Testprogramms ist fehlgeschlagen. Im Test unter Linux funktioniert es normal. Mutex, Pipes, FIFO, Message Queues, Shared Memory und Signale laufen auch auf dem Mac.
3.4 Deadlock
Deadlock = zwei (oder mehr) Threads/Prozesse warten gegenseitig auf eine Ressource, die der andere hält → keiner kommt weiter, das Programm hängt.
Thread 1: lock(mutex1) ... wartet auf mutex2Thread 2: lock(mutex2) ... wartet auf mutex1 -> beide warten für immer
deadlock.c / deadlockSharedMem.c: verschiedene Sperrreihenfolge → Deadlock (getestet: Programm hängt nach 3 s noch).
Lösung (deadlockSolved.c, deadlockSharedMemorySolved.c): Alle sperren in derselben Reihenfolge (erst 1, dann 2), freigeben in umgekehrter Reihenfolge.
Weitere Gegenmittel: so wenige Sperren wie möglich, Sperren nicht lange halten, pthread_mutex_trylock.
Teil 4 – Typische Fehler
-pthread beim Kompilieren vergessen → Linker-Fehler
pipe()nachfork() aufgerufen → Eltern und Kind haben verschiedene Pipes
Nicht benötigte Pipe-Enden nicht geschlossen → read() bekommt nie EOF, Programm hängt
fd[0] und fd[1] verwechselt (0 = lesen, 1 = schreiben)
Kein wait() → Zombie-Prozesse; Elternprozess beendet sich vor dem Kind
Shared Memory / Message Queue / FIFO am Ende nicht gelöscht (shmctl IPC_RMID, msgctl IPC_RMID, unlink, shm_unlink)
ftruncate() bei shm_open vergessen → Speicher hat Größe 0
Signalhandler nachfork() registriert → Kind kennt ihn nicht; im Handler printf statt nur Flag setzen
Bei Message Queues sizeof(struct) statt sizeof(m.text) übergeben (mtype zählt nicht mit)
&i aus der Schleife an Threads übergeben
pthread_join vergessen → Main beendet sich, alle Threads sterben mit
Mutex gesperrt, aber vor break/return nicht entsperrt
Prüfen und Ändern nicht im selben Lock (z. B. if (z < 1000) außerhalb, z++ innerhalb) → Zähler läuft über
Unterschiedliche Sperrreihenfolge → Deadlock
printf vor fork() ohne fflush(stdout) → doppelte Ausgabe
Begründungs-Kommentare für IPC und Signal vergessen (werden in der Aufgabe verlangt)
Teil 5 – Übungsaufgaben
Die Lösungen zu Aufgabe 1–11 und 13 wurden kompiliert und ausgeführt (macOS, clang, -pthread). Die angegebenen Ausgaben stammen aus diesen Läufen. Aufgabe 12 (Semaphore) ist hergeleitet, weil sem_init auf macOS nicht läuft.
Aufgabe 1 – fork zählen
fork();fork();printf("Hallo von PID %d\n", getpid());
Wie oft wird „Hallo“ ausgegeben? Wie viele Prozesse gibt es?
Lösung
4-mal, es gibt 4 Prozesse (22): der erste fork macht 2, jeder der beiden ruft den zweiten fork auf → 4. Getestet: 4 Zeilen mit 4 verschiedenen PIDs.
Damit keine Zombies entstehen, am Ende while (wait(NULL) > 0);.
Aufgabe 2 – Pipe Eltern → Kind
Der Elternprozess schickt „Hallo Kind“ über eine Pipe an sein Kind, das Kind gibt die Nachricht aus. Beide beenden sich sauber.
Header: stdio.h, string.h, fcntl.h, unistd.h, sys/stat.h. Ausgabe: Gelesen: Hallo ueber FIFO. pipe() geht nicht, weil die beiden Programme nicht verwandt sind (kein gemeinsamer fork). Die FIFO hat einen Namen im Dateisystem, über den beide sie finden.
Aufgabe 4 – Message Queue
Der Elternprozess schickt drei Nachrichten („Nachricht 1“ bis „Nachricht 3“) vom Typ 1 über eine Message Queue an das Kind. Das Kind gibt sie aus, danach wird die Queue gelöscht.
Lösung
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/ipc.h>#include <sys/msg.h>#include <sys/wait.h>struct nachricht { long mtype; char text[64]; };int main(void) { int qid = msgget(IPC_PRIVATE, IPC_CREAT | 0666); if (qid == -1) { perror("msgget"); exit(1); } pid_t pid = fork(); if (pid == 0) { struct nachricht m; for (int i = 0; i < 3; i++) { msgrcv(qid, &m, sizeof(m.text), 1, 0); printf("[Kind] %s\n", m.text); } exit(0); } struct nachricht m = { .mtype = 1 }; for (int i = 1; i <= 3; i++) { snprintf(m.text, sizeof(m.text), "Nachricht %d", i); msgsnd(qid, &m, sizeof(m.text), 0); } wait(NULL); msgctl(qid, IPC_RMID, NULL); return 0;}
Lege mit shmget Speicher für ein int an. Das Kind schreibt 42 hinein, der Elternprozess gibt den Wert nach wait() aus. Was würde ohne Shared Memory (normale Variable) ausgegeben?
Ausgabe: [Kind] sende SIGUSR1 an <PID> · [Eltern] Signal 30 empfangen (macOS; unter Linux steht dort 10).
Die while-Schleife schützt davor, dass pause() wegen eines anderen Signals aufwacht.
Aufgabe 7 – Summe mit 6 Threads (Folie 13)
Summiere mit 6 Threads die Zahlen von 1 bis 600 000 000. Jeder Thread rechnet ein Intervall, der Main-Thread sammelt die Teilergebnisse.
Lösung
#include <stdio.h>#include <pthread.h>#define N 600000000LL#define T 6typedef struct { long long von, bis, summe; } Bereich;void *summiere(void *arg) { Bereich *b = (Bereich *)arg; b->summe = 0; for (long long i = b->von; i <= b->bis; i++) b->summe += i; return NULL;}int main(void) { pthread_t th[T]; Bereich b[T]; long long schritt = N / T; // 100 000 000 pro Thread for (int i = 0; i < T; i++) { b[i].von = i * schritt + 1; b[i].bis = (i + 1) * schritt; pthread_create(&th[i], NULL, summiere, &b[i]); } long long gesamt = 0; for (int i = 0; i < T; i++) { pthread_join(th[i], NULL); gesamt += b[i].summe; } printf("Summe: %lld\n", gesamt); return 0;}
Ausgabe: Summe: 180000000300000000
Kontrolle mit Gauß: 2n(n+1)=2600000000⋅600000001=180000000300000000 ✔ long long ist nötig, int (max. ≈ 2,1 Mrd.) würde überlaufen.
Aufgabe 8 – Gemeinsamer Zähler mit 3 Threads (Folie 14)
Drei Threads zählen einen gemeinsamen Zähler von 0 auf genau 1000 hoch. Nach jeder Änderung wird der Stand ausgegeben.
Lösung
int zaehler = 0;pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void *zaehle(void *arg) { int id = *(int *)arg; while (1) { pthread_mutex_lock(&mutex); if (zaehler >= 1000) { // Prüfen UND Erhöhen im selben Lock pthread_mutex_unlock(&mutex); break; } zaehler++; printf("Thread %d: %d\n", id, zaehler); pthread_mutex_unlock(&mutex); } return NULL;}int main(void) { pthread_t th[3]; int ids[3] = {1, 2, 3}; for (int i = 0; i < 3; i++) pthread_create(&th[i], NULL, zaehle, &ids[i]); for (int i = 0; i < 3; i++) pthread_join(th[i], NULL); printf("Endstand: %d\n", zaehler); pthread_mutex_destroy(&mutex); return 0;}
Getestet: genau 1000 Ausgabezeilen, Endstand: 1000. Welcher Thread welche Zahl erwischt, ist bei jedem Lauf anders.
Aufgabe 9 – Race Condition erklären
Zwei Threads führen je 100 000-mal counter++ aus, ohne Mutex. Was wird ausgegeben und warum?
Lösung
Erwartet wären 200 000, herauskommt aber ein zufälliger, meist kleinerer Wert (getestet: 100 000 bis 100 122). counter++ besteht aus Lesen, Erhöhen, Schreiben. Lesen beide Threads denselben alten Wert, schreiben beide denselben neuen Wert zurück → eine Erhöhung geht verloren.
Lösung: pthread_mutex_lock/unlock um counter++ (wie mutex.c) → immer 200 000.
Aufgabe 10 – Globale und lokale Variable (Übung 2)
Was gibt Übung 2 aus (global = 5, lokal = 3, Thread erhöht beide)? Sind die Werte gleich oder unabhängig?
Global: gemeinsam (gleicher Adressraum). Lokal: unabhängig, weil der Thread nur eine Kopie des Werts bekommt.
Mit fork() statt Thread wäre auch die globale im Elternprozess noch 5.
a) Deadlock: Thread 1 hält m1 und wartet auf m2, Thread 2 hält m2 und wartet auf m1 → beide warten für immer (getestet: Programm hängt).
b) Gleiche Sperrreihenfolge in beiden Threads: erst m1, dann m2 (so in deadlockSolved.c). Dasselbe gilt für Semaphore im Shared Memory zwischen Prozessen (deadlockSharedMemorySolved.c).
Aufgabe 12 – Semaphore
semaphore.c: sem_init(&s, 0, 3), 5 Threads führen sem_wait, sleep(2), sem_post aus. Wie läuft das ab? Wie lange dauert das Programm ungefähr?
Lösung
Drei Threads kommen sofort durch (Zähler 3 → 0), zwei warten. Nach ≈ 2 s geben die ersten frei, die zwei Wartenden kommen dran. Gesamtdauer ≈ 4 s (2 Durchgänge à 2 s).
Mit Startwert 1 würde der Semaphor wie ein Mutex wirken → ≈ 10 s.
Ein Erzeuger (Elternprozess) schickt die Quadratzahlen 1, 4, 9, 16, 25 an einen Verbraucher (Kind). Der Verbraucher gibt jede Zahl aus, bildet die Summe und meldet sich am Ende per Signal beim Erzeuger. IPC und Signal im Kommentar begründen, jeder Prozess protokolliert seine Aktionen.
Lösung
#include <stdio.h>#include <stdlib.h>#include <signal.h>#include <unistd.h>#include <sys/wait.h>// IPC: anonyme Pipe, weil Eltern und Kind verwandt sind und die Daten nur in eine Richtung fließen.// Signal: SIGUSR1, weil es frei für eigene Zwecke ist (SIGTERM/SIGKILL würden den Prozess beenden).volatile sig_atomic_t signal_caught = 0;static void handler(int sig) { signal_caught = sig; }int main(void) { int fd[2]; signal(SIGUSR1, handler); if (pipe(fd) == -1) { perror("pipe"); exit(1); } pid_t pid = fork(); if (pid == -1) { perror("fork"); exit(1); } if (pid == 0) { // Kind: Verbraucher close(fd[1]); int zahl, summe = 0; while (read(fd[0], &zahl, sizeof(zahl)) > 0) { printf("[Verbraucher %d] erhalten: %d\n", getpid(), zahl); summe += zahl; } close(fd[0]); printf("[Verbraucher %d] Summe = %d, sende SIGUSR1 und beende mich\n", getpid(), summe); kill(getppid(), SIGUSR1); exit(0); } close(fd[0]); // Eltern: Erzeuger for (int i = 1; i <= 5; i++) { int q = i * i; printf("[Erzeuger %d] sende %d\n", getpid(), q); write(fd[1], &q, sizeof(q)); } close(fd[1]); wait(NULL); if (signal_caught == SIGUSR1) printf("[Erzeuger] SIGUSR1 empfangen, Verbraucher fertig\n"); return 0;}
Ausgabe (Verbraucher): 1, 4, 9, 16, 25 und Summe = 55 (1+4+9+16+25=55 ✔), danach [Erzeuger] SIGUSR1 empfangen, Verbraucher fertig.
Aufgabe 14 – Wiederholung der Classroom-Aufgaben
Programmiere ohne Vorlage noch einmal Sensorüberwachung (Shared Memory + SIGUSR1/SIGUSR2) und Produktionsstraße (Parent → Child → Child mit 2 Pipes + SIGUSR1).
Lösung
Deine fertigen Lösungen liegen lokal in Technologie und Planung/Skripte/Sensorueberwachung/sensorueberwachung.c und .../Produktionsstrasse/produktionsstrasse.c. Checkliste zum Vergleichen:
Shared Memory / Pipes vor dem jeweiligen fork() angelegt
Signalhandler vorfork() registriert, Flag volatile sig_atomic_t
Nicht benötigte Pipe-Enden geschlossen, nach dem Senden close() → EOF
Jeder Prozess gibt [Name, PID] Aktion aus
Jeder Prozess meldet sein Ende (Signal) und beendet sich mit exit(0), Eltern wait()en
Shared Memory am Ende mit shmdt + shmctl(IPC_RMID) aufgeräumt
Begründung für IPC-Methode und Signal als Kommentar (1–2 Sätze)