Programming lesson
Cs 444/544 Programmierprojekte 1 bis 5: Eine Schritt-für-Schritt-Anleitung zu Fork, Exec und eigenem Heap-Manager
Lerne die Kernkonzepte der Systemprogrammierung in C: Prozessverwaltung mit fork() und exec(), Dateioperationen und die Implementierung eines eigenen Heap-Managers mit brk() und sbrk(). Perfekt für Studierende der Betriebssysteme.
Einführung in die Cs 444/544 Programmierprojekte
Die Programmierprojekte 1 bis 5 im Kurs Cs 444/544 vermitteln grundlegende Fähigkeiten in der C-Programmierung unter Unix/Linux. Du wirst dich mit fork(), exec()-Familie, Dateioperationen und einem eigenen Heap-Manager auseinandersetzen. Diese Konzepte sind essenziell für das Verständnis von Betriebssystemen und werden auch in modernen Anwendungen wie KI-gestützten Systemen oder Hochfrequenzhandelsplattformen genutzt, wo Prozessverwaltung und Speichereffizienz entscheidend sind.
Projekt 1: Makefile und erste Schritte
Ein korrektes Makefile ist die Grundlage. Es muss mit make clean und make all funktionieren. Platziere alle Quellcodedateien in einem Verzeichnis ohne Unterordner. Ein typisches Makefile sieht so aus:
CC = gcc
CFLAGS = -Wall -g
all: fork_demo file_demo order_demo exec_demo
fork_demo: fork_demo.c
\t$(CC) $(CFLAGS) -o fork_demo fork_demo.c
file_demo: file_demo.c
\t$(CC) $(CFLAGS) -o file_demo file_demo.c
order_demo: order_demo.c
\t$(CC) $(CFLAGS) -o order_demo order_demo.c
exec_demo: exec_demo.c
\t$(CC) $(CFLAGS) -o exec_demo exec_demo.c
clean:
\trm -f fork_demo file_demo order_demo exec_demoStelle sicher, dass alle Programme mit einem Befehl gebaut werden können. Das spart Zeit und vermeidet Fehler bei der Abgabe.
Projekt 2: fork() und Variablenvererbung
Rufe fork() auf und weise der Variable xx vor dem Fork den Wert 100 zu. Nach dem Fork ändern Eltern- und Kindprozess xx unabhängig voneinander. Der Kindprozess sieht zunächst den Wert 100, aber jede Änderung ist isoliert. Dieses Verhalten ist vergleichbar mit einem Chatbot, der für jeden Benutzer einen eigenen Kontext hat – globale Variablen werden nicht geteilt.
#include <stdio.h>
#include <unistd.h>
int main() {
int xx = 100;
pid_t pid = fork();
if (pid == 0) {
xx = 777;
printf("Kind: xx = %d\n", xx);
} else {
xx = 999;
printf("Eltern: xx = %d\n", xx);
}
return 0;
}Die Ausgabe zeigt, dass beide Prozesse unterschiedliche Werte sehen. Das liegt am Copy-on-Write-Mechanismus moderner Betriebssysteme.
Projekt 3: Dateioperationen mit fork()
Öffne die Datei JUNK.txt mit fopen(). Der Elternprozess schreibt „before fork“ hinein, dann wird geforkt. Beide Prozesse schreiben 10 Mal „parent“ bzw. „child“ in die Datei. Da sie den gleichen Dateideskriptor teilen, kann die Ausgabe durcheinandergeraten – ähnlich wie bei gleichzeitigen API-Aufrufen in einer Web-App. Kommentiere im Code, dass die Reihenfolge nicht garantiert ist.
#include <stdio.h>
#include <unistd.h>
int main() {
FILE *fp = fopen("JUNK.txt", "w");
fprintf(fp, "before fork\n");
pid_t pid = fork();
for (int i = 0; i < 10; i++) {
if (pid == 0)
fprintf(fp, "child\n");
else
fprintf(fp, "parent\n");
}
fclose(fp);
return 0;
}Projekt 4: Reihenfolge von Kind- und Elternprozess
Das Kind soll „hello“ und der Elternprozess „goodbye“ ausgeben, wobei das Kind immer zuerst dran ist. Ohne wait() oder eine große Schleife kann man sleep() im Elternprozess vermeiden, indem man pause() verwendet und dem Kind ein Signal schickt. Oder einfacher: Der Elternprozess ruft sleep(1) auf, was aber nicht elegant ist. Eine bessere Lösung ist die Verwendung von Signalen: Das Kind sendet ein Signal an den Elternprozess, nachdem es „hello“ ausgegeben hat.
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int sig) {}
int main() {
signal(SIGUSR1, handler);
pid_t pid = fork();
if (pid == 0) {
printf("hello\n");
kill(getppid(), SIGUSR1);
} else {
pause();
printf("goodbye\n");
}
return 0;
}So wird die Reihenfolge garantiert, ohne dass der Elternprozess blockiert.
Projekt 5: exec()-Familie
Der Kindprozess soll ls -l -F -h mit verschiedenen exec()-Varianten ausführen: execl(), execlp(), execv() und execvp(). Der Elternprozess wartet mit wait(). Die Unterschiede liegen in der Übergabe der Argumente (Liste vs. Array) und der Pfadsuche (p-Versionen durchsuchen PATH).
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// execl: Liste, kein PATH
execl("/bin/ls", "ls", "-l", "-F", "-h", NULL);
// execlp: Liste mit PATH
// execlp("ls", "ls", "-l", "-F", "-h", NULL);
// execv: Array, kein PATH
// char *args[] = {"ls", "-l", "-F", "-h", NULL};
// execv("/bin/ls", args);
// execvp: Array mit PATH
// execvp("ls", args);
perror("exec");
return 1;
} else {
wait(NULL);
printf("Kind beendet\n");
}
return 0;
}Kommentiere im Code, dass execl und execv den absoluten Pfad benötigen, während execlp und execvp die Umgebungsvariable PATH nutzen.
Heap-Manager: beavalloc und beavfree
Implementiere deinen eigenen Heap-Manager mit beavalloc(), beavfree(), beavcalloc() und beavrealloc(). Verwende brk() und sbrk(), nicht die Bibliotheks-malloc(). Fordere Speicher in 1024-Byte-Blöcken an und verwalte freie Blöcke mit einer einfach oder doppelt verketteten Liste. Verwende den First-Fit-Algorithmus: Scanne die Liste nach dem ersten Block, der groß genug ist. Wenn er deutlich größer ist, teile ihn auf.
typedef struct block {
size_t size;
int free;
struct block *next;
struct block *prev;
} block_t;
#define BLOCK_SIZE sizeof(block_t)
#define CHUNK 1024
void *beavalloc(size_t size) {
if (size == 0) return NULL;
block_t *block = find_free_block(size);
if (!block) {
block = request_space(size);
if (!block) return NULL;
}
// Block als belegt markieren und ggf. teilen
block->free = 0;
return block + 1; // Zeiger auf Datenbereich
}Teste deinen Heap-Manager gründlich, z.B. mit Allokationen und Freigaben in unterschiedlicher Reihenfolge. Achte darauf, dass beavfree() benachbarte freie Blöcke zusammenführt, um Fragmentierung zu vermeiden.
Häufige Fehler und Tipps
- Makefile: Vergiss nicht
make cleanzu testen. Ein fehlendes Makefile führt zur Note 0. - fork(): Denke daran, dass Kind- und Elternprozess unabhängige Speicherbereiche haben.
- Dateioperationen: Synchronisiere den Zugriff, wenn die Reihenfolge wichtig ist.
- exec(): Nach einem erfolgreichen
exec()wird der restliche Code nicht ausgeführt. - Heap-Manager: Verwende
sbrk()nur, wenn nötig. Teste auch Randfälle wie Allokation von 0 Bytes.
Fazit
Diese Projekte sind die Basis für fortgeschrittene Systemprogrammierung. Mit fork(), exec() und einem eigenen Heap-Manager gewinnst du tiefe Einblicke in die Prozessverwaltung und Speicherverwaltung von Unix/Linux. Diese Kenntnisse sind auch in Cloud-Computing, Embedded Systems und KI-Frameworks gefragt. Viel Erfolg!