Technologie und Planung Test 22.10.2026

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

  1. Teil 1–3 durchlesen und die Syntaxkästen auswendig können (Funktionsnamen, Parameter, Header)
  2. Teil 4 (typische Fehler) genau anschauen
  3. Teil 5 selbst programmieren, kompilieren und laufen lassen, erst dann die Lösung aufklappen
  4. 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.


Teil 1 – Prozesse und IPC

1.1 Prozess erzeugen: fork()

#include <unistd.h>
#include <sys/wait.h>
 
pid_t pid = fork();
if (pid == -1) { perror("fork"); exit(1); }   // Fehler
else if (pid == 0) { /* Kind */ exit(0); }      // Kind: fork() liefert 0
else { /* Eltern: pid = PID des Kindes */ wait(NULL); }
  • 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() → 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

MethodeRichtungFür wen?Wichtige FunktionenWann wählen?
Anonyme Pipeunidirektionalnur verwandte Prozesse (fork)pipe, read, write, closeDatenstrom Eltern → Kind (Produktionsstraße)
FIFO (Named Pipe)unidirektionalauch fremde Prozesse (Datei im Dateisystem)mkfifo, open, read, write, unlinkzwei getrennt gestartete Programme
Message Queuebeliebig, nachrichtenweisebeliebige Prozessemsgget, msgsnd, msgrcv, msgctleinzelne Nachrichten mit Typ, Reihenfolge bleibt
Shared Memorygemeinsamer Speicherbeliebige Prozesseshmget/shmat/shmdt/shmctl oder shm_open/mmapschnellste Methode, große Datenmengen, aktueller Zustand (Sensorwerte)
SignaleBenachrichtigung, keine DatenProzess mit bekannter PIDsignal, kill, pauseEreignis 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.

1.3 Anonyme Pipe

int fd[2];            // fd[0] = Leseende, fd[1] = Schreibende
pipe(fd);             // VOR fork() anlegen!
pid_t pid = fork();
if (pid == 0) {       // Kind liest
    close(fd[1]);
    while (read(fd[0], buf, sizeof(buf)) > 0) { ... }
    close(fd[0]);
    exit(0);
}
close(fd[0]);         // Eltern schreibt
write(fd[1], daten, laenge);
close(fd[1]);         // -> Kind bekommt EOF, read() liefert 0
wait(NULL);
  • Merke: „0 = lesen, 1 = schreiben“ (wie stdin = 0, stdout = 1)
  • 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 anlegen
int fd = open("/tmp/mein_fifo", O_WRONLY);   // bzw. O_RDONLY beim Leser
write(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 mtype
msgrcv(qid, &m, sizeof(m.text), 1, 0);             // 1 = nur Nachrichten vom Typ 1, 0 = beliebiger Typ
msgctl(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.

1.6 Shared Memory

System V (wie in sensorueberwachung.c):

#include <sys/ipc.h>
#include <sys/shm.h>
 
key_t key = ftok("/tmp", 'S');                         // Schlüssel aus Pfad + Projekt-ID
int id = shmget(key, sizeof(SharedData), IPC_CREAT | 0666);
SharedData *p = (SharedData *)shmat(id, NULL, 0);      // einblenden
if (p == (SharedData *)-1) { perror("shmat"); exit(1); }
p->werte[0] = 42;
shmdt(p);                                              // ausblenden
shmctl(id, IPC_RMID, NULL);                            // löschen (einmal, am Ende)

POSIX (wie in write.c / read.c):

#include <sys/mman.h>
#include <fcntl.h>
 
int fd = shm_open("/mein_shm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, SIZE);                                   // Größe festlegen!
char *mem = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
strcpy(mem, "Hallo");
munmap(mem, SIZE); close(fd);
shm_unlink("/mein_shm");                               // löschen
  • 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 erbt
kill(pid, SIGUSR1);          // Signal an Prozess pid senden
pause();                     // schlafen, bis ein Signal kommt
SignalBedeutungabfangbar?
SIGUSR1, SIGUSR2frei für eigene Zweckeja
SIGINTStrg+Cja
SIGTERMhöflich beenden (Standard bei kill)ja
SIGKILLsofort beendennein
SIGSTOPanhaltennein
SIGCHLDein Kind hat sich beendetja
  • 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)
Adressraumeigener (Kopie)gemeinsamer
Globale Variablengetrenntgeteilt
Eigener Stack / Befehlszählerjaja (jeder Thread)
Code, Daten, Heap, offene DateienKopiegeteilt
Kommunikationbraucht IPCeinfach über gemeinsame Variablen
Absturznur der Prozessreiß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).

2.2 pthread-Funktionen

#include <pthread.h>    // kompilieren mit:  gcc datei.c -o datei -pthread
 
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*funktion)(void *), void *arg);   // attr = NULL -> Standard
void pthread_exit(void *wert);                  // Thread beenden (= return wert;)
int pthread_join(pthread_t thread, void **ret); // warten (wie waitpid bei Prozessen)
int pthread_detach(pthread_t thread);           // abhängen, kein join mehr möglich
pthread_t pthread_self(void);                   // eigene Thread-ID
  • 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ückgabe
typedef 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.

2.4 Globale und lokale Variablen (Übung 2)

Ausgabe von Übung 2 (getestet)

[Main] global: 5 local: 3
[Thread] global: 5 local: 3
[Thread] global: 6 local: 4
[Main] global: 6 local: 3

Die globale Variable ist geteilt → Main sieht die 6. Die lokale wurde als Wert kopiert → Main hat weiterhin 3.


Teil 3 – Synchronisation

3.1 Race Condition und kritischer Abschnitt

  • counter++ ist nicht atomar: lesen → erhöhen → zurückschreiben. Zwei Threads können sich dazwischen drängen → Erhöhungen gehen verloren.
  • Kritischer Abschnitt = Codeteil, in dem auf gemeinsame Daten zugegriffen wird; dort darf immer nur einer gleichzeitig sein.

Getestet: 2 Threads × 100 000 counter++ ohne Mutex

Ergebnisse von 5 Läufen: 100 000, 100 122, 100 102, 100 000, 100 000 statt 200 000.

3.2 Mutex (gegenseitiger Ausschluss)

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
 
pthread_mutex_lock(&mutex);
counter++;                    // kritischer Abschnitt
pthread_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!).

3.3 Semaphore

#include <semaphore.h>
 
sem_t sem;
sem_init(&sem, 0, 3);   // 2. Param: 0 = zwischen Threads, 1 = zwischen Prozessen (im Shared Memory!)
                        // 3. Param: Startwert = Anzahl freier Ressourcen
sem_wait(&sem);         // P: Zähler -1, blockiert bei 0
sem_post(&sem);         // V: Zähler +1, weckt einen Wartenden
sem_destroy(&sem);
  • 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) vor fork().

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 mutex2
Thread 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() nach fork() 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)
  • Rückgabewerte nicht geprüft: fork() == -1, pipe() == -1, shmat() == (void *)-1, mmap() == MAP_FAILED → immer perror() + exit(1)
  • 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 nach fork() 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?

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.

Aufgabe 3 – FIFO zwischen zwei Programmen

Schreibe writer.c und reader.c, die über die FIFO /tmp/mein_fifo einen Text austauschen. Warum geht das hier nicht mit pipe()?

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.

Aufgabe 5 – Shared Memory mit fork

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?

Aufgabe 6 – Signal vom Kind an die Eltern

Das Kind wartet 1 s und schickt dann SIGUSR1 an den Elternprozess. Der Elternprozess wartet mit pause() und meldet den Empfang.

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.

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.

Aufgabe 9 – Race Condition erklären

Zwei Threads führen je 100 000-mal counter++ aus, ohne Mutex. Was wird ausgegeben und warum?

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?

Aufgabe 11 – Deadlock finden und beheben

void *thread1(void *a) { lock(&m1); sleep(1); lock(&m2); ... unlock(&m2); unlock(&m1); }
void *thread2(void *a) { lock(&m2); sleep(1); lock(&m1); ... unlock(&m1); unlock(&m2); }

a) Was passiert? b) Wie behebst du es?

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?

Aufgabe 13 – Mini-Produktionsstraße (Prüfungsformat)

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.

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).


Teil 6 – Spickzettel (zum Auswendiglernen)

ThemaSyntax
Kompilierengcc datei.c -o datei -pthread
Prozesspid_t pid = fork(); → 0 Kind, >0 Eltern, −1 Fehler
Wartenwait(NULL); · waitpid(pid, NULL, 0);
IDsgetpid(), getppid(), pthread_self()
Pipeint fd[2]; pipe(fd); → fd[0] lesen, fd[1] schreiben, unbenutztes Ende close
FIFOmkfifo(pfad, 0666); open(pfad, O_WRONLY/O_RDONLY); unlink(pfad);
Message Queuemsgget(key, IPC_CREAT\|0666); msgsnd(q, &m, sizeof(m.text), 0); msgrcv(q, &m, sizeof(m.text), typ, 0); msgctl(q, IPC_RMID, NULL);
SHM System Vftok("/tmp",'S'); shmget(key, size, IPC_CREAT\|0666); shmat(id, NULL, 0); shmdt(p); shmctl(id, IPC_RMID, NULL);
SHM POSIXshm_open(name, O_CREAT\|O_RDWR, 0666); ftruncate(fd, size); mmap(NULL, size, PROT_READ\|PROT_WRITE, MAP_SHARED, fd, 0); munmap; shm_unlink(name);
Signalsignal(SIGUSR1, handler); kill(pid, SIGUSR1); pause(); Flag: volatile sig_atomic_t
Threadpthread_create(&t, NULL, f, arg); pthread_join(t, NULL); · void *f(void *arg)
Thread beendenpthread_exit(NULL); / return NULL; · pthread_detach(t);
Mutexpthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; lock; unlock; destroy;
Semaphorsem_init(&s, 0/1, n); sem_wait(&s); sem_post(&s); sem_destroy(&s);
Deadlockgegenseitiges Warten → immer gleiche Sperrreihenfolge
Headerunistd.h (fork, pipe, read, write), sys/wait.h, signal.h, pthread.h, semaphore.h, sys/ipc.h + sys/shm.h/sys/msg.h, sys/mman.h + fcntl.h, sys/stat.h (mkfifo)