Häufig gemachte Fehler und allgemeine Hinweise

Disclaimer: Dieser Thread wurde aus dem alten Forum importiert. Daher werden eventuell nicht alle Formatierungen richtig angezeigt. Der ursprüngliche Thread beginnt im zweiten Post dieses Threads.

Häufig gemachte Fehler und allgemeine Hinweise
Da mich die Korrektur der ersten Aufgabe schon dazu veranlasst hat, meinen Benutzertitel in diesem Forum umzuändern, tue ich hier zum Ausgleich mal etwas für die Allgemeinheit und stelle eine Liste von Fehlern zusammen, die mir beim Korrigieren aufgefallen sind - in der Hoffnung, dass sie dem Einen oder Anderen zur Erleuchtung gereichen wird: :wink:

1. (Aufgabe 1) Negative Werte nicht ignoriert
Es stand deutlich in der Aufgabenstellung, dass Eingaben < 0 in der Funktion [m]append_element()[/m] ignoriert werden sollen. Warum das gefordert war, sollte einleuchten, wenn man sich klar macht, dass die Funktion [m]remove_element()[/m] bei einer leeren Liste den negativen Wert -1 zurückliefern soll. Wenn man -1 als regulären Wert in der Schlange zuließe, gäbe es dann natürlich eine Überschneidung.

Allgemein gilt: Lest euch die Aufgabenstellung genau durch, und prüft nochmals, wenn ihr fertig seid, ob euer Programm genau das tut, was vom Aufgabensteller verlangt wird!

2. Fehlende oder unvollständige Fehlerbehandlung
Murphys Gesetz besagt: Alles, was irgendwie schief gehen kann, wird auch schief gehen. Also: Bei allen Bibliotheksfunktionen, die ihr aufruft, den Rückgabewert überprüfen und im Fehlerfall eine Meldung ausgeben. Je nach Art und Schwere des Fehlers hat es keinen Zweck mehr, das Programm fortzusetzen - falls z.B. [m]malloc()[/m] fehlschlägt, muss irgendetwas ziemlich kaputt sein, also sollte man mit [m]exit(EXIT_FAILURE);[/m] die logische Konsequenz daraus ziehen.

3. Speicherlecks
Ein Speicherleck entsteht, wenn man sich mit [m]malloc()[/m] (oder einer verwandten Funktion) Speicher geholt hat, aber diesen nicht mehr mit [m]free()[/m] zurückgibt. Das ist zwar eher unkritisch, weil sämtlicher Speicher beim Beenden des Programms sowieso wieder freigegeben wird, aber es ist schlechter Programmierstil und könnte z.B. bei Programmen, die über eine längere Zeit laufen und immer wieder dynamisch Speicher nachfordern, leicht dazu führen, dass der RAM des Rechners irgendwann plötzlich voll ist.
Ob eure Anwendung Speicherlecks enthält, findet ihr mit dem Programm [m]valgrind[/m] heraus, das in der Tafelübung vorgestellt wurde. Wenn ihr euer Programm mit dem zusätzlichen Flag -g kompiliert, kann [m]valgrind[/m] übrigens noch genauere Informationen anzeigen.
(Falls ihr ein Programm an einer Stelle unerwartet wegen eines Fehlers beendet, müsst ihr übrigens den Speicher nicht unbedingt aufräumen.)

Liste wird unten fortgesetzt…


4. (Aufgabe 1) Bug beim Aufrufen von [m]remove_element()[/m] auf eine leere Warteschlange
Hier wird es etwas komplizierter.
Der einfachste Weg zu überprüfen, ob die verkettete Liste leer ist, ist das Vergleichen des [m]head[/m]-Zeigers mit NULL.
Damit das auch wirklich funktioniert, muss man aber eine der folgenden Möglichkeiten verwenden:
a) Wenn vor dem Entfernen nur noch ein Element existiert ([m]head == tail[/m]), [m]head[/m] explizit auf NULL setzen.
b) Fall a) nicht extra behandeln, sondern einfach [m]head = head->next;[/m] verwenden. Das funktioniert einwandfrei, so lange man oben in seiner [m]append_element()[/m] für das neu angelegte Element den [m]next[/m]-Zeiger auf NULL setzt.

Tut man dies nicht, dann ist [m]head[/m] bei einer leeren Liste nicht zwangsläufig ein NULL-Zeiger, sondern er enthält irgendwelche zufälligen Daten, die vorher schon drinstanden. Wenn man Glück hat, enthielt er durch Zufall den Wert 0x00000000 (= NULL) - falls nicht, hat man ein Problem, weil der Zeiger ins Nirvana verweist und das Programm deswegen mit einem Speicherzugriffsfehler abstürzt.

Solche Fehler sind leider relativ schwer zu finden, weil sie nicht immer auftreten. Hier kommt wieder [m]valgrind[/m] ins Spiel!
Bekanntes Vorgehen: Programm mit dem zusätzlichen Flag -g kompilieren und das Lieblings-Werkzeug eines jeden C-/C+±Programmierers anwerfen. :wink:
Wenn [m]valgrind[/m] irgendeine Meldung der Art Read error, Write error, Conditional jump depends on uninitialized data o.ä. ausspuckt, ist etwas faul. Dank der obigen Compileroption wird genau angezeigt, in welcher Zeile des Quellcodes der Fehler auftritt - hierdurch sollte man (mit etwas Erfahrung) das Problem schnell eingrenzen können. :slight_smile:

Zu [m]valgrind[/m] gibt es auch noch eine Alternative namens “Electric fence”, die auf eine etwas andere Art und Weise arbeitet. Hierfür muss man seine Anwendung mit dem Flag -lefence linken. Danach das Programm ganz normal ausführen. Falls Efence einen Fehler findet, gibt es eine Meldung aus und beendet den Prozess sang- und klanglos.


Da hänge ich mich doch glatt mal dran. Ein paar Sachen, die zwar mehr schlechter Stil (über den man bekanntlich streiten kann) als konkrete Fehler sind, aber dennoch (mindestens bei mir g), in der Hoffnung, einen Blick für solche Sachen zu schärfen, Punkte kosten können. Zumindest in den späteren Aufgaben (> 3), wenn man das oft genug vorher gepredigt hat. :wink:

5. static bei globalen Variablen/Funktionen vergessen
Globale Funktionen/Variablen sind ja bekanntlich (weil global) von überall aus veränderbar. Wenn also ein anderer Programmierer ein Modul schreibt, und darin auf meine globalen Variablen zugreift, dann bekomme ich das wahrscheinlich nicht mit und wundere mich dann über sich plötzlich “von selbst” verändernde Werte o.ä. Genau deswegen gibt es das Prinzip der Datenkapselung bzw. des Information Hiding. Jeder bekommt nur das zu sehen und zu verwenden, was er unbedingt braucht.
Deswegen: Alles was nicht unbedingt global sein MUSS, gar nicht erst global machen (Tipp: Man kann Funktionen auch Werte als Parameter mitgeben - über Zeiger sogar “by reference”, also innerhalb der Funktion veränderbar). Und wenn es denn global sein muss, dann wenigstens “lokal global”, also static. Damit ist die Variable/Funktion nur innerhalb dieses Moduls (dieser .c-Datei) sichtbar und man kann halbwegs überblicken was damit passiert. Ausnahme natürlich: Funktionen, die von außerhalb aufgerufen werden sollen. Z.B. append_element() und remove_element() der queue.

6. Funktionen, die keinen Parameter erfordern, nicht explizit als void deklariert
Das ist eine sehr “interessante” Eigenart von C. Wenn man eine Funktion dieser Art deklariert:

void func();

Dann kann sie prinzipiell mit beliebig vielen Parametern aufgerufen werden:

func();
func(1);
func("bla");
func(1,2,3);

All das kompilliert anstandslos und funktioniert auch. Das produziert an sich natürlich erst mal noch keinen Fehler, aber es kann zu Fehlverhalten im Sinne von nicht erwartetem Verhalten führen. Man ruft eine Funktion mit irgendwelchen Parametern auf und erwartet dass sie was damit macht. Macht sie aber natürlich nicht, weil der Programmierer (wahrscheinlich) eine Funktion ohne jeglichen Parameter schreiben wollte. Also bitte so deklarieren:

void func(void);

7. Unnötige if-Verschachtelungen
Was in der queue relativ häufig vorkam:

void append_element(int value) {
    if (value >=0) {
        /* code */
    }
}

Das ist natürlich korrekt. Und dafür gibt’s auch keinen Punktabzug. Aber einen netten Kommentar. g Das macht den Code doch nur unnötig unübersichtlich. Wenn möglich, vermeidet lange {}-Blöcke. Besonders wenn sie mal über mehr als eine Seite gehen, wird es sehr schwer, das zu überblicken. Eine einfache Abhilfe hier wäre:

void append_element(int value) {
    if (value < 0)
        return;

    /* code */
}

8. Unerwünschte Funktionalität
Der Airhardt hat ja schon auf das Einhalten der Aufgabenstellung hingewiesen. Was aber auch noch ein Problem ist, ist unnötige bzw. nicht geforderte Funktionalität. Natürlich verstehen wir, dass man sich auch mal an einer Aufgabe austoben will und Spaß dran hat, noch dies oder das zu implementieren. Aber man kann’s auch übertreiben (siehe z.B. eine Shell mit eingebauten Kommandos und allem Komfort…).
Und grundsätzlich sollte man sich mal Gedanken über das Thema machen - gerade im Hinblick aufs Berufsleben, auf das die Uni ja ab und zu auch vorbereiten soll. :wink: Wenn ich dazu mal den juk (recht frei) zitieren darf: “Überlegt euch mal, wer das bezahlen soll. Der Auftraggeber hat bestimmte Features gefordert und ihr implementiert ihm noch 10 andere. Auf wessen Rechnung soll die Arbeitszeit gehen, die ihr dafür braucht?” Klar ist es bei SoS1 in erster Linie euer Problem, wenn ihr lieber noch was in eine fertige Aufgabe einbaut als das Sommerwetter zu nützen. :wink: Und mehr Übung im Programmieren schadet natürlich auch nicht. Aber bedenkt dabei: Mehr Code bietet mehr Potential für Fehler. Ein Programm, das volle Punkte gegeben hätte, erweitert mit einem unnötigen Teil, der einen Fehler enthält, gibt am Ende Punktabzug. Wenn ihr eine 20-seitige Lösung abgebt, macht ihr euch außerdem nicht gerade bei eurem Übungsleiter beliebt. :wink: UND: Es gibt einige Fächer (v.a. im Informatik-Hauptstudium), die für nicht verlangte Funktionalität rigoros einiges an Punkten abziehen!


Weitere Hinweise:

9. Häufige Fehler im Makefile

  • Das Pseudo-Target [m]install[/m] muss die ausführbare Datei als Abhängigkeit haben, damit das Programm automatisch gebaut wird, falls die Datei noch nicht existiert.
  • Das Pseudo-Target [m]clean[/m] darf keine Abhängigkeiten haben. Wenn ich in meinem Verzeichnis aufräumen will, wäre es ziemlich sinnlos, erst das Programm zu bauen und gleich im Anschluss wieder zu löschen. Außerdem muss jeder [m]rm[/m]-Aufruf mit der Option [m]-f[/m] versehen werden, damit der Vorgang nicht abgebrochen wird, falls eine der zu löschenden Dateien nicht existiert.
  • Alle Pseudo-Targets (Targets, die keine Datei des selben Namens erzeugen), also in diesem Fall [m]install[/m] und [m]clean[/m] müssen mit [m].PHONY: install clean[/m] explizit als solche gekennzeichnet werden. Warum das wichtig ist, könnt ihr ausprobieren, indem ihr eine neue Datei namens [m]clean[/m] anlegt und dann [m]make clean[/m] aufruft. :slight_smile:

10. Strings und String-Funktionen
Immer dran denken: Jeder C-String ist durch das Zeichen [m]‘\0’[/m] terminiert, deswegen braucht man immer [m]n + 1[/m] Bytes an Platz.
Alle Funktionen, die irgendwas mit Strings arbeiten und eine Größenangabe als Argument fordern (z.b. [m]fgets()[/m] oder [m]strncpy()[/m]) schreiben immer ein abschließendes [m]‘\0’[/m], d.h. wenn ihr mit [m]strncpy()[/m] 100 Zeichen kopieren wollt, müsst ihr den Zielpuffer 101 Bytes groß machen und der Funktion den Wert 101 als Größe übergeben.
An dieser Stelle ist mir noch aufgefallen, dass in den abgegebenen Aufgaben oft [m]strncpy()[/m] verwendet wurde (und das teilweise falsch - s.o.), obwohl es problemlos mit [m]strcpy()[/m] funktioniert hätte. Als Tipp: [m]strcpy()[/m] ist nur dann gefährlich, wenn man nicht weiß, ob der zu kopierende String (+ [m]‘\0’[/m]) in den Zielpuffer passt. Wenn ihr also genau wisst, dass euer Zielpuffer groß genug ist (z.B. weil ihr in mit der Größe [m]strlen(string) + 1[/m] allokiert habt), könnt ihr gefahrlos [m]strcpy()[/m] verwenden.

11. Verwendung von [m]fgets()[/m]
Die einfachste Möglichkeit, um zu prüfen, ob eine Zeile länger als 100 Zeichen ist, geht in etwa so (wichtig ist auch die Fehlerabfrage!):

char buffer[102];
if (fgets(buffer, 102, stdin) == NULL) {
    if (ferror(stdin)) {
        /* Fehler beim Lesen aufgetreten -> Programm abbrechen */
    } else {
        /* Dateiende -> Schleife verlassen */
    }
}
if (buffer[strlen(buffer) - 1] != '\n') {
    /* <= 100 Zeichen -> Zeile verarbeiten */
} else {
    /* > 100 Zeichen -> Rest der Zeile einlesen und wegwerfen */
}

Wie und warum das funktioniert, versteht ihr am besten, wenn ihr euch mal den ungefähren Inhalt des Puffers in den Grenzfällen auf Papier malt.

12. Verwendung von [m]perror()[/m]
Viele Leute haben die Funktion [m]perror()[/m] verwendet, um Fehlermeldungen auszugeben, falls eine zu lange Zeile eingelesen wurde. [m]perror()[/m] gibt aber nicht einfach so eine Meldung nach [m]stderr[/m] aus, sondern tut in etwa das hier:

void perror(const char *s)
{
    fprintf(stderr, "%s: %s\n", s, strerror(errno));
}

Im Klartext: [m]perror()[/m] gibt den übergebenen String auf [m]stderr[/m] aus, gefolgt von einem Doppelpunkt und der textuellen Beschreibungs des zuletzt aufgetretenen Fehlers, dessen Fehlercode in der Variable [m]errno[/m] gespeichert ist. Bei den Leuten, die [m]perror()[/m] falsch verwenden, sieht es dann z.B. so aus:
Diese Zeile ist laenger als 100 Zeichen!: Success
:wink:


13. Uninitialisierter Speicher
Immer daran denken: Speicher, der von malloc() und realloc() (der neue Teil natürlich) kommt und alle lokalen Variablen sind IMMER uninitialisiert. Da kann alles Mögliche drin stehen. Also aufpassen, ob ihr irgendwo die Variable verwendet, ohne ihr vorher etwas zugewiesen zu haben.

14. Böse Sachen mit argv anstellen
argv ist ein Aufrufparameter von main(). Als solcher liegt es auf dem Stack. Versucht nicht, daran mit realloc() herumzubasteln! In argv bekommt man seine Aufrufparameter übergeben und es ist kein Speicherplatz für eigene Dinge…


15. man-pages lesen!
Das gilt nicht nur für Aufgabe 3 ([m]mini_sh[/m]), wo ihr euch die Funktionsweise von [m]getcwd()[/m], [m]strtok()[/m] und [m]chdir()[/m] eigenständig erarbeiten solltet, sondern ganz allgemein.
Ein paar Dinge, die so ganz deutlich in der man-page drinstehen und trotzdem immer wieder falsch gemacht wurden:

  • [m]getcwd()[/m]: The buf argument should be a pointer to an array at least PATH_MAX bytes long.
  • In der Seite zu [m]wait()[/m] findet man ganz unten ein wunderschönes Beispiel, wie man die Makros zur Analyse des Rückgabestatus verwendet.
  • In allen man-pages steht genau, was die jeweilige Funktion im Fehlerfall zurückgibt. Diese Rückgabe muss man dann natürlich auch abfragen, um festzustellen, ob ein Fehler aufgetreten ist!

Auch gerade bei der aktuellen [m]malloc()[/m]-Aufgabe solltet ihr auch genau lesen, wie das Verhalten der von euch zu implementierenden Funktionen spezifiziert ist (Stichwort spezielle Parameter und Fehlerbehandlung). :finger:

16. Statische vs. dynamische Arrays
Schön, wenn ihr etwas gelernt habt und es dann auch gleich exzessiv anwenden wollt. :wink:
Im Falle von [m]malloc()[/m] und Konsorten ist das aber nicht immer optimal. In den allermeisten Situationen reicht es völlig aus, statt

char *buffer = malloc(1337 * sizeof(char));

ganz einfach

char buffer[1337];

zu schreiben.
Dadurch erspart ihr euch nicht nur einen ganzen Haufen Arbeit, weil ihr weder eine Fehlerüberprüfung braucht noch euch darum kümmern müsst, den Speicher später wieder mit [m]free()[/m] freizugeben - sondern auch etliche potenzielle Fehlerquellen.
Das Ganze funktioniert natürlich nur unter zwei Bedingungen:
1. Statische Arrays liegen im Stack-Segment und sind deswegen nur so lange gültig, wie sich der Kontrollfluss des Programms in der Funktion befindet, in der das Array deklariert wurde (oder in einer Funktion, die direkt oder indirekt von dieser Funktion aus aufgerufen wurde).
2. Die (maximale) Größe des Arrays muss zum Zeitpunkt der Programmierung bekannt sein, weil man sie zur Laufzeit nicht mehr verändern kann.

Bei der [m]mini_sh][/m] konnte man z.B. komplett ohne dynamische Arrays auskommen:

char  dir[PATH_MAX]; /* fuer getcwd(); wegen PATH_MAX s.o. */
char  input[1026];   /* fuer fgets(); 1024 Zeichen + '\n' + '\0' */
char *args[513];     /* Ergebnisse von strtok() */

[m]args[/m] ist ein Feld von Zeigern auf die einzelnen Teilstrings der zerlegten Eingabe. Theoretisch kann die Eingabezeile maximal in 512 Teile zerlegt werden - nämlich dann, falls immer abwechselnd ein Zeichen und dann ein Leerzeichen kommt. Sobald das Array zusammengebaut wurde, muss noch ein abschließendes [m]NULL[/m] dahinter gehängt werden (das wurde gerne vergessen!), deswegen insgesamt 513.


Einer meiner Lieblinge:
17. Magic numbers are evil
Wann immer es sich vermeiden lässt, sollte man im Code keine direkten numerischen Literale verwenden. Das macht es nicht nur schwerer, den Sinn der Zahl zu verstehen, sondern auch nachträgliche Änderungen (wenn die Zahl mehrfach vorkommt) werden fehleranfällig.
Bei allen sizeof()s sollte das hoffentlich einsichtig sein (z.B. nicht 4 sondern sizeof(char *)…), aber dann:
Die Puffergröße in der Shell für Eingaben ist z.B. idealerweise 1026. Also am Anfang vom Programm ein #define BUFSIZE 1026 und dann überall wo man das braucht BUFSIZE verwenden und nicht 1026.
Die Returnwerte sind auch ein guter Kandidat. Bei der main() am Besten konsequent IMMER EXIT_SUCCESS oder EXIT_FAILURE und NIE 0 oder 1 verwenden. Und wenn man eigene Rückgabewerte für main definieren möchte (darf man ja auch), dann die auch oben als Konstanten (Makros) definieren.
Aber auch wenn man eine eigene Funktion schreibt, die mehrere oder gar viele verschiedene Rückgabewerte mit eigener Semantik hat. Dann ruhig #define FOOSUCCESS 0, #define FOONOMEM 1, #define FOOBADWEATHER 2 usw.

18. Datentypen haben auch Rechte
Wenn eine Funktion einen bestimmten Datentyp erwartet, dann hat das meistens einen Grund. Die ganzen Speicherverwaltungsfunktionen z.B. erwarten alle eine Größenangabe vom Typ size_t. Sehr beliebt ist aber, ihnen eine Variable vom Typ int zu übergeben. Gut, size_t ist meistens auch int. Aber was wenn nicht? Wenn size_t kleiner als int ist? Dann bekommt malloc() einen völlig anderen Wert übergeben, als man erwartet. Deswegen muss man natürlich nicht Zahlen casten - malloc((size_t)400) ist unnötig, weil das der Compiler eh macht (hier natürlich 17. lesen und überlegen, ob an der Stelle 400 stehen sollte :wink: ). Also bitte in der Manpage nachschauen, was eine Funktion (oder Struktur) für Datentypen erwartet, und diese auch verwenden.
Zu dem Thema passt auch unsigned/signed. Wenn man die miteinander mischt, warnt einen der Compiler meistens. Bevor man jetzt die Warnung einfach durch einen expliziten Cast unterdrückt, sollte man noch mal kurz nachdenken, ob an der Stelle nicht vielleicht doch ein Bug versteckt ist. Z.B. daran denken dass unsigned int viel größere (positive) Zahlen darstellen kann als int. Das gibt sehr lustige Effekte wenn man den Bereich eines Datentyps überschreitet.


Und weiter geht’s mit Aufgabe 4:

19. man-pages lesen, Teil 2
Gut, das war diesmal bei weitem nicht so extrem, wie ich befürchtet hatte. Ich kann mir vorstellen, dass die mitgelieferten Testcases da dem einen oder anderen von euch den richtigen Weg gezeigt haben. :wink:
Konkret geht es mir hier um die diversen Sonderfälle, die bei den Eingabeparametern zu beachten sind: [m]malloc(0)[/m], [m]free(NULL)[/m], [m]realloc(NULL, n)[/m] und [m]realloc(p, 0)[/m].
Außerdem sollte im Fehlerfall das programm explizit nicht abgebrochen und auch keine Fehlermeldung ausgegeben werden, sondern nur die [m]errno[/m] gesetzt und ein [m]NULL[/m]-Zeiger zurückgegeben. Um die endgültige Fehlerbehandlung muss sich dann der Benutzer kümmern, der eure Funktionen in seinem eigenen Programm verwendet!

20. Fehler in [m]realloc()[/m] und [m]calloc()[/m]
Mit [m]realloc()[/m] kann man einen Speicherblock nicht nur vergrößern, sondern auch verkleinern. Falls man nun den Inhalt des alten Blocks in den neuen Block kopiert, muss man dabei als Byteanzahl immer das Minimum der alten und der neuen Blockgröße angeben. Sonst könnten jeweils folgende Situationen auftreten:
a) Man kopiert immer die neue Anzahl von Bytes. Falls man den Puffer vergrößern will und der alte Block am Ende des 1-MB-Segments liegt, liest man bei [m]memcpy()[/m] über dessen Rand hinaus und sorgt dadurch für einen Programmabsturz.
b) Man kopiert immer die alte Anzahl von Bytes. Falls man den Puffer verkleinern will, wird durch [m]memcpy()[/m] der nachfolgende Block und seine Verwaltungsstruktur teilweise überschrieben.

Außerdem muss man sowohl in [m]realloc()[/m] als auch in [m]calloc()[/m] den Rückgabewert des intern verwendeten [m]malloc()[/m]-Aufrufs abfragen. Es kann ja jederzeit sein, dass dieser fehlschlägt, weil nicht genügend Platz frei ist. Wenn man das nicht abfängt, sondern einfach drauflos schreibt (in [m]realloc()[/m] mit [m]memcpy()[/m], in [m]calloc()[/m] mit [m]memset()[/m]), gibt es einen kleinen Programmabsturz…

21. Testcases verwenden
Ich persönlich war vom Ergebnis meiner Gruppe extrem positiv überrascht, deswegen schätze ich, dass die meisten Leute die mitgelieferten Testcases auch verwendet haben. Auf vielfachen Wunsch einer einzelnen Person erwähne ich das aber trotzdem: Wer diesen Luxus nicht ausgenutzt hat, ist selber schuld! :-p
Und falls der Testcase mitten in der Ausführung abgstürzt, ist der [m]gdb[/m] euer bester Freund. Die drei häufigsten Ursachen für Crashes:

  • Zugriff auf [m]NULL-Zeiger[/m] (entweder beim “Suchzeiger” oder beim “Schleppzeiger”).
  • Beim Durchlaufen der Schleife stößt man auf einen [m]0x00beef00[/m]-Zeiger, weil man einen Fehler im [m]free()[/m] hat.
  • Man landet mit einem Zeiger jenseits der 1-MB-Grenze. Das findet man leicht raus, indem man von dieser Adresse den Wert des [m]newmem[/m]-Zeigers abzieht.

22. Dokumentation nicht vernachlässigen
Mit einem kleinen bisschen Hinsetzen und Nachdenken wäre die Bug-Finde-Teilaufgabe eigentlich relativ simpel zu lösen gewesen. Zur Not nimmt man halt mal einen Stift und ein Blatt analoges Papier und pinselt sich hin, welcher Zeiger wohin zeigt und was in welchem Speicherblock steht. :finger:
In meinen Gruppen gab es auf die Teilaufgaben c) und d) insgesamt drei Punkte, das ist dann doch immerhin ein Viertel der Gesamtpunktzahl!


In den letzten zwei Wochen haben mir Zeit und Motivation gefehlt, diesen Thread weiterzuführen. Da hat sich einiges angestaut. :slight_smile:

23. Makefiles: “Umweg” über .o-Datei gehen
Dies ist der Grund, warum bei Aufgabe 5 so gut wie niemand die vollen 4,0 Punkte bekommen hat. Zwar ziemlich pingelig, aber hoffentlich einzusehen: Wenn man in seinen beiden Targets [m]wsort-static[/m] und [m]wsort[/m] jeweils die [m]wsort.c[/m] als Abhängigkeit angegeben hat, bürdet man dem Compiler unnötige Arbeit auf, denn:

  • Für beide Anwendungen müsste die [m]wsort.c[/m] jeweils neu kompiliert werden, obwohl beide sich bekanntlich nur in der Verwendung der halde-Bibliothek unterscheiden.
  • Falls man an seiner Bibliothek etwas ändert, würde das gesamte Programm neu kompiliert werden, obwohl es nur neu gelinkt werden muss.
    Deshalb wird in der Musterlösung ein zusätzliches Target [m]wsort.o[/m] erstellt und dieses für die Anwendungen als Abhängigkeit angegeben.
    An dieser Stelle nochmals der Hinweis: Alle Targets, die keine gleichnamige Datei als Ausgabe erzeugen, müssen als [m].PHONY[/m] markiert werden.

24. SetUID, SetGID und Sticky-Bit nicht vergessen!
Das wurde im [m]printdir[/m] gerne weggelassen, obwohl es eigentlich eindeutig in den Übungsfolien steht.

25. Fehlerbehandlung, die Zweite
Obwohl das immer wieder bis zur Vergasung wiederholt wurde, mache ich das hiermit zum x-ten Male deutlich: Bei allen Funktionen, die laut man-page fehlschlagen können, muss der Rückgabewert abgefragt und falls nötig eine
Fehlerbehandlung durchgeführt werden!
Einzige Ausnahme: Bei Funktionen wie z.B. [m]sigaddset()[/m], die nur dann einen Fehler meldet, wenn man ihr trotteligerweise eine ungültige Signalnummer übergeben hat, kann man die Fehlerabfrage weglassen. Ansonsten gilt (Gebetsmühle): Fehlermeldung nach [m]stderr[/m] ausgeben (meistens bietet sich [m]perror()[/m] an) und dann den Fehler je nach Situation angemessen behandeln.
“Angemessen” bedeutet, dass nicht prinzipiell immer sofort das Programm abgeschossen wird. Gegenbeispiel: Wenn beim [m]printdir[/m] das Einlesen eines Verzeichnisses fehlschlägt (z.B. weil der Zugriff verweigert wurde), bietet es sich an, dieses Verzeichnis zu überspringen und mit dem nächsten fortzufahren.
Nur bei wirklich kritischen Fehlern wie einer fehlgeschlagenen Speicheranforderung muss das Programm beendet werden. In diesem Fall braucht man auch keine aufwändige Speicherbereinigung mehr durchzuführen.

26. Sortierung nach Änderungszeit
Wie man [m]qsort()[/m] verwendet, sollte seit Aufgabe 2 bekannt sein. Die meisten Leute haben es auch richtig gemacht und sich ein Array aus Strukturen zusammengebastelt, die alle wichtigen Informationen (inklusive Modifikationszeit) aus den eingelesenen Verzeichniseinträgen enthalten. Die Vergleichsfunktion muss dann entsprechend zwei Zeiger auf solche Strukturen erhalten und deren Modifikationszeit-Einträge miteinander vergleichen. Wichtig ist hier, dass die jüngste Datei (die mit der höchsten Modifikationszeit) als erstes kommen sollte, d.h. das Array muss absteigend sortiert werden.
Kleine Randnotiz: So etwas wie [m]return (e2->mTime - e1->mTime);[/m] funktioniert auf 32-Bit-Systemen problemlos, während es auf einem 64-Bit-System zu unerwarteten Ergebnissen kommen könnte, falls die eine Datei > 68 Jahre (INT_MAX Sekunden) jünger ist als die andere. Dann gäbe es nämlich einen Überlauf, weil zwei 64-Bit-Zahlen voneinander abgezogen werden und das Ergebnis in eine 32-Bit-Zahl gespeichert wird… (weiterführende Informationen) :smiley:
Ganz streng genommen müsste man es also so machen:

if (e1->mTime > e2->mTime) {
    return -1;
} else if (e1->mTime < e2->mTime) {
    return 1;
} else {
    return 0;
}

Ein paar Leute haben sich auch nur die Dateinamen gemerkt und dann in der Vergleichsfunktion [m]lstat()[/m] aufgerufen. Das ist konzeptionell nicht sehr toll, langsam, und stellt einen vor die Frage, was man macht, falls [m]lstat()[/m] fehlschlagen sollte. <_<

27. Noch mal ein altes Lieblingsthema
Spätestens nach dem fünften Mal, dass der Übungsleiter einem einen halben Punkt abgezogen hat, sollte die Information durchgesickert sein, dass Variablen und Hilfsfunktionen, die nur innerhalb des eigenen Moduls verwendet werden sollen, als [m]static[/m] deklariert werden müssen. :-p
Dummerweise wurde das in der [m]mini_sh[/m], die für Aufgabe 7 vorgegeben war, auch falsch gemacht… :scared:

28. Signalbehandlung
Was viele vergessen haben, obwohl in der Tafelübung deutlich darauf hingewiesen wurde: Wird in einer Signalbehandlungsfunktion ein Bibliotheks- oder Systemaufruf verwendet, der fehlschlagen kann, muss man vorher die [m]errno[/m] sichern und anschließend wiederherstellen.

So viel für heute - Fortsetzung folgt… :cool:


29. Nebenläufigkeitsprobleme in der [m]job_sh[/m]
Eine kurze Auflistung, was man hätte machen sollen:
a) Die Jobliste mit [m]jl_new()[/m] anlegen, bevor der SIGCHLD-Handler installiert wurde.
b) SIGCHLD vorübergehend blockieren, während man im Kommando “jobs” die Liste der aktuellen Hintergrundprozesse durchgeht.
c) Wenn ein Hintergrundprozess gestartet wird: SIGCHLD und SIGINT vor dem [m]fork()[/m] blockieren, damit der Hintergrundprozess nicht abgeräumt werden kann, bevor er überhaupt in die Jobliste eingetragen wurde. Das SIGCHLD kann im Kindprozess sofort wieder deblockiert werden, das SIGINT erst dann, wenn es mittels [m]sigaction()[/m] auf die Ignorierliste gesetzt wurde.
Im Vaterprozess kann SIGINT sofort deblockiert werden, während man SIGCHLD erst wieder dann freigeben kann, sobald man den Kindprozess in die Jobliste eingetragen hat.
d) Bei der vorgegebenen Shell gab es Probleme, wenn ein Hintergrundprozess fertig wurde, während ein Vordergrundprozess am Laufen war - genauer gesagt wurde der Exitstatus des Hintergrundprozesses zusammen mit der Befehlszeile des Vordergrundprozesses ausgegeben. Für diese Problematik gibt es prinzipiell zwei Lösungsmöglichkeiten, gedacht war folgende:
SIGCHLD vor dem [m]fork()[/m] blockieren (damit im Fall, dass der Kindprozess vorzeitig stirbt, das SIGCHLD erst einmal zurückgehalten wird). Im Kindprozess kann man es dann sofort wieder deblockieren; im Vaterprozess wartet man mit [m]sigsuspend()[/m], bis der Signalhandler den Kindprozess aufgeräumt hat. Dazu müsste man sich global die PID und die Kommandozeile des Vaterprozesses merken und in der Signalbehandlungsfunktion überprüfen, ob die von [m]waitpid()[/m] zurückgelieferte PID die des Vaterprozesses ist.
e) Wer es ganz genau nimmt, müsste beim Programmende vor dem Aufruf von [m]jl_destroy()[/m] (den nicht wenige Leute vergessen haben) das SIGCHLD blockieren und danach nicht mehr deblockieren (oder es alternativ ignorieren).

30. SIGCHLD-Handler
Das Warten auf Kindprozesse muss in einer Schleife erfolgen, denn es kann vorkommen, dass nur ein Signal ausgelöst wird, obwohl mehrere Kinder gestorben sind. Man muss also [m]waitpid()[/m] mit der PID [m]-1[/m] (auf alle Kindprozesse warten) und dem Options-Flag [m]WNOHANG[/m] (nicht blockieren, falls kein Zombie gefunden wurde) aufrufen und den Rückgabewert überprüfen. Falls die Rückgabe 0 ist (oder -1 und [m]errno == ECHILD[/m]) liegt kein Zombie (mehr) vor.
Allgemein sollte man beim Einrichten von Signal-Handlern das Flag [m]SA_RESTART[/m] setzen, sonst müsste man streng genommen jeden einzelnen Systemaufruf, der mit [m]EINTR[/m] fehlschlagen könnte, einzeln abprüfen und falls nötig neu aufsetzen.


31. Schließen nicht benötigter Sockets

Das heißt im Klartext: [m]accept()[/m] erzeugt bekanntlich bei der Verbindungsannahme einen neuen zusätzlichen Socket, der für die Kommunikation mit dem verbundenen Client verwendet werden kann, während man auf dem ursprünglichen Socket weiter auf eingehende Verbindungen lauschen kann.
Der Kindprozess soll die aktuelle Uhrzeit verschicken und sich nicht um weitere Verbindungsannahmen kümmern, deswegen muss er mit [m]close()[/m] den „Horch-Socket“ schließen. Der Vaterprozess seinerseits braucht den „Kommunikations-Socket“ nicht mehr und muss ihn schließen.
Hier nochmals der Hinweis: [m]close()[/m] schließt einen Dateiseskriptor nur für den aufrufenden Prozess, aber nicht für andere Prozesse, die auf den selben Deskriptor zugreifen. Im Gegensatz dazu kann man mit [m]shutdown()[/m] den Hahn komplett zudrehen - was uns gleich zum nächsten Punkt bringt. :wink:

32. Umgang mit [m]shutdown()[/m]
Wer in seinem [m]port_forward[/m] kein [m]shutdown()[/m] an den richtigen Stellen verwendet hat und sein Programm ausgiebig getestet hat, dürfte festgestellt haben, dass in manchen Fällen der Kindprozess offen gehalten wurde, obwohl eine der zwei Gegenstellen eigentlich die Verbindung beendet hatte. Der Grund hierfür ist, dass der andere Rechner nicht mitbekommt, dass die Verbindung getrennt wurde und dadurch der zweite Forward-Thread endlos weiter läuft.
Um es einmal grafisch aus Sicht eines der zwei Threads darzustellen:

[Rechner A]--->[port-forward]--->[Rechner B]

Schlägt nun das Lesen von Rechner A fehl, dann ist die Verbindung getrennt. In diesem Fall muss Rechner B Bescheid bekommen, dass (zumindest in diese Richtung) keine Daten mehr laufen können, und zwar indem ein [m]shutdown()[/m] mit dem Parameter [m]SHUT_WR[/m] auf den Schreib-Socket durchgeführt wird.
Analoges gilt, falls das Schreiben auf Socket B fehlschlägt - dann muss Socket A in Leserichtung ([m]SHUT_WR[/m]) geschlossen werden.


push
vl wird damit einigen Fragen vorgebeugt


Hui, der Thread ist wirklich alt. Damals war ich zum allerersten Mal Übungsleiter! :wink:


Die SP Übungen sind dafür scheinbar zeitlos :wink:


@Airhardt: Und ich war dabei :smiley:


kann man den thread nicht sticky machen?


bump

Ziemlich sinnvoller Thread. Schade, dass er so weit hinter gerutscht ist.


Hab mir Mal erlaubt diese wertvollen Tipps in ne PDF zu packen :slight_smile:
http://db.tt/J0ZDeg9z

3 „Gefällt mir“

Bester erster Post ever? Wäre echt sinnvoll, das irgendwo bei den Prüfungen zu verlinken. Danke auf jeden Fall dafür :slight_smile:

** EDIT **
Hab’s jetzt mal bei den Prüfungen als Unterpunkt vermerkt.