SP2 Übungen - T02 und T05

Stand: 2.2. 15:19


Übungen und Codeschnipsel (Folien und Videos)

T02
T05
Thema
Material
25.10.
27.10.
1.11.
3.11.
(In dieser Woche finden keine Tafelübungen statt)
 
8.11.
10.11.
15.11.
17.11.
Besprechung der snail und der Miniklausur
22.11.
24.11.
29.11.
1.12.
Besprechung der sister
6.12.
8.12.
13.12.
15.12.
Besprechung rush
20.12.
22.12.
27.12.
29.12.
(Ferien)
 
3.1.
5.1.
(Ferien)
 
10.1.
12.1.
17.1.
19.1.
Besprechung jbuffer, Klausurvorbereitung
24.1.
26.1.
Klausurvorbereitung: Programmieraufgabe
31.1.
2.2.
Besprechung mother, Evaluierung, Klausurvorbereitung

Klausurvorbereitung


Checkliste für Abgaben

  • Ein Makefile schreiben.
  • Aufgabenstellung *genau* lesen, auch die Hinweise. Spezifikation genau einhalten!
  • Unnötige globale Variablen vermeiden.
  • Globale Variablen und Funktionen, soweit möglich, als static deklarieren.
  • Funktionen, die keine Argumente bekommen, in der Argumentliste mit void kennzeichnen.
  • Fehlerbehandlung! (Siehe man-Pages), Korrekturrichtlinien
  • Jegliche durch malloc, fopen o.ä. angeforderten Ressourcen wieder freigeben (siehe valgrind).
  • Beim Terminieren im Fehlerfall müssen die Ressourcen nicht wieder freigegeben werden.
  • Mit valgrind auf ungültige Speicherzugriffe prüfen.
  • Selber Testcases schreiben, besonders auf Randfälle prüfen!

Aufgabe 1: snail (einzeln)

  • Abgabe T02: 8.11. 17:30
  • Abgabe T05: 10.11. 17:30
  • Rechtzeitig das Passwort für das SVN-Repository setzen.
  • Doppelten Code vermeiden, Statuscode-Überprüfung in eine Funktion auslagern.
  • Ihr könnt nc verwenden, um euer Programm zu debuggen, indem ihr mit nc -l 2345 in die Rolle des Mailservers schlüpft (faui03 und Port 2345 dementsprechend anpassen!)
  • Die faui03 akzeptiert nur Mails aus dem Uni-Netz.
  • snail-Proxy

Aufgabe 2: sister (Gruppe)

  • Abgabe T02: 22.11. 17:30
  • Abgabe T05: 24.11. 17:30
  • Webserver mit dem wwwpath aus dem Beispiel starten und testen, ob die Grafiken dargestellt werden
  • Die Module können einzeln getestet werden, wenn sie mit den anderen Teilen der Referenzimplementierung zusammen gelinkt werden
  • Fehlerbehandlung beim Schreiben auf die Netzwerkverbindung ist hier nicht notwendig (da SIGPIPE den Prozess im Fehlerfall terminiert und einzelne Anfragen mit fork in eigenen Prozessen abgearbeitet werden)
  • Endian-Konvertierung: man byteorder
  • (PSA) Es gibt doch keinen Proxy für den HTTP-Traffic (wie für SMTP bei der snail), weil:
    • Sich der Proxy gegenüber dem Browser anders verhalten könnte, als die sister
    • Würde nicht so viel bringen, da der Netzwerkverkehr nur aus der Anfrage und der Antwort besteht
    • Es wäre sehr unübersichtlich, wenn die Anfrage-Header des Browsers und der Antwort-Body im Terminal landen
    • Bilder im Terminal anzeigen (im Sinne von cat $bilddatei) ist nicht so cool
    • Man müsste beide Richtungen trennen, da sonst die Ausgaben "vermischt" werden (nach der Anfragezeile senden beide u.U. "gleichzeitig")

Aufgabe 3: rush (Gruppe)

  • Abgabe T02: 6.12. 17:30
  • Abgabe T05: 8.12. 17:30
  • Die Zusammenfassung "Signale" lesen - wenn Sachen unklar sind, gerne nachfragen! :)
  • Mehrfach verwendeten Code in Funktionen auslagern
  • Fehler, die nicht auftreten können (z.B. falsche Funktionsparameter bei garantiert gültigen Parametern), müssen auch nicht behandelt werden
  • Ganz genau auf Race-Conditions zwischen Signalen und Hauptprogramm achten achten!
  • Definierten Startzustand für die verwendeten Signalmasken und -behandlungen herstellen (die sind erstmal undefiniert!)
  • Von der rush gestartete Kindprozesse sollen eine "sinnvolle" Umgebung bekommen (im Sinne der Vererbung von Signalmasken und -Behandlungen)

Aufgabe 4: jbuffer (einzeln)

  • Abgabe T02: 7.1. 17:30
  • Abgabe T05: 11.1. 17:30
  • Das Makefile wird länger als bei den anderen Aufgaben, plant hierfür bitte genügend Zeit ein
  • Keine Ausgaben und kein exit in den "Bibliotheksfunktionen" (und sinnvoll Fehler behandeln)
  • Auf korrekte Synchronisierung mit Semaphoren und atomic_compare_exchange_strong achten
  • Wo braucht ihr volatile oder _Atomic?
  • Strengere Compilerflags wie -Wconversion (siehe unten) können interessant sein

Aufgabe 5: mother (Gruppe)

  • Abgabe T02: 24.1. 17:30
  • Abgabe T05: 26.1. 17:30
  • Netzwerkdinge fehlerbehandeln, weil SIGPIPE den Prozess nicht terminieren darf!
  • Was ist der Unterschied zwischen exit und _exit?
  • Es geht einfacher, als opendir/readdir - schaut euch da mal die manpages an
  • Statt dup könnt ihr fcntl mit F_DUPFD_CLOEXEC verwenden, um das Close-on-exec Flag atomar zu setzen (dup würde noch ein zusätzliches fcntl erfordern und ist damit nicht atomar).
  • Die Race-Condition bei accept könnt ihr ignorieren - die bekäme man nur mit accept4 weg, das ist allerdings nicht POSIX-konform.

Zusätzliche Compilerflags

Warum noch mehr Compilerflags? Wenn euch der Compiler durch genauere Prüfung schon vor Fehlern warnt, gebt ihr sie nicht mit ab. 😉

>> Wichtig: Der Code muss mit den "offiziellen" Flags kompilieren! <<

-std=c11 -D_XOPEN_SOURCE=700 -pedantic -Werror -Wall -g -Wextra -Wconversion -Wshadow -Wformat=2 -Wno-unused

Erklärung:

-g: Ermöglicht besseres Debugging mit valgrind oder gdb.

-Wextra: Schaltet noch mehr Checks an

-Wconversion: Weist auf implizite Casts hin, bei denen man Wertebereiche prüfen und ggf. abbrechen sollte

-Wshadow: Warnt bei überschriebenen Variablen (in Schleifen z.B.)

-Wformat=2: Erkennt kaputte/fehlende Format-Strings bei printf etc.

-Wno-unused: Fehler bei unbenutzten Funktionen/Variablen/… ignorieren

Legende:

  • Immer anschalten
  • (Erstmal) weglassen, wenn ihr unverständliche Fehlermeldungen bekommt
  • Code muss auch ohne dieses Flag kompilieren, vor Abgabe sicherstellen!

Erkennen von Speicherlecks, Zugriffsfehlern usw.

Das Compilerflag -g verwenden, um Debug-Informationen mit ins fertige Programm zu schreiben. Nur so bekommt ihr Zeilennummern bei den Fehlerausgaben.
valgrind --leak-check=full --show-reachable=yes --track-origins=yes ./lilo

Zusammenfassung "Signale"

Die Zusammenfassung über Signale ist nicht final und kann sich noch ändern. Bei Fehlern oder Verbesserungsvorschlägen, schreib mir gerne! :)

Ignorieren vs Blockieren - oder auch: Behandlung vs Maskierung

Die Behandlung eines Signals (SIG_IGN, ..., eigene Behandlung) gilt prozessweit für alle Threads. Ignorierte Signale werden verworfen, solange die Behandlung aktiv ist. Meistens wird die Behandlung einmal zum Programmstart definiert - nachträgliche Änderungen sind eher ungewöhnlich. Bei SIGCHLD hat (explizites) Ignorieren zur Folge, dass das Betriebssystem keine Zombieprozesse mehr anlegt. Welche Signale überhaupt zur Behandlung zugelassen werden, definiert die aktive, threadspezifische Signalmaske. Sie definiert für jeden Thread, welche der Signale erlaubt oder blockiert werden. Blockierte Signale werden (in mehrfädigen Programmen) entweder von einem anderen Thread bearbeitet, der das Signal erlaubt hat - oder verzögert (wenn kein solcher Thread existiert oder das Signal explizit an diesen Thread und nicht an den Prozess gerichtet ist). Von blockierten Signalen wird (pro Signal) je ein Vorkommen gepuffert.

Einrichtung der Signalbehandlung

Die Behandlungfunktion für Signale wird vom Elternprozess geerbt und ist daher zu Programmstart nicht definiert und muss explizit gesetzt werden. Das gilt allerdings nur für SIG_IGN und SIG_DFL. Warum? Wenn mit exec ein anderes Programm geladen wird, existiert die "alte" Behandlungsfunktion nicht mehr im neuen Programm und die Signalbehandlung wird auf das Standardverhalten zurückgesetzt. ¯\_(ツ)_/¯
static void handler(int signum) {
	(void) signum;

	int errno_backup = errno;

	// Die Signalbehandlung sollte *möglichst kurz* gehalten werden!
	// Falls die errno verändert werden könnte, vorher
	// sichern und danach wiederherstellen.

	errno = errno_backup;
}

int main(void) {

	struct sigaction action = {
		.sa_handler = handler,
		.sa_flags = SA_RESTART,
	};
	sigemptyset(&action.sa_mask);

	sigaction(SIGINT, &action, NULL);

	// [...]
}
Die Maske sa_mask gibt an, welche Signale zusätzlich zu den aktuell schon blockierten Signalen außerdem während der Ausführund der Behandlung noch blockiert werden sollen. In der Maske gesetzte Signale werden hierbei zeitweise der aktiven, threadspezifischen Signalmaske hinzugefügt. Der ursprüngliche Zusand der aktiven Signalmaske wird nach der Behandlung automatisch wiederhergestellt. Nicht gesetzte Signale in sa_mask sind "don't care"s. Sie werden nicht verändert - es ist daher nicht möglich, aktuell blockierte Signale per sa_mask freizuschalten!

Bearbeiten der aktiven, threadspezifischen Signalmaske

Die aktive, threadspezifische Signalmaske ist, wie die Behandlungsfunktion, vom Elternprozess geerbt und nicht definiert. Daher muss sie ebenfalls zu Programmstart auf einen definierten Wert gesetzt werden, wenn Signale empfangen werden sollen oder explizit nicht erwünscht sind. Zum Bearbeiten der aktiven, threadspezifischen Signalmaske können Sets vom Typ sigset_t verwendet werden. Diese Sets enthalten nur eine Liste an Signalen - was diese Liste aussagt hängt davon ab, wie sie genutzt wird. Mit sigprocmask können
  • alle gesetzten Signale blockiert werden - nicht gesetzte Signale = don't care, werden nicht verändert (SIG_BLOCK)
  • alle gesetzten Signale erlaubt werden (nicht gesetzt = don't care, werden nicht angefasst) (SIG_UNBLOCK)
  • die aktive Signalmaske ersetzt werden - gesetzte Signale werden blockiert, nicht gesetzte erlaubt (SIG_SETMASK)
Außerdem kann sigprocmask die bis dahin aktive Maske speichern. Das kann nützlich sein, um sie nach einem kritischen Abschnitt wiederherstellen zu können. Blockierte Signale werden in diesem Set gesetzt, erlaubte Signale gelöscht. Signale mit zuvor undefiniertem Zustand werden beim Zurücksetzen auch wieder auf den alten (unbekannten) Wert gesetzt. Ein Beispiel dazu gibt's weiter unten. In Programmen mit mehreren Threads muss statt sigprocmask die Funktion pthread_sigmask verwendet werden. Mit sigsuspend(A) kann auf erlaubte Signale gewartet werden - dabei wird A die neue aktive Signalmaske (SIG_SETMASK). Nur in A erlaubte/nicht gesetzte Signale werden zugestellt und können den Thread aufwecken. Nachdem der Thread aufgeweckt wurde, wird die alte Maske wiederhergestellt.

Erstellen und Bearbeiten von Sets

Signal-Sets können (z.B. auf dem Stack) mit sigset_t mask; deklariert werden. Zur Intialisierung müssen entweder sigemptyset oder sigfillset verwendet werden. Mit sigemptyset wird das Set geleert und kein Signal ist "gesetzt" - sigfillset füllt das Set, sodass alle Signale "gesetzt" sind. Sobald ein leeres oder volles Set erzeugt wurde, kann es mit sigaddset und sigdelset weiter modifiziert werden. Mit sigaddset kann ein Signal "gesetzt" werden, mit sigdelset wird es "gelöscht". Da laut Spezifikation nicht festgelegt ist, ob gesetzte Signale eine 1 oder 0 in der Bitmaske sind, darf zur Initialisierung kein memset verwendet werden und alle Änderungen müssen über die gerade beschriebenen Funktionen durchgeführt werden.

Beispiel zum Blockieren von SIGCHLD

int main(void) {
	sigset_t old;

	sigset_t mask;
	sigemptyset(&mask);
	sigaddset(&mask, SIGCHLD);

	sigprocmask(SIG_BLOCK, &mask, &old);

	// Kritischer Abschnitt - hier kann kein SIGCHLD auftreten.

	sigprocmask(SIG_SETMASK, &old, NULL);

	// Hier ist SIGCHLD -wie vorher- undefiniert!
	// SIGCHLD kann erlaubt oder auch blockiert sein.

	sigprocmask(SIG_UNBLOCK, &mask, NULL);

	// Hier ist SIGCHLD auf jeden Fall erlaubt.

	// [...]
}
Zuerst wird eine leere Maske erzeugt, in dieser dann SIGCHLD "gesetzt". Der Aufruf von sigprocmask blockiert alle gesetzten Signale - also in dem Beispiel nur SIGCHLD, alle anderen Signale bleiben unverändert. Nach dem kritischen Abschnitt wird die alte Signalmaske wiederhergestellt. Ob SIGCHLD erlaubt ist, ist unbekannt, da keine sinnvolle Initialisierung stattgefunden hat. Erst nach dem SIG_UNBLOCK Aufruf kann man sich drauf verlassen, dass SIGCHLD zugestellt werden kann.

Kontakt und Sonstiges

Bei Fragen zur Übung, zu euren Übungsaufgaben und deren Korrekturen (Korrekturrichtlinien) oder zu sonstigen SP-Themen könnt ihr mich gerne kontaktieren.

Mail: milan.stephan@fau.de

Discord: nudelsalat#8505

Rechnerübung: Mittwoch 12:15-15:45 Uhr - zwei Übungen hintereinander mit einer Pause in der Mitte (Terminplan)

StudOn Kurs mit Forum (offiziell)

FSI-Forum (SP-Unterforum) (inoffiziell)

IRC-Guide, Jahrgangschannel #faui2k20 und #sp *

RocketChat der FSI Informatik, Jahrgangschannel #faui2k20 und #2_sp1/#3_sp2

>> FAU Informatik Discord Server << (inoffiziell und auch für andere Studiengänge, die SP hören)

Andere SP-Seiten: