4.1 Grundlegende Eigenschaften
Unix und Linux sind Betriebssysteme, die auf der gesamten Bandbreite der heutigen Computer eingesetzt werden können. Im PC Bereich und auf Kleincomputer (Pocket-PCs) wird häufig Linux verwendet. Beim gewerblich genutzten Computer (Großrechner) wird Unix verwendet.
Unix und Linux sind Allzweck-Systeme. Sie sind geeignet für die Entwicklung von System-und Anwendersoftware. Durch eine verständliche und auch oft einfache Programmierschnittstelle ist das Schreiben von Programmen, insbesondere von Anfängern und Privatpersonen, einfacher, als z.B. unter Windows NT.
Beide Betriebssysteme, Unix und Linux, unterstützen Mehrbenutzer- und Mehrprogrammbetrieb. Sie besitzen beide ein geräteunabhäniges hierarchisches Dateisystem. Dienstprogramme, für beide Systeme, sowie Treiber für fast jede Hardware, sind heutzutage in großer Anzahl erhältlich. Linux ist frei erhältlich. Für umfangreiche Komplettpakete (z.B. von SuSE) muß nur ein Bruchteil bezahlt werden, von dem was andere Betriebssysteme kosten. Linux erfreut sich im Privatbereich immer gößerer Verbreitung, da es eine günstige und vollständige Alternative ist.
4.2 Prozesse / Threads in Unix / Linux
Die beiden Betriebssyteme Unix und Linux sind Multitasking-Systeme. Dies bedeutet, daß mehrere Anwendungen und Hintergrundprozesse parallel aktiv sein können. Es ist aber nicht immer möglich, daß eine Anwendung (Prozess) selbst aus mehreren "parallel" ablaufenden Teilen besteht. Auf klassischen Unix-Systemen werden derartige Probleme so gelöst, daß ein Programm mehrere Prozesse erzeugt, die dann in einer eigenen Umgebung, mit einem eigenen Speicherbereich etc. ablaufen und über die üblichen Inteprozesskommunikationsprimitiven miteinander kommunizieren. Diese Prozesse werden Kind-Prozesse genannt. Die Verwaltung solcher Kind-Prozesse kostet jedoch Zeit und Systemressourcen.
In Linux kann man, da die Kind-Prozesse einer Anwendung inhaltlich eng aneinander gebunden sind und auch auf gemeinsame Variablen zugreifen sollten, die sogenannten Threads (Light-Weight-Processes) verwenden.
Threads unter Linux kann man gemeinsame Speicherbereiche zuteilen. Bestimmte Speicherbereiche können als sog. "Shared Memory" vereinbart werden.
System intern wird in Linux allerdings kein Unterschied zwischen einem Thread und einem Prozess gemacht. Ein Thread ist für Linux ein Prozess, der seinen Kontext ganz oder teilweise mit einem anderen Prozess teilt.
In dieser Diplomarbeit wird zum "parallelen" Abarbeiten von Programmteilen die klassische Methode, die Erzeugung von mehreren Kind-Prozessen, besprochen. Diese Art kann sowohl bei allen gängigen Unix-Systemen als auch bei einem Linux-System angewendet werden.
Abb. 4.1: Zustandsdiagramm von Prozessen in Unix / Linux [Vogt 2001]
Während seiner Existenz kann ein Prozess in Unix / Linux mehrere Zustände annehmen. Ein Zustandswechsel wird dabei durch ein Ereignis ausgelöst.
Der Zustand rechnend besagt,
daß der Prozess gerade von der CPU bearbeitet wird.
Der Zustand bereit besagt, daß der Prozess zwar bereit
ist, aber auf die Benutzung der CPU wartet.
Der Zustand blockiert besagt, daß der Prozess auf das
Eintreten eines bestimmten Ereignisses wartet (z.B.
Benutzereingabe) bevor er weiter ausgeführt werden kann.
Bei Unix / Linux gibt es neben den oben genannten Zuständen eine
etwas abgewandelte Version. Der Zustand rechnend wird
aufgeteilt in einen User Mode und einen
Kernel Mode. Der Übergang zwischen diesen beiden
Zuständen erfolgt durch den Aufruf einer Systemfunkion bzw. die
Rückkehr von derselben. Man beachte, daß der Übergang in einen
anderen Zustand nur aus dem Kernel Mode möglich ist und
nicht aus dem User Mode.
Ein weiterer Zustand ist Zombie. In diesen Zustand gelangen Prozesse, wenn z.B. ein Sohnprozess terminiert und der Vater noch keinen wait() Aufruf durchgeführt hat, da erst dann ein Prozess vollständig gelöscht wird. Terminiert ein Vaterprozess ohne wait() Aufruf, so erbt der Init-Prozess diese Prozesse und sorgt durch wait() Aufrufe für die vollständige Löschung dieser Zombies nachdem sie terminiert sind.
Es werden zwei Arten von Scheduling unterschieden, unterbrechendes und nicht unterbrechendes Scheduling. Unix und Linux benutzen ein unterbrechendes Scheduling, so das ein Prozess vom Betriebssystem unterbrochen werden kann, damit ein anderer Prozess von der CPU ausgeführt wird. Beide Betriebsysteme arbeiten allerdings mit unterschiedlichen Systemen. Unix benutzt ein prioritätgesteuertes Scheduling bei dem die Priorität der einzelnen Prozesse vom System anhand mehrerer Faktoren in bestimmten Zeitabständen berechnet wird. Diese Faktoren sind z.B. wie rechenintensiv der Prozess bis zu diesem Zeitpunkt war oder wie lange der Prozess schon warten mußte. Der nice Wert, dem man beim Starten eines Prozesses vorgeben kann wird ebenfalls in diese Berechnung einbezogen. In Unix bedeutet i.a. ein höherer Prioritätswert eine niedrige "Dringlichkeit".
Linux verwendet eine Mischung aus verschiedenen Schedulingklassen.
Diese Klassen sind:
Linux unterscheidet hierbei zwei Klassen von Prozessen dringende "Realzeitprozesse" und weniger dringende "Prozesse". Realzeitprozesse besitzen eine Realzeitpriorität, die entscheidet in welcher Reihenfolge die Prozesse abgearbeitet werden. Je größer dieser Realzeitprioritäswert, desto "dringender" ist dieser Prozess. Diese Prozesse werden in der SCHED_FIFO und SCHED_OTHER Klasse verwaltet. Weniger "dringende" Prozesse erhalten die Realzeitpriorität 0 und werden in der Schedulingklasse SCHED_OTHER verwaltet. Diese Prozesse erhalten erst dann Rechenzeit, wenn keine Realzeitprozesse bearbeitet werden müssen [Vogt2001].
Obwohl beide Betriebssysteme unterschiedliche Methoden anwenden haben sie das gleiche Ziel. Sie versuchen alle Prozesse so zu koordinieren, das auch, wenn das System ausgelastet ist, möglichst jeder Prozess, auch wenn es ein weniger wichtiger ist, die CPU zugeteilt bekommt wenn dadurch "dringende" Prozesse nicht behindert werden.
4.5 Erzeugung und Steuerung von Prozessen
Prozesse werden in Unix erzeugt, indem ein bestehender Prozess die Funktion fork() aufruft. Das gilt nicht für Prozesse, die vom Systemkern automatisch beim Hochlaufen des Systems erstellt werden, wie z.B. den Swapper.
Abb. 4.2: Erzeugen eines Sohnprozesses und Warten auf dessen Beendigung [GuOb1995]
Der neue, von fork() kreierte Prozess, wird Kindprozess genannt. Er ist eine Kopie des aufrufenden Prozesses und erhält z.B. eine Kopie des Datenbereiches des Elternprozesses. Die beiden Prozesse teilen sich aber diese Speicherbereiche nicht. Die Funktion wird nur einmal aufgerufen, liefert aber zwei Rückgabewerte. Dem Kindprozess wird der Wert 0, dem Elternprozess dagegen die Prozessidentifikationsnummer (PID) des Kindprozesses übergeben. Die PID des Kindprozesses wird dem Elternprozess übergeben, da ein Elternprozess über mehrere Kindprozesse verfügen kann. Es gibt keine zusätzliche Funktion, mit der ein Elternprozess die PID der Kindprozesse ermitteln kann. Ein Kindprozess kann aber immer nur einen Elternprozess haben, daher erhält der Kindprozess immer den Rückgabewert 0.
Da fork() eine genaue Kopie des aufrufenden Prozesses erzeugt und startet, ist die PID der einzige Unterschied. Anhand der unterschiedlichen PID's, die Vater- und Sohnprozess besitzen, können sie in verschiedene Programmstücke verzweigen.
Ein Prozess beendet sich selber, wenn er das Ende des Programmcodes erreicht oder die Schnittstelle exit() aufruft. Bei dem Aufruf exit() wird ein ganzzahliger Parameter (z.B. als Fehlercode) an den Vaterprozess übergeben.
Prozesse können aber auch durch andere Prozesse mit Hilfe von Signalen beeinflußt werden, falls sie die Berechtigung dazu haben. Die Schnittstellenfunktion hierfür lautet kill().
Signale können auch mit der Funktion pause() zur Synchronisation eingesetzt werden (siehe Kapitel 4.7.2 Synchronisation durch Signale).
Einige Schnittstellenfunktionen im Zusammenhang mit Prozessen sind:
execv | Aufruf eines Programmes aus einem Prozess |
Prototyp: | int execv ( char *pfad, char *argv[ ] ); |
Parameter: | char *pfad | Name bzw. Pfad des Programms |
char *argv[] | Zeiger auf das Feld, in dem die Argumente
für das aufgerufene Programm steht. Das letzte Element muss ein NULL-Pointer sein. |
exit | Beendet den Prozess. |
Prototyp: | void exit ( int status ); |
Parameter: | int status | Status der zurückgegeben wird (0 = OK) |
Beispiel: | exit(0); /* Fehlerfreie Terminierung des Prozesses */ |
fork | Starten einen neuen Prozess. |
Prototyp: | int fork ( ); |
Rückgabe: | 0 an Kindprozess, Prozessidentifikationsnummer (PID) des Kindes an Elternprozess, -1 bei Fehler. |
Beispiel: | int kind_PID; if ((kind_PID= fork()) == 0) { ** Programmcode wird vom Sohnprozess ausgeführt ** } |
getpid | Liefert die Prozessidentifikationsnummer (PID) des aufrufenden Prozesses bzw. -1 bei einem Fehler. |
Prototyp: | int getpid ( ); |
Beispiel: | printf("Meine PID ist:
%i\n",getpid()); /* Gibt die eigene PID aus */ |
getppid | Liefert
die Prozessidentifikationsnummer des Vaterprozesses (PPID) bzw. -1 bei einem Fehler. |
Prototyp: | int getppid ( ); |
Beispiel: | printf("Die PID meines
Vaters ist: %i\n",getppid()); /* Gibt PID des Vaters aus */ |
kill | Das
Signal sig wird durch diese Funktion
an den Prozess mit der Prozessidentifikationsnummer pid geschickt. |
Includes: | #include <signal.h> |
Prototyp: | int kill ( int pid, int sig ); |
Parameter: | int pid int sig | PID
des Empfänger-Prozesses Signalnummer SIGKILL bzw. 9 terminiert den Prozess, ohne daß er die Möglichkeit hat, das Signal abzufangen. Andere Signale wie z.B. SIGUSR1 können vom Empfängerprozess abgefangen werden und bewirken eine Ausführung des "Signal-Handlers"eine vom Benutzer definierte Funktion. |
Beispiel: | int kind_PID; kill (kind_PID,SIGKILL); |
/* terminiert den Prozess, dessen PID in der Variable kind_PID gespeichert ist. */ |
Beispiel zur Synchronisation: siehe Kapitel 4.7.2 Synchronisation durch Signale.
pause | Der Prozess wird angehalten und wartet auf ein Signal. |
Prototyp: | int pause ( ); |
Beispiel: | siehe Synchronisation durch Signale. |
signal | Diese Funktion bindet das Signal sig an einen Signal Handler. |
Includes: | #include <signal.h> |
Prototyp: | void signal ( int sig, int *sighand ); |
Parameter: | int sig int *sighand |
Signal,
welches an den Signal Handler gebunden werden soll. Signal Handler der ausgeführt werden soll. |
Beispiel: | siehe Synchronisation durch Signale. |
sleep | Der aufrufende Prozess blockiert für eine bestimmte Zeit.. |
Prototyp: | unsigned sleep ( int sec ); |
Parameter: | int sec | Dauer in Sekunden. |
Beispiel: | sleep(10); /* Prozess blockiert 10 Sekunden*/ |
wait | Es
wird auf die Beendigung eines Sohnprozesses gewartet.Hat
ein oder mehrere Sohnprozesse bereits terminiert, so
kehrt der Aufruf sogleich zurück. Ist dies nicht der Fall wird auf die Beendigung des nächsten Sohnprozesses gewartet. |
Prototyp: | int wait ( int *statusp ); |
Parameter: | int *statusp | Zeiger auf Variable, in der der Terminierungsstatusdes Sohnes zurückgegeben wird. Benötigt man den Rückgabestatus nicht, kann 0 als Parameter benutzt werden. |
Rückgabe: | PID
des terminierten Sohnes bzw. -1 wenn kein Sohnprozess
existiert oder bereits terminierte Söhne durch frühere
wait() Aufrufe entgegengenommen wurden. |
waitpid | Es wird auf die Beendigung eines bestimmten Sohnprozesses gewartet. |
Prototyp: | int waitpid ( int pid, int * statusp, int optionen ); |
Parameter: | int *statusp int pid int optionen |
siehe
wait() Aufruf pid > 0, PID des Prozesses auf den gewartet werden soll. Bei -1 wird auf die Beendigung eines beliebigen Sohnprozesses gewartet. Der Parameter bestimmt wie und worauf gewartet werden soll. Er ist aber abhänig davon ob das System z.B. eine Job-Kontrolle unterstützt. |
Beispiel: | watipid(pid,0,0); /* Prozess wartet bis
der Sohnprozess mit der PID = pid terminiert */ |
4.6 Hintergrundprozesse "Dämons"
Dämons werden häufig schon beim
Laden des Systems gestartet und erst beim Systemabschluß
beendet. Sie laufen sozusagen im Hintergrund, da sie über kein
steuerndes Terminal verfügen. Sie werden häufig für
Systemaktivitäten benötigt, die nebenläufig zu
Benutzerprogrammen ablaufen z.B. warten auf Mails.
Ein Dämon entsteht auch, wenn durch fork() ein
Sohnprozess gestartet wurde und der Eltenprozess beendet wird
bevor der Sohnprozess terminiert. Dies kann mit dem Aufruf von wait() vor dem Beenden des Vaterprozesses verhindert
werden.