SP2 Übungen - Milan Stephan
Ich bin zwar kein Tutor mehr aber ich lass euch die Seite aber online, sie basiert auf WS21.
Bei Fragen bin ich trotzdem noch für euch da. Wenn sich Sachen grundlegend geändert haben, bitte Bescheid geben!
Stand: 22.4.2022; die verlinkten Folien sind noch aus 2021!
Übungen und Codeschnipsel (Folien und Videos)
sister
rush
mother
, Evaluierung, KlausurvorbereitungCheckliste 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 (siehevalgrind
). - 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)
- 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 mitnc -l 2345
in die Rolle des Mailservers schlüpft (faui03
und Port2345
dementsprechend anpassen!) - Die
faui03
akzeptiert nur Mails aus dem Uni-Netz. - snail-Proxy
Aufgabe 2: sister (Gruppe)
- 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 mitfork
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")
- Sich der Proxy gegenüber dem Browser anders verhalten könnte, als die
Aufgabe 3: rush (Gruppe)
- 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)
- 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)
- Statt
readdir
könnt ihr auchscandir
verwenden, aberreaddir
nochmal zu machen ist auch nicht verkehrt. - 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 ihrfcntl
mitF_DUPFD_CLOEXEC
verwenden, um das Close-on-exec Flag atomar zu setzen (dup
würde noch ein zusätzlichesfcntl
erfordern und ist damit nicht atomar). - Die Race-Condition bei
accept
könnt ihr ignorieren - die bekäme man nur mitaccept4
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.
-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"
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ürSIG_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); // [...] }
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 Typsigset_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
)
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) mitsigset_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. // [...] }
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: nud3ls4l4t
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)
*Stef: Hilfe gibts nur hier™
Andere SP-Seiten: