IPC – Lernzettel für den Praxistest
Was du können solltest
C-Programme mit mehreren Prozessen schreiben: Prozesse erzeugen, Daten austauschen, Signale behandeln und Ressourcen sauber freigeben.
Die sechs ausführlich behandelten Bereiche sind fork/Warten, Pipes, FIFOs, Message Queues, Shared Memory und Signale. Sockets werden in der allgemeinen IPC-Liste genannt, aber in den verlinkten Materialien nicht ausführlich behandelt. Die genaue Testabgrenzung ist dort nicht angegeben.
Übersicht
| Bereich | Zweck | Wichtigste Funktionen |
|---|---|---|
| Prozesse | Prozesse erzeugen und auf Kinder warten | fork, wait, waitpid |
| Pipe | Bytestrom zwischen typischerweise verwandten Prozessen | pipe, read, write, close |
| FIFO | Benannter Bytestrom, auch für unabhängig gestartete Prozesse | mkfifo, open, read, write, unlink |
| Message Queue | Einzelne Nachrichten puffern und nach Typ empfangen | msgget, msgsnd, msgrcv, msgctl |
| Shared Memory | Gemeinsamer Speicher für mehrere Prozesse | shmget, shmat, shmdt, shmctl |
| Signale | Ereignisse melden und Prozesse steuern | kill, raise, signal, alarm, setitimer |
Zu den Codebeispielen
Die kurzen Sammlungen sind Merkhilfen und keine vollständigen Programme. Rückgabewerte und Fehlerbehandlung musst du im eigenen Programm ergänzen. Die Funktionen gehören zur Linux-/Unix-Programmierung in C.
1. Prozesse: fork, wait und waitpid
Das musst du verstehen
fork()erzeugt einen Kindprozess. Beide Prozesse laufen ab der nächsten Anweisung weiter.- Eltern- und Kindprozess haben getrennte Adressräume. Normale Variablen werden kopiert; Änderungen sind nicht automatisch gemeinsam sichtbar.
- Der Elternprozess muss beendete Kinder mit
wait()oderwaitpid()einsammeln, damit keine Zombieprozesse zurückbleiben. - Für
Parent → Child → Childmuss das erste Kind das zweite Kind erzeugen.
Befehlssammlung
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
pid_t pid = fork();
// -1: Fehler
// 0: Dieser Code läuft im Kind
// >0: Dieser Code läuft im Elternprozess; Wert = Kind-PID
getpid(); // Eigene PID
getppid(); // PID des Elternprozesses
exit(42); // Prozess mit Exit-Code 42 beenden
int status;
wait(&status); // Auf irgendein Kind warten
waitpid(pid, &status, 0); // Auf bestimmtes Kind warten
if (WIFEXITED(status)) {
int code = WEXITSTATUS(status);
}Nicht blockierend prüfen:
pid_t r = waitpid(pid, &status, WNOHANG);
// r == 0: Noch kein passender Kindstatus verfügbar
// r > 0: Status eines Kindes erhalten
// r == -1: FehlerGrundmuster
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
} else if (pid == 0) {
// Arbeit des Kindes
exit(0);
} else {
// Arbeit des Elternprozesses
waitpid(pid, NULL, 0);
}Typischer Fehler
Ein Kind läuft versehentlich in einen weiteren
fork()hinein. Dadurch entstehen mehr Prozesse als geplant. Prozesszweige und deren Ende bewusst festlegen.
2. Anonyme Pipes
Eine Pipe überträgt einen Bytestrom in eine Richtung. Typisch: Elternprozess und Kindprozess.
Das musst du können
- Pipe vor
fork()erstellen, damit die Deskriptoren vererbt werden. fd[0]zum Lesen undfd[1]zum Schreiben verwenden.- In jedem Prozess alle unbenutzten Pipe-Enden schließen.
- Für Hin- und Rückweg zwei Pipes verwenden.
Befehlssammlung
#include <unistd.h>
#include <string.h>
int fd[2];
pipe(fd); // Vor fork()!
// Schreiber:
close(fd[0]);
write(fd[1], "Hallo", strlen("Hallo") + 1);
close(fd[1]);
// Leser in einem anderen Prozess:
close(fd[1]);
char buf[100];
ssize_t n = read(fd[0], buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
}
close(fd[0]);Unbedingt merken
Ergebnis von read() | Bedeutung |
|---|---|
> 0 | Anzahl gelesener Bytes |
0 | EOF: keine Daten mehr und alle Schreibenden geschlossen |
-1 | Fehler |
- Eine leere Pipe mit noch geöffnetem Schreibende lässt
read()normalerweise warten. - EOF kommt erst, wenn alle Schreibenden geschlossen sind, auch geerbte Deskriptoren in anderen Prozessen.
- Schreiben ohne Leser kann
SIGPIPEauslösen; wird es ignoriert, meldetwrite()normalerweiseEPIPE. - Ein
read()entspricht nicht automatisch einem vollständigenwrite(). - Nachrichten durch feste Größen, Längenfelder oder Trennzeichen abgrenzen. Auch Teilübertragungen berücksichtigen.
- Bei gegenseitigem Warten kann ein Deadlock entstehen.
Merksatz
0 liest, 1 schreibt. Unbenutzte Enden schließen!
3. FIFOs – benannte Pipes
Eine FIFO hat einen Pfad im Dateisystem. Damit können auch unabhängig gestartete Programme kommunizieren. Die Daten sind weiterhin ein Bytestrom und werden nicht dauerhaft in der FIFO-Datei gespeichert.
Befehlssammlung
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
mkfifo("/tmp/meine_fifo", 0666);
// Im Sender:
int fd = open("/tmp/meine_fifo", O_WRONLY);
// Alternativ im Empfänger:
int fd = open("/tmp/meine_fifo", O_RDONLY);
write(fd, daten, anzahl);
read(fd, puffer, groesse);
close(fd);
unlink("/tmp/meine_fifo");Die beiden open()-Zeilen sind Alternativen für getrennte Prozesse.
Unbedingt merken
- Beide Programme verwenden denselben Pfad.
- Ohne besondere Flags wartet
open()normalerweise auf die Gegenseite. close()schließt den Zugriff;unlink()entfernt den Pfad.- Existiert die FIFO bereits, liefert
mkfifo()einen Fehler miterrno == EEXIST. Diesen Fall beim Neustart berücksichtigen. 0666bedeutet Lese-/Schreibrechte für alle; die tatsächlichen Rechte werden durch dieumaskeingeschränkt.
Terminal
mkfifo /tmp/meine_fifo
# Sender und Empfänger in getrennten Terminals:
echo "Hallo" > /tmp/meine_fifo
cat /tmp/meine_fifo
rm /tmp/meine_fifo4. Message Queues – Nachrichtenwarteschlangen
Eure Beispiele verwenden System V Message Queues. Der Kernel puffert einzelne Nachrichten, bis sie empfangen werden. Nachrichtengrenzen bleiben erhalten. Sender und Empfänger müssen nicht gleichzeitig laufen.
Ablauf merken
Schlüssel → Queue öffnen → Nachricht senden/empfangen → Queue entfernen
Befehlssammlung
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct message {
long mtype; // Erstes Feld! Wert > 0
char mtext[100];
};
key_t key = ftok("/tmp/ipc_key", 'A');
int id = msgget(key, IPC_CREAT | 0666);
struct message msg = {0};
msg.mtype = 1;
strcpy(msg.mtext, "Hallo");
msgsnd(id, &msg, strlen(msg.mtext) + 1, 0);
msgrcv(id, &msg, sizeof(msg.mtext), 1, 0);
msgctl(id, IPC_RMID, NULL);Parameter und Flags
| Ausdruck | Bedeutung |
|---|---|
ftok(pfad, projekt_id) | Schlüssel aus existierendem Dateipfad und Projekt-ID ableiten |
IPC_CREAT | Ressource erstellen, falls sie noch nicht existiert |
IPC_CREAT \| IPC_EXCL | Fehler, falls Ressource schon existiert |
Empfangstyp 0 | Erste Nachricht unabhängig vom Typ |
Empfangstyp > 0 | Erste Nachricht mit genau diesem Typ |
IPC_NOWAIT | Nicht auf verfügbare Nachricht bzw. freien Platz warten |
MSG_NOERROR | Zu große empfangene Nachricht abschneiden |
IPC_RMID | Queue entfernen |
Unbedingt merken
- Die Datei für
ftok()muss existieren.ftok()erzeugt sie nicht und garantiert keine absolut eindeutigen Schlüssel. - Beide Programme brauchen denselben Schlüssel und eine übereinstimmende Nachrichtenstruktur.
- Die Größenangabe enthält nur die Nutzdaten, ohne
long mtype! msgrcv()entfernt die empfangene Nachricht aus der Queue.- Ohne
IPC_NOWAITblockiert der Empfang, wenn keine passende Nachricht vorhanden ist. msgsnd()kann bei voller Queue ebenfalls blockieren.- Empfangene Bytes sind nicht automatisch ein nullterminierter C-String. Bei Text das Übertragungsformat und Puffergrenzen beachten.
- Die Queue bleibt nach Prozessende bestehen, bis sie entfernt wird oder das System neu startet.
Terminal
touch /tmp/ipc_key
ipcs -q # Queues anzeigen
ipcrm -q 123 # Queue mit ID 123 entfernen5. Shared Memory – gemeinsamer Speicher
Mehrere Prozesse greifen auf denselben Speicherbereich zu. Nach dem Anhängen erfolgt der Datenzugriff direkt über einen Zeiger.
Ablauf auswendig können
ftok → shmget → shmat → Datenzugriff → shmdt → shmctl
Befehlssammlung
#include <sys/ipc.h>
#include <sys/shm.h>
struct SharedData {
int messwert;
int status;
};
key_t key = ftok("/tmp/ipc_key", 'B');
int id = shmget(key, sizeof(struct SharedData),
IPC_CREAT | 0666);
struct SharedData *p = shmat(id, NULL, 0);
if (p == (void *) -1) {
// Fehler behandeln
}
// Zugriff nur nach erfolgreichem shmat():
p->messwert = 42;
int wert = p->messwert;
shmdt(p);
shmctl(id, IPC_RMID, NULL);Unbedingt merken
shmget()liefert eine ID;shmat()liefert einen Zeiger.- Fehlerwert von
shmat():(void *) -1, nichtNULL. shmdt()trennt nur den aktuellen Prozess vom Segment.shmctl(..., IPC_RMID, ...)markiert das Segment zum Entfernen. Endgültig verschwindet es nach dem letzten Abtrennen.- Gemeinsamer Speicher synchronisiert Zugriffe nicht automatisch: Race Conditions sind möglich.
- Das Statusmodell aus euren Beispielen ist
0 = leer,1 = Daten vorhanden,2 = beenden. - Ein normales Statusflag und
sleep()ersetzen keine zuverlässige Synchronisation mit Semaphoren oder prozessübergreifend eingerichteten Mutexes. Auchvolatileallein löst das nicht.
Terminal
ipcs -m # Segmente anzeigen
ipcrm -m 123 # Segment mit ID 123 entfernenPOSIX-Alternative
Eure Unterlagen erwähnen zusätzlich
shm_open(),ftruncate(),mmap(),munmap()undshm_unlink(). Die ausführlichen Beispiele verwenden aber System V.
6. Signale und Timer
Signale melden asynchrone Ereignisse und steuern Prozesse. Für umfangreiche Daten nutzt du eine andere IPC-Methode.
Wichtige Signale
| Signal | Bedeutung |
|---|---|
SIGINT | Unterbrechung, normalerweise Strg+C |
SIGTERM | Aufforderung zum Beenden; behandelbar |
SIGKILL | Sofort beenden; nicht abfangbar oder ignorierbar |
SIGUSR1, SIGUSR2 | Eigene Bedeutung im Programm |
SIGCHLD | Statusänderung eines Kindes |
SIGPIPE | Schreiben in Pipe ohne Leser |
SIGALRM | Ablauf eines Alarms |
SIGSTOP | Stoppen; nicht abfangbar oder ignorierbar |
SIGTSTP | Stoppsignal des Terminals, normalerweise Strg+Z |
SIGCONT | Gestoppten Prozess fortsetzen |
SIGPROF | Ablauf des Profiling-Timers |
Befehlssammlung
#include <signal.h>
#include <unistd.h>
kill(pid, SIGUSR1); // Signal an anderen Prozess
raise(SIGUSR1); // Signal an sich selbst
signal(SIGUSR1, handler); // Eigenen Handler registrieren
signal(SIGINT, SIG_IGN); // Ignorieren
signal(SIGINT, SIG_DFL); // Standardverhalten herstellen
alarm(5); // SIGALRM nach 5 Sekunden
alarm(0); // Alarm abbrechenEinfaches Handler-Muster
volatile sig_atomic_t stop = 0;
void handler(int sig) {
stop = 1;
}
// Im Hauptprogramm:
if (signal(SIGTERM, handler) == SIG_ERR) {
// Fehler behandeln
}
while (!stop) {
// Arbeit erledigen
}
// Hier aufräumen und Kinder einsammelnUnbedingt merken
- Im Handler möglichst nur ein Flag setzen. Ausgabe und Aufräumen im normalen Programmablauf erledigen.
printf()ist nicht async-signal-safe.write()ist geeignet.SIGKILLundSIGSTOPkönnen weder abgefangen noch ignoriert werden.- Normale Signale sind keine zuverlässigen Nachrichtenzähler: Mehrere gleiche anhängige Signale können zusammenfallen.
SIGUSR1undSIGUSR2beenden ohne eigenen Handler standardmäßig den Prozess.- Blockierende Systemaufrufe können durch Signale unterbrochen werden; gegebenenfalls
errno == EINTRbehandeln. - Für robustes Registrieren von Handlern ist
sigaction()üblich; eure Beispiele verwendensignal().
Korrektur zu den Unterrichtsunterlagen
Strg+Z sendet normalerweise
SIGTSTP, nichtSIGSTOP. Die Forderung der Übung, alle anderen Signale zu ignorieren, ist wegenSIGKILLundSIGSTOPnur eingeschränkt umsetzbar.
Intervalltimer
#include <sys/time.h>
struct itimerval t = {0};
t.it_value.tv_usec = 10000; // Erster Ablauf: 10 ms
t.it_interval.tv_usec = 10000; // Wiederholung: 10 ms
setitimer(ITIMER_PROF, &t, NULL);it_value: Zeit bis zum ersten Ablauf.it_interval: Wiederholungsintervall; null bedeutet einmalig.ITIMER_PROF: verbrauchte Benutzer- und System-CPU-Zeit, SignalSIGPROF.- CPU-Zeit ist kein fester Takt nach tatsächlicher Uhrzeit.
- Vorher passenden Handler installieren, wenn das Signal den Prozess nicht beenden soll.
Terminal
kill -l
kill -USR1 1234
kill -USR2 1234
kill -TERM 1234
kill -KILL 12347. Praktische Aufgaben aus euren Materialien
Produktionsstraße
Struktur: Produkt-Generator → Qualitätskontrolle → Endmontage/Versand
- Prozesskette
Parent → Child → Childkorrekt erzeugen. - Produkt-IDs mit einer geeigneten IPC-Methode weiterleiten.
- Beispielsweise zwei Pipes für die zwei Übertragungsstrecken verwenden.
- Alle unbenutzten Pipe-Enden in jedem Prozess schließen.
- Aktionen und Verarbeitung nachvollziehbar ausgeben.
- Das Ende der Übertragung festlegen, etwa durch EOF oder Endnachricht.
- Beendigung wie gefordert signalisieren und Signalwahl begründen.
- Jeder Elternprozess wartet auf sein direktes Kind.
- Wahl der IPC-Methode kurz im Quellcode begründen.
Sensorüberwachung
- Hauptprozess initialisiert Shared Memory.
- Mehrere Sensorprozesse erzeugen oder getrennt starten.
- Sensoren und Anzeige hängen das Segment an.
- Messwerte und Status in einer gemeinsamen Struktur speichern.
- Kritische Werte mit
kill(anzeige_pid, SIGUSR1)oderSIGUSR2melden. - Anzeige setzt im Handler ein Flag und verarbeitet die Warnung außerhalb des Handlers.
- Zugriffe synchronisieren und am Ende Speicher freigeben.
Alive-Kommunikation
- Eltern- und Kindprozess senden abwechselnd alle zwei Sekunden eine Alive-Nachricht.
- Zwei Pipes für Kommunikation in beide Richtungen verwenden; alternativ FIFO/Message Queue.
-
SIGUSR1beendet den Elternprozess kontrolliert. -
SIGUSR2beendet den Kindprozess kontrolliert. - Ausfall der Gegenseite erkennen, etwa durch EOF oder
SIGPIPE/EPIPE. - Verbleibenden Prozess ebenfalls sauber beenden.
8. Terminal und Fehlersuche
# Kompilieren und starten
gcc -Wall -Wextra -g programm.c -o programm
./programm
# Prozesse ansehen
ps -f
pstree -p
# IPC-Ressourcen ansehen
ipcs -q
ipcs -m
# Dokumentation
man 2 fork
man 2 waitpid
man 2 pipe
man 3 mkfifo
man 2 msgsnd
man 2 shmat
man 2 sigaction
man 7 signal
man 7 signal-safetyFehlerbehandlung
#include <stdio.h>
#include <errno.h>
if (/* Systemaufruf */ == -1) {
perror("Funktionsname");
// Fehler behandeln, gegebenenfalls Ressourcen freigeben
}| Funktion | Fehlerwert |
|---|---|
fork, pipe, open, read, write | -1 |
wait, waitpid | -1 |
ftok | (key_t)-1 |
msgget, msgsnd, msgrcv, msgctl | -1 |
shmget, shmdt, shmctl | -1 |
shmat | (void *)-1 |
signal | SIG_ERR |
kill, setitimer | -1 |
9. Letzte Lerncheckliste
- Ich kann die drei Rückgabefälle von
fork()erklären. - Ich kann einen vorgegebenen Prozessbaum zeichnen und programmieren.
- Ich kann mit
waitpid()auf Kinder warten und Exit-Codes auswerten. - Ich weiß:
fd[0]liest,fd[1]schreibt. - Ich weiß, warum unbenutzte Pipe-Enden geschlossen werden müssen.
- Ich kann EOF und Kommunikationsfehler erkennen.
- Ich kann Pipe und FIFO unterscheiden.
- Ich kenne den vollständigen Ablauf einer Message Queue.
- Ich weiß: Nachrichtengröße ohne
mtypeangeben. - Ich kenne den vollständigen Ablauf von Shared Memory.
- Ich kann
shmdt()undIPC_RMIDunterscheiden. - Ich kann Signale senden und einen einfachen Handler schreiben.
- Ich weiß, welche Signale nicht abgefangen werden können.
- Ich kann alle Ressourcen am Ende sauber freigeben.
Wichtigste Reihenfolge bei jeder Aufgabe
Rückgabewerte prüfen → Prozesse richtig verzweigen → Daten übertragen → Ende erkennen → Ressourcen schließen/entfernen → Kinder einsammeln.