OSUE Einführungstutorial

Denise Ratasich edited this page Mar 1, 2017 · 5 revisions

Laborumgebung

Im TILAB sind vergleichsweise moderne GNU/Linux-Desktop-PCs verfügbar. Die voreingestellte Shell (das Programm, das u.a. die Eingabeaufforderung zur Verfügung stellt) ist die bash; als PDF-Reader ist Evince (evince) installiert. Bei Webbrowsern hat man die Wahl zwischen Firefox (firefox), Google Chrome (google-chrome), Opera (opera) sowie Konqueror (konqueror).

Editoren

Für erfahrene Unix-Benutzer sind die Editoren vim und gvim (vim, gvim), sowie Emacs (emacs) installiert. Etwas einsteigerfreundlichere Editoren fürs Terminal sind pico (pico) und sein großer Bruder GNU nano (nano); für die grafische Benutzeroberfläche dann gedit (gedit), KWrite und Kate (kwrite, kate).

Fernzugriff

Auf die TILAB-Rechner ist auch der Zugriff von außen möglich; dies geschieht über das SSH-Protokoll. Unter Unix-artigen Systemen (z.B. Linux, Mac OS X) kann OpenSSH, das oftmals bereits vorinstalliert ist, verwendet werden; unter Windows bietet sich das Programm PuTTY an.

Der OpenSSH-Client lässt sich mit dem Befehl ssh _benutzername_@ssh.tilab.tuwien.ac.at starten. Der Benutzername lautet normalerweise y_matrikelnummer_. (Beispiel: ssh y0925631@ssh.tilab.tuwien.ac.at) Die Abfrage betreffend Authentizität kann meist getrost mit yes beantwortet werden; das Kennwort, sofern nicht bereits geändert, steht am Abriss, der bei der Accountabholung ausgehändigt wurde. Bei der Eingabe des Kennwortes erscheinen keine Sternchen oder Punkte; das Terminal bleibt stumm, bis Enter gedrückt wurde. Wenn alles erfolgreich verlaufen ist, wird die Shell gestartet und es erscheint eine Eingabeaufforderung wie auf einem lokalen Terminal.

Bei PuTTY wird das Feld Host Name (or IP Address) wie bei OpenSSH mit _benutzername_@ssh.tilab.tuwien.ac.at ausgefüllt; unter Protocol ist SSH zu wählen. Nach einem Klick auf Open wird eine Verbindung zum Server aufgebaut und eine Kennwortabfrage angezeigt. Ab dort verhält sich PuTTY annähernd genauso wie OpenSSH.

Es ist egal, welcher der 34 Rechner gewählt wird. Der Inhalt des eigenen Home-Ordners, die eigenen Daten also, sind an einem zentralen Server gespeichert und werden von allen Rechnern gleichermaßen eingebunden. Eine Aufteilung ist trotzdem gewünscht: wenn sich mehrere Leute mit demselben Computer verbinden, ist er stärker ausgelastet und wird für alle eingeloggten User „langsamer“.

Kennwortänderung

Das Kennwort kann im Terminal mit Hilfe des Programms passwd geändert werden. Dieses fragt zuerst nach dem bestehenden Kennwort und dann zweimal nach einem neuen. Bei der Eingabe sämtlicher Kennwörter bleibt das Terminal stumm; es erscheinen, wie bei SSH, keine Sternchen oder Punkte. Sollte man ein zu einfaches Kennwort gewählt haben, wird es mit einer entsprechenden Meldung abgewiesen.

Ein einfaches C-Programm

Ein minimales C-Programm sieht wie folgt aus:

int main(void)
{
    return 0;
}

Die Funktion main hat eine Sonderstellung: sie wird im Programm als erste aufgerufen. Sie gibt eine Ganzzahl (int) zurück und nimmt zumindest in diesem Beispiel keine Argumente entgegen. (Aus historischen Gründen würden leere Parameter-Klammern bedeuten, dass die Funktion „irgendwas“ entgegen nimmt; das void ist notwendig, um zu unterstreichen, dass sie gar keine Parameter akzeptiert.) Der Rückgabewert ist 0, wenn das Programm erfolgreich ausgeführt wurde, und ein anderer Wert, falls ein Fehler auftrat (nach dem für Unix typischen Motto ein Programm kann nur auf eine Art und Weise erfolgreich sein, aber auf abertausende Arten von Problemen stoßen).

Das Programm wird nun in einer Datei namens cexample.c abgespeichert.

Compiler-Aufruf

Der entsprechende Compiler-Aufruf sieht wie folgt aus:

gcc -std=c99 -pedantic -Wall -D_DEFAULT_SOURCE -o cexample cexample.c

Diese lange Wurst besteht aus folgenden Zutaten:

  • gcc: der Name des Compilers (GNU C Compiler)
  • -std=c99: verwende den C99-Standard und keinen anderen (C90, C11, K&R C, usw.)
  • -pedantic: nimm den angegebenen Standard sehr ernst; gib eine Warnung aus, falls vom Standard nicht vorgesehene Features verwendet werden
  • -Wall: aktiviere alle sinnvollen Warnungen
  • -D_DEFAULT_SOURCE: aktiviert POSIX 2008 (leider versteckt -std=c99 einige Funktionen, die für die Entwicklung unter Unix ganz nützlich sind; diese werden von diesen Parametern wieder aktiviert; siehe auch man 7 feature_test_macros oder glibc Feature-Test-Macros)
  • -o cexample: die Datei, in die das kompilierte Programm gespeichert wird (hier cexample)
  • cexample.c: die Quelldateien des Programms

Es gibt einen Unterschied zwischen Fehlern (errors) und Warnungen (warnings): Fehler werden vom Compiler gemeldet, wenn der Code ungültig ist; Warnungen werden gemeldet, wenn der Code zwar gültig ist, aber „ich bin mir nicht sicher, dass du wirklich das gemeint hast, was du geschrieben hast“). Notizen (notes) enthalten weiterführende Informationen zu Fehlern oder Warnungen. Bitte beachten Sie, dass wir bei Ihren Programmen auf einer Kompilierung ohne Fehlermeldungen oder Warnungen bestehen. Versuchen Sie also bitte noch vor der Abgabe Ihr Programm in der TILab-Umgebung zu kompilieren (siehe auch den Abschnitt Fernzugriff).

Wenn der Compiler keine Meldung ausgibt, verlief die Kompilierung problemlos.

Danach kann das Programm mit ./cexample ausgeführt werden – es gibt aber (noch) nichts aus.

Hello, World!

Erweitern wir das Programm nun um eine textuelle Ausgabe.

int main(void)
{
    printf("Hello, world!\n");
    return 0;
}

Die Funktion printf erlaubt eine formatierte Ausgabe, kann aber auch für das einfache Ausgeben von Strings verwendet werden. Jedoch gibt der Compiler nun folgende Warnung aus:

cexample.c: In function ‘main’:
cexample.c:3: warning: implicit declaration of function ‘printf’
cexample.c:3: warning: incompatible implicit declaration of built-in function ‘printf’

Vor allem die dritte Zeile mutet an, dass der Compiler die Funktion kennt, wir aber noch eine entsprechende Header-Datei einbinden müssen. Nur wo finden wir diese Information?

Manpages

Die Manpages (manual pages, haben also nichts mit Männern zu tun) sind das mitgelieferte Handbuch eines Unix-Systems. Darin sind unter anderem auch alle Funktionen der C-Bibliothek dokumentiert.

Wir versuchen also, die printf-Manpage mit dem Befehl man printf zu öffnen. Nach einer kurzen Lektüre stellt sich jedoch heraus, dass das nicht die erwünschte Seite ist – hier wird der Shell-Befehl printf beschrieben. In der Kopfzeile steht PRINTF(1), also der Artikel printf aus dem Buch 1 (dem Buch der Shell-Befehle). Funktionen, die für C relevant sind, befinden sich jedoch in den Büchern 2 (Systemaufrufe) und 3 (Bibliotheksaufrufe). Generell empfiehlt es sich, zuerst im Buch 2 und dann im Buch 3 nachzuschlagen.

man 2 printf findet nichts, also versuchen wir man 3 printf und kommen zur erwünschten Manpage. Oben steht #include <stdio.h>; wir müssen also unseren Quellcode wie folgt anpassen:

#include 

int main(void)
{
    printf("Hello, world!\n");
    return 0;
}

Der Betrachter der Manpages ist less und unterstützt folgende Kommandos:

  • q: beenden
  • /bla: nach dem nächsten Vorkommen des Textes bla suchen
  • n: zum nächsten Treffer springen
  • N: zum vorigen Treffer springen
  • ?bla: wie /bla, nur rückwärts suchen (auch die Bedeutung von n und N wird vertauscht!)

Es ist vom Vorteil, sich ausgiebig mit den Manpages auseinanderzusetzen. Sie sind das einzige erlaubte Hilfsmittel bei den Tests!

Das Programm kompiliert nun ohne Warnungen und gibt, wie erwünscht, Hello, world! aus.

make

Natürlich nervt es, jedes Mal den ellenlangen Compileraufruf eingeben zu müssen, um zu einem ausführbaren Programm zu gelangen. Abhilfe schafft das Programm make, das ein „Kochrezept“ aus der Textdatei namens Makefile einliest und abarbeitet.

Eine Makefile sieht im Allgemeinen wie folgt aus:

variable1=wert1
variable2=wert2

# Kommentar

ziel1: quelle1 quelle2 quelle3
    befehl1
    befehl2

ziel2: quelle1 quelle4 quelle5
    befehl1
    befehl2

Die minimale Makefile für cexample sieht wie folgt aus:

cexample: cexample.c
    gcc -std=c99 -pedantic -Wall -D_DEFAULT_SOURCE -o cexample cexample.c

Ein einfacher Aufruf von make reicht, und das Programm wird kompiliert. (Falls die Datei cexample neuer ist als die Datei cexample.c, macht make jedoch nichts, damit keine Zeit grundlos verbraten wird.)

Eine „gute“ Makefile stellt konventionell zumindest zwei (virtuelle) Ziele zur Verfügung: all und clean. all sorgt dafür, dass auch in einem Projekt mit mehr als einem Programm sämtliche Programme kompiliert werden. clean löscht alle Produkte und Zwischenprodukte, die während des Kompilierungsprozesses erstellt wurden.

Wenn kein Argument an make übergeben wird, wird einfach das erste Ziel in der Makefile abgearbeitet. Es ist demnach empfohlen, all als erstes Ziel anzuführen. Eine entsprechend erweiterte Makefile sieht wie folgt aus:

all: cexample

cexample: cexample.c
    gcc -std=c99 -pedantic -Wall -D_DEFAULT_SOURCE -o cexample cexample.c

clean:
    rm -f cexample

(Das -f (force) im rm-Aufruf sorgt dafür, dass rm auch dann erfolgreich beendet wird, wenn die Datei, oder eine der Dateien, nicht (mehr) existiert.)

Größere Projekte (Compiler vs. Linker)

Stellen wir uns nun ein Projekt vor, das aus drei Quelldateien besteht: common.c, client.c und server.c. Daraus sollen nun zwei Binaries werden: client und server, wobei neben der jeweils entsprechenden Quelldatei beide Binaries von common.c abhängen.

Eine Makefile nach üblichem Schema würde wie folgt aussehen:

all: client server

client: client.c common.c
    gcc -std=c99 -pedantic -Wall -D_DEFAULT_SOURCE -o client client.c common.c

server: server.c common.c
    gcc -std=c99 -pedantic -Wall -D_DEFAULT_SOURCE -o server server.c common.c

clean:
    rm -f client server

Dies ist etwas ineffizient. Bereits ein einfaches #include <stdio.h> bewirkt, dass der Text, den der Compiler „durchlesen und verstehen“ muss, um etwa 500 Zeilen länger wird. Wenn wir nun die Datei common.c zweimal kompilieren, geht damit viel Zeit verloren; außerdem wird sie jedes mal mitkompiliert, wenn sich die client.c oder die server.c ändern.

Um dieser Situation etwas entgegenzuwirken, ermöglichten die C-Entwickler eine zweistufige Kompilierung:

  1. Die eigentliche Kompilierung. Aus den Quelldateien werden Objektdateien erstellt, die bereits den kompilierten Programmcode, aber nur leere Verweise auf externe Funktionen enthalten.
  2. Das Linken. Aus den Objektdateien wird eine ausführbare Binärdatei erstellt, indem der Code aus allen Objektdateien zusammengeführt und die Verweise auf externe Funktionen entsprechend angepasst werden.

Eigentlich ist genau das bereits früher passiert, aber gcc hatte sich darum gekümmert, dieses Detail weg zu abstrahieren. Die expliziten Aufrufe des Compilers und des Linkers sehen wie folgt aus:

gcc -std=c99 -pedantic -Wall -D_DEFAULT_SOURCE -c -o server.o server.c
gcc -std=c99 -pedantic -Wall -D_DEFAULT_SOURCE -c -o common.o common.c
gcc -o server server.o common.o

In den ersten zwei Zeilen ist der (subtile) Unterschied das Flag -c, welches kompilieren aber nicht linken heißt. Damit ist es vor allem nunmehr nötig, die Datei common.c nur einmal zu kompilieren und dann einfach in die zwei Binaries einzubinden.

Eine entsprechend angepasste Makefile sieht wie folgt aus:

all: client server

client: client.o common.o
    gcc -o client client.o common.o

server: server.o common.o
    gcc -o server server.o common.o

client.o: client.c
    gcc -std=c99 -pedantic -Wall -D_DEFAULT_SOURCE -c -o client.o client.c

common.o: common.c
    gcc -std=c99 -pedantic -Wall -D_DEFAULT_SOURCE -c -o common.o common.c

server.o: server.c
    gcc -std=c99 -pedantic -Wall -D_DEFAULT_SOURCE -c -o server.o server.c

clean:
    rm -f client server
    rm -f client.o server.o common.o

Der Anblick dieser Makefile ist grauenvoll. Auch hier ist Copy-Paste unerwünscht, und zum Glück gibt es auch hier Abhilfe. So lässt sich eigentlich sagen, dass jede Objektdatei von einer gleich benannten Quellendatei abhängt, und die Umwandlung geschieht, den Dateinamen ausgenommen, nach dem selben Schema. make bietet hierzu sinnvolle Platzhalter:

%.o: %.c
    gcc -std=c99 -pedantic -Wall -D_DEFAULT_SOURCE -c -o $@ $^

Diese Regel trifft nun auf alle Objektdateien zu, für die eine gleich benannte Quellendatei existiert (oder durch eine andere Regel erstellt werden kann) und für die es keine explizite Regel gibt. Der Platzhalter $@ steht für alle Ziele, der Platzhalter $^ für alle Quellen (wie sie oben im Regelkopf angeführt sind). Die $-Platzhalter lassen sich auch in „normalen“ Regeln anführen; unsere Makefile sieht jetzt also wie folgt aus:

all: client server

client: client.o common.o
    gcc -o $@ $^

server: server.o common.o
    gcc -o $@ $^

%.o: %.c
    gcc -std=c99 -pedantic -Wall -D_DEFAULT_SOURCE -c -o $@ $^

clean:
    rm -f client server
    rm -f client.o server.o common.o

Doxygen

Oft ist die einzige verfügbare Dokumentation aus dem Kontext gerissen, sodass ein mühsames Suchspiel notwendig ist, um zu jedem Stück Dokumentation das passende Stück Code zu finden und umgekehrt.

In der Java-Welt hat das Tool Javadoc dieses Problem größtenteils eliminiert: Dokumentation ist da, wo sie hingehört, und kann im Laufe des Programmierens ähnlich wie der Programmcode angepasst werden. Ein Javadoc-ähnliches Tool gibt es auch für C und andere Programmiersprachen: Doxygen.

Bevor Doxygen ausgeführt werden kann, braucht es eine Konfigurationsdatei. In Anlehnung an makes Makefile heißt Doxygens Konfigurationsdatei Doxyfile und ist ebenfalls eine Textdatei. Eine Vorlage dafür kann mit dem Befehl doxygen -g Doxyfile generiert werden, danach sind jedoch drei Anpassungen nötig:

  1. Die Variable INPUT muss auf den/die Ordner, die den Quellcode enthalten, eingestellt werden. Wenn sich die Doxyfile im selben Ordner wie der Quellcode befindet, kann der aktuelle Ordner mit dem Wert . (Punkt) angedeutet werden.
  2. Die Variable FILE_PATTERNS enthält die Namensmuster der Dateien, die von Doxygen verarbeitet werden sollen. Für die Entwicklung mit C ist *.c *.h optimal.
  3. Die Variable EXTRACT_STATIC bestimmt, ob die Dokumentation auch statische Variablen und Funktionen enthalten soll. In dieser Übung ist der Wert YES empfohlen, da die meisten Funktionen/Variablen static sein werden.

Natürlich können auch andere Änderungen vorgenommen werden; diese sprengen jedoch den Rahmen des Einführungstutorials.

Auch das Format der Kommentare mutet eine Verwandtschaft mit Javadoc an: mehrzeilige C-Kommentare mit zwei statt einem Sternchen am Anfang.

#include 

/*
 * This is not a Doxygen comment, but the next one is.
 */

/**
 * The main entry point of the program.
 *
 * @param argc The number of command-line parameters in argv.
 * @param argv The array of command-line parameters, argc elements long.
 * @return The exit code of the program. 0 on success, non-zero on failure.
 */
int main(int argc, char **argv)
{
    printf("Hello, world!\n");
    return 0;
}

Wenn wir nun doxygen ausführen, erstellt es zwei Unterordner: html und latex. Ersterer enthält HTML-Dateien, die im Webbrowser betrachtet werden können; letzterer LaTeX-Quelldateien. Wenn wir nun im html-Unterordner die Datei index.html öffnen, sehen wir... nichts.

Doxygen verarbeitet eine Datei nur, wenn sie am Anfang ein Kommentar mit dem @file-Tag enthält. Wenn unsere Quellendatei also cexample.c heißt, müssen wir sie wie folgt erweitern:

/**
 * @file cexample.c
 */

#include 

/*
 * This is not a Doxygen comment, but the next one is.
 */

/**
 * The main entry point of the program.
 *
 * @param argc The number of command-line parameters in argv.
 * @param argv The array of command-line parameters, argc elements long.
 * @return The exit code of the program. 0 on success, non-zero on failure.
 */
int main(int argc, char **argv)
{
    printf("Hello, world!\n");
    return 0;
}

Und wenn wir schon dabei sind, können wir auch Autor und Datum hinzufügen. (Wenn wir unseren Code in einem SCM-System speichern, ist das in vielen Fällen nicht unbedingt sinnvoll, aber da die Betriebssysteme-Abgaben nicht über ein SCM stattfinden, machen wir es an dieser Stelle.)

/**
 * @file cexample.c
 * @author Ondřej Hošek (0925631) 
 * @date 2012-03-15
 */

#include 

/*
 * This is not a Doxygen comment, but the next one is.
 */

/**
 * The main entry point of the program.
 *
 * @param argc The number of command-line parameters in argv.
 * @param argv The array of command-line parameters, argc elements long.
 * @return The exit code of the program. 0 on success, non-zero on failure.
 */
int main(int argc, char **argv)
{
    printf("Hello, world!\n");
    return 0;
}

Beim nächsten Ausführen von Doxygen sehen wir zwar wieder eine leere Seite, doch nun ist oben eine Registerkarte Files dazugekommen, wo wir dann auch zwischen den einzelnen Quellcode-Dateien wählen und ihre Dokumentation betrachten können.

Hinweise zur Lehrveranstaltung

Nachfolgend einige nützliche Tipps zum Bestreiten der Betriebssysteme-Übung:

  • Das erste Beispiel besteht aus zwei Teilen. Teil 1A ist eine Einführung in das Programmieren mit C; Teil 1B in die Netzwerkprogrammierung mit Sockets.
  • Werfen Sie öfters einen Blick auf die LVA-Homepage, insbesondere den Abschnitt Aktuelles!
  • Nutzen Sie das Betreuungsangebot. Annähernd jede Woche sind zu bestimmten Uhrzeiten Tutoren im Labor anwesend, die Ihnen beim Lösen der Übungen und Verstehen des Stoffs behilflich sein können.
  • Beachten Sie vor der Abgabe eines Übungsbeispiels die Richtlinien. Sie können sich damit manch einen Punkteabzug ersparen.
  • Ein Übungstest steht auf der OSUE-Website zur Verfügung.
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.