2.1. Betriebssystemarchitektur

Unter einem Betriebssystem versteht man alle Softwarekomponenten, die

steuern und überwachen.

Diese Komponenten lassen sich unterteilen in den Kernel (Betriebssystemkern) und in sonstige Betriebssystemkomponenten. Der Betriebssystemkern ist ein Programm, das sämtlichen Applikationen Dienste in Form so genannter Systemcalls zur Verfügung stellt; dies gilt insbesondere auch für Betriebssystemapplikationen. Mit solchen Diensten lassen sich beispielsweise Daten schreiben und lesen, Daten auf einem Bildschirm oder einem Drucker ausgeben, oder Daten von einer Tastatur oder einem Netzwerk-Interface entgegennehmen. Die sonstigen Betriebssystemkomponenten nutzen die Dienste, um damit Systemkonfigurationen vorzunehmen oder einem Anwender die Möglichkeit zu geben, seine Programme zu starten und ablaufen zu lassen.

2.1.1. Komponenten des Kernels

Abbildung 2-1. Betriebssystem-Architektur

In der Abbildung Betriebssystem-Architektur ist vereinfacht ein Rechensystem dargestellt. Die grau unterlegten Subsysteme stellen das Betriebssystem dar, wobei sich im unteren Teil der Kernel, im oberen die sonstigen Betriebssystemkomponenten (Services und Bibliotheken) befinden.

Der Betriebssystemkern besteht damit im Wesentlichen aus den folgenden Komponenten, die im Anschluss genauer vorgestellt werden:

Systemcall-Interface

Applikationen können die Dienste, die ein Betriebssystem zur Verfügung stellt, über das Systemcall-Interface in Gebrauch nehmen. Technisch ist diese Schnittstelle über Software-Interrupts realisiert. Möchte eine Applikation einen Dienst (zum Beispiel das Lesen von Daten aus einer Datei) nutzen, löst sie einen Software-Interrupt aus und übergibt dabei Parameter, die den vom Kernel auszuführenden Dienst hinreichend charakterisieren. Der Kernel selbst führt nach Auslösen des Software-Interrupts die zugehörige Interrupt-Service-Routine (ISR) aus und gibt der aufrufenden Applikation einen Rückgabewert zurück.

Das Auslösen des Software-Interrupts wird im Regelfall durch die Applikationsentwickler nicht selbst programmiert. Vielmehr sind die Aufrufe der Systemcalls in den Standardbibliotheken versteckt und eine Applikation nutzt eine dem Systemcall entsprechende Funktion in der Bibliothek.

Die Anwendungen fordern die Dienste des IO-Managements über Systemcalls an. Damit wird bei einer Anwendung nicht nur der Code abgearbeitet, der vom Programmierer erstellt wurde, sondern auch der Code, der über die Bibliotheken der eigenen Applikation hinzugebunden wurde, sowie der Kernel-Code, der bei der Abarbeitung eines Systemcalls ausgeführt wird.

Welche Systemcalls in Linux vorhanden bzw. implementiert sind, ist in der Datei <asm/unistd.h> aufgelistet. Hier ein Ausschnitt:

...
#define __NR_exit         1
#define __NR_fork         2
#define __NR_read         3
#define __NR_write        4
#define __NR_open         5
...

Der Systemcall mit der Nummer 1 ist der Aufruf, um einen Rechenprozess zu beenden (exit); das Erzeugen eines neuen Rechenprozesses erfolgt über den Systemcall fork, welcher die Nummer 2 hat. Über den Systemcall mit der Nummer 3 (read) lassen sich Daten aus Dateien oder von Geräten lesen. Kernel 2.6 bietet mehr als 270 unterschiedliche Systemcalls.

Prozessmanagement

Eine zweite Komponente des Betriebssystemkerns stellt das Prozess-Subsystem dar. Im Wesentlichen verhilft es Einprozessorsystemen (Uniprocessor System, UP) dazu, mehrere Applikationen quasi parallel auf dem einen Mikroprozessor (CPU) abarbeiten zu können. Bei einem Mehrprozessorsystem werden die Applikationen auf die unterschiedlichen Prozessoren verteilt (Symmetric Multiprocessing, SMP). Aus Sicht des Betriebssystems werden Applikationen als Rechenprozesse – genauer Tasks oder Threads – bezeichnet.

Jeder Rechenprozess besteht aus Code und Daten. Dafür wird im Rechner jeweils ein eigener Speicherblock reserviert. Ein weiterer Speicherblock kommt hinzu, um die während der Abarbeitung des Prozesses abzulegenden Daten zu speichern, der so genannte Stack. Damit belegt jeder Prozess mindestens drei Speicherblöcke: ein Codesegment, ein Datensegment und ein Stacksegment.

Um Ressourcen (Speicher) zu sparen, können sich mehrere Rechenprozesse auch Segmente teilen. Wird beispielsweise dieselbe Office-Applikation zweimal gestartet, wird vom Betriebssystemkern nicht zweimal ein identisches Codesegment angelegt, sondern für beide Rechenprozesse nur eins.

Teilen sich zwei Rechenprozesse sowohl das Codesegment als auch das Datensegment, spricht man von Threads. Rechenprozesse, die jeweils ein eigenes Datensegment besitzen, werden Tasks genannt.

Abbildung 2-2. Verarbeitung mehrerer Applikationen auf einer CPU

Da auf einer CPU nicht wirklich mehrere Rechenprozesse gleichzeitig ablaufen können, sorgt das Prozessmanagement dafür, dass jeweils nur kurze Abschnitte der einzelnen Rechenprozesse hintereinander bearbeitet werden. Am Ende einer derartigen Bearbeitungsphase unterbricht das Betriebssystem – ausgelöst durch einen Interrupt – den gerade aktiven Rechenprozess und sorgt dafür, dass ein Folgeabschnitt des nächsten Rechenprozesses bearbeitet wird. Hierdurch entsteht der Eindruck der Parallelität. Welcher der rechenbereiten Tasks bzw. Threads wirklich rechnen darf, wird durch einen Scheduling-Algorithmus bestimmt, der auch kurz als Scheduler bezeichnet wird. Den Vorgang der Auswahl selbst nennt man Scheduling.

Abbildung 2-3. Rechenprozesszustände

Konkurrieren zum Beispiel drei Rechenprozesse auf einem Einprozessorsystem um die Ressource CPU, dann rechnet zu einem Zeitpunkt maximal einer der drei. Dieser eine Rechenprozess ist im Zustand aktiv. Die anderen beiden Rechenprozesse werden dagegen unterbrochen (preempted) und befinden sich im Zustand lauffähig (running). Neben diesen beiden Zuständen gibt es noch den Zustand wartend, der auch schlafend genannt wird. Wie der Name bereits andeutet, wartet eine Applikation auf ein Ereignis, zum Beispiel darauf, dass Daten von der Peripherie geliefert werden oder darauf, dass eine bestimmte Zeit verstreicht. Ein vierter Zustand schließlich wird als ruhend oder terminiert (terminated) bezeichnet. Er steht für die Situation, bevor ein Rechenprozess gestartet – und damit lauffähig – wird oder nachdem der Rechenprozess beendet worden ist.

Abbildung 2-4. Rechenprozesszustände in Linux

Im Linux-Kernel selbst ist der Zustand wartend weiter unterteilt in unterbrechbares und in nicht unterbrechbares Warten (TASK_INTERRUPTIBLE und TASK_UNINTERRUPTIBLE). Der Ablauf eines Rechenprozesses kann nicht nur durch den Kernel, sondern eventuell auch durch andere Rechenprozesse über so genannte Signale beeinflusst werden. Diese werden durch Applikationen verschickt. Im Zustand unterbrechbares Warten wird eine schlafende Task durch ein Signal wieder in den Zustand lauffähig (TASK_RUNNING) versetzt; im Zustand nicht unterbrechbares Warten dagegen nicht.

Eine weitere Änderung gegenüber dem vereinfachten Prozesszustandsmodell bringt der Linux-Kernel durch den Zustand Zombie (TASK_ZOMBIE) mit sich. Beendet sich eine Task, wechselt sie nicht direkt in den Zustand ruhend/terminiert, sondern zunächst in den Zustand Zombie. In diesem Zustand hat das Betriebssystem noch den Exitcode der Task gespeichert. Erst wenn der Prozess, der den gerade beendeten Rechenprozess ursprünglich gestartet hat, diesen Exitcode abholt, ist die Task wirklich terminiert.

Abbildung Rechenprozesszustände in Linux verdeutlicht nochmals die Abläufe. Wird mit Hilfe des Systemcalls fork ein neuer Rechenprozess erzeugt, befindet sich dieser im Zustand TASK_RUNNING. Wird dieser neue Prozess vom Scheduler ausgewählt, dann wird der Prozess aktiv. Dieser Zustand wird im Prozess-Kontrollblock als solcher nicht abgespeichert. In einem System können genauso viele Prozesse aktiv sein wie Verarbeitungseinheiten (CPUs) vorhanden sind. Welcher Rechenprozess aktiv ist, ist in Linux in dem Variablenfeld »current « abgelegt. Für jeden Prozessor (CPU) ist in diesem Feld ein Element angelegt. Muss ein Prozess warten, wird er per Kernelfunktion »sleep « in den Wartezustand (TASK_INTERRUPTIBLE oder TASK_UNINTERRUPTIBLE) versetzt. Per wakeup bzw. im Fall des Zustandes TASK_INTERRUPTIBLE auch per Signal verändert sich der Zustand wieder in TASK_RUNNING. Ein Rechenprozess beendet sich schließlich über den Systemcall exit. Der Parameter dieses Systemcalls ist der Exitcode, der im so genannten Prozess-Kontrollblock (process control block, PCB) eingetragen wird. Solange dieser Exitcode nicht von einem Elternprozess abgeholt wurde, bleibt der PCB im System bestehen. Der Prozesszustand jedoch wird auf TASK_ZOMBIE gesetzt. Erst wenn (per Systemcall wait ) der Elternprozess den Exitcode abgeholt hat, wird auch der PCB freigegeben.

Das in Abbildung Rechenprozesszustände in Linux vorgestellte Modell ist nicht ganz vollständig. So fehlt beispielsweise der Zustand TASK_STOPPED, der im Kontext von Debugging und Systemcall-Tracing notwendig wird, für die Treiberentwicklung aber keine Relevanz hat.

Abbildung 2-5. Prozess-Kontrollblock

Werden die Tasks bzw. Threads durch den Betriebssystemkern unterbrochen, muss dieser eine Reihe von Informationen speichern. Dazu wird ebenfalls der PCB verwendet. Im Prozess-Kontrollblock werden unter anderem der Prozesszustand, die Prozessidentifikation (PID) und der Inhalt sämtlicher Register zum Zeitpunkt der Unterbrechung – der so genannte Maschinenzustand – abgelegt. Auch Scheduling-Parameter, beispielsweise Priorität des Rechenprozesses oder verbrauchte Rechenzeit, sind hier gespeichert.

Im Linux-Kernel ist der PCB durch die Taskstruktur struct task_struct repräsentiert (siehe Header-Datei <linux/sched.h>). So stellt beispielsweise das Feld pid die Prozessidentifikation dar, in der Unterstruktur thread wird der Maschinenzustand abgelegt und das Feld rt_priority enthält die Prozesspriorität.

Speicher-Management

Die dritte Komponente moderner Betriebssysteme ist die Speicherverwaltung. Hard- und Software machen es möglich, dass in Programmen Adressen (so genannte logische Adressen) verwendet werden, die nicht den physikalischen Adressen entsprechen. Der Entwickler kann Speicherbereiche (Segmente) definieren, die er dann – durch die Hardware unterstützt – bezüglich lesender und schreibender Zugriffe überwachen kann. Darüber hinaus kann sichergestellt werden, dass aus einem Datensegment kein Code gelesen wird bzw. in ein Codesegment keine Daten abgelegt werden.

Systemtechnisch wird dies dazu genutzt, sowohl dem Betriebssystemkern als auch jeder einzelnen Applikation eigene Segmente zuzuordnen. Damit wird verhindert, dass eine Applikation auf den Speicher der anderen Applikation oder gar auf den Speicher des Betriebssystemkerns zugreift. Der Speicherbereich, der vom Kernel genutzt werden kann, wird mit Kernel Space bezeichnet. Die Speicherbereiche der Applikationen heißen User-Space.

Allerdings kann aber auch der Kernel, und hier insbesondere der Gerätetreiber, nicht direkt auf den Speicher einer Applikation zugreifen. Zwar ist der physikalische Speicher – zumindest unter Linux für die x86 Prozessoren – direkt auf die logischen Adressen umgesetzt (lineares Address-Mapping), doch kennt der Kernel bzw. Treiber damit immer noch nicht die physikalischen Adressen einer bestimmten Task. Schließlich sind die identischen logischen Adressen zweier Tasks auf unterschiedliche physikalische Adressen abgelegt. Erschwerend kommt hinzu, dass das Speicherverwaltungs-Subsystem auch für das so genannte Paging und Swapping zuständig ist, also die Einbeziehung von Hintergrundspeicher (Festplatte) als Teil des Hauptspeichers. Durch das Swappen kann es geschehen, dass sich der Inhalt eines Segmentes überhaupt nicht im Hauptspeicher, sondern auf der Festplatte befindet. Bevor auf solche Daten zugegriffen werden kann, müssen sie erst wieder in den Hauptspeicher geladen werden.

Die Umrechnung logischer Adressen auf physikalische Adressen wird durch Funktionen innerhalb des Kernels durchgeführt. Das funktioniert aber immer nur für die eine Task, die sich im Zustand aktiv befindet (auf die also die globale Variable current zeigt).

IO-Management

Ein vierter großer Block des Betriebssystemkerns ist das IO-Management. Dieses ist für den Datenaustausch der Programme mit der Peripherie, den Geräten, zuständig.

Das IO-Management hat im Wesentlichen zwei Aufgaben:

  1. Ein Interface zur systemkonformen Integration von Hardware anzubieten und

  2. eine einheitliche Programmierschnittstelle für den Zugriff auf Peripherie zur Verfügung zu stellen.

Idee dieses Programmier-Interfaces ist es, den Applikationen jegliche Peripherie in Form von Dateien zu präsentieren, die dann Gerätedateien genannt werden. Die Gerätedateien sehen für den normalen Anwender wie herkömmliche sonstige Dateien aus. Innerhalb des Dateisystems sind sie aber durch ein Attribut als Gerätedatei gekennzeichnet. In einem Unix-System sind die meisten Gerätedateien im Verzeichnis /dev/ abgelegt. Dass dies nicht zwingend der Fall ist, liegt daran, dass die Dateien an jedem beliebigen anderen Ort im Verzeichnisbaum erzeugt werden können.

Beispiel 2-1. Datei und Gerätedatei

-rw-r-----    1 root     adm         35532 Oct  1 11:50 /var/log/messages
crw-rw----    1 root     lp         6,   0 Feb 23  1999 /dev/lp0

In Beispiel Datei und Gerätedatei ist die Ausgabe des Kommandos ls -l für eine normale Datei (ordinary file) und für eine Gerätedatei (device file) angegeben. Gleich anhand des ersten Zeichens, dem »c«, erkennt man bei der Datei /dev/lp0, dass es sich um eine Gerätedatei, genauer um ein so genanntes Character-Device-File, handelt.

Innerhalb des IO-Managements bzw. IO-Subsystems sind die Zugriffsfunktionen auf Dateien und Geräte realisiert. Dabei ist das Interface vor allem in der jüngeren Vergangenheit ausgebaut und abhängig von den unterschiedlichen Geräten differenziert worden. So existieren inzwischen neben den klassischen Dateizugriffsfunktionen beispielsweise eigene Zugriffsfunktionen für Multimediageräte.

Ähnliches gilt auch für die internen Schnittstellen zur systemkonformen Ankopplung der Peripherie. Klassisch wurden interne Schnittstellen für zeichenorientierte Geräte, so genannte »Character-Devices«, und blockorientierte Geräte, »Block-Devices«, zur Verfügung gestellt. Ein Gerät ist zeichenorientiert, wenn die Daten zeichenweise verarbeitet werden. Zeichen kommen mehr oder minder einzeln in einem Strom (Stream) an bzw. gehen in einem Strom weg. Bei einem Character-Device ist damit im Regelfall eine Positionierung innerhalb des Datenstroms nicht möglich. Deshalb lassen sich auch die letzten Zeichen nicht vor den ersten lesen bzw. schreiben.

Bei einem blockorientierten Gerät liegt der Fall anders. Hier werden die Daten in Blöcken verarbeitet. Dabei kann durchaus zunächst das Ende eines Datenstroms, dann die Mitte und zuletzt der Anfang gelesen werden. Festplatten, Bänder oder Disketten sind typischerweise Block-Devices. Diese werden zur Ablage von Dateien verwendet, wobei auf die Dateien über ein Dateisystem zugegriffen wird.

Die für den Zugriff auf zeichen- oder blockorientierte Geräte definierten Schnittstellen reichen für eine moderne Multimedia-Peripherie nicht mehr aus. Innerhalb des IO-Managements sind daher spezifische Subsysteme für die Integration von Netzwerkkarten, Grafikkarten, Soundkarten usw. implementiert. Diese Subsysteme existieren jedoch nicht nur für die unterschiedlichen Gerätetypen, sondern auch für die unterschiedlichen Arten, Geräte anzukoppeln. So gibt es ein SCSI-Subsystem, ein PCI-Subsystem, ein USB-Subsystem oder ein PCMCIA-Subsystem innerhalb des Linux-Kernels.

Gerätetreiber

Die fünfte Komponente eines Betriebssystems – Mittelpunkt dieses Buches – sind die Gerätetreiber. Als Softwarekomponente erfüllen sie eine überaus wichtige Funktion: Sie steuern den Zugriff auf alle Geräte! Erst der Treiber macht es einer Applikation möglich, über ein bekanntes Interface die Funktionalität eines Gerätes zu nutzen.

Ganz verschiedene Arten von Hardware werden über Gerätetreiber in ein Betriebssystem integriert: Drucker, Kameras, Tastaturen, Bildschirme, Netzwerkkarten, Scanner – um nur einige Beispiele anzuführen.

Da diese Geräte darüber hinaus über diverse Bussysteme (z.B. PCI, SCSI, USB) angeschlossen werden können, haben Betriebssysteme im Allgemeinen und Linux im Besonderen unterschiedliche Treiber-Subsysteme.

Während traditionell zwischen zeichenorientierten Geräten (Character-Devices) und Blockgeräten (Block-Devices) unterschieden wird, findet man bei Linux die folgenden Subsysteme (unvollständige Liste):

Für diese Vielfalt von Subsystemen ist die Applikationsschnittstelle erweitert worden. Nunmehr lassen sich folgende Interfaces differenzieren:

Realisiert sind die Interfaces zumeist auf Basis eines Sets standardisierter Datenstrukturen und IO-Controls (um das Systemcall-Interface nicht erweitern zu müssen).

Der Treiber muss die IO-Controls auswerten. Viele Treiber bestehen dabei aus mehreren Schichten (Low-level, Core und High-level) mit jeweils spezifischen Aufgaben. Man nennt sie deshalb auch geschichtete Treiber (»stacked driver«).

Abbildung 2-6. Treiberstruktur eines geschichteten Treibers

Der Low-level-Treiber ist für die Ansteuerung der internen Hardwareschnittstelle, also beispielsweise eines ganz spezifischen USB-Controllers, zuständig. Da die Anzahl bzw. Auswahl der Komponenten für den direkten Hardwarezugriff gering ist, kommt man hier mit einer geringen Anzahl von Treibern aus. Der Low-level-Treiber greift direkt auf die Register der Hardware zu. Der High-level-Treiber dagegen ist für einen Gerätetyp, z.B. eine Webcam, zuständig. Der notwendige Datentransfer zwischen dem Gerät (der Webcam) und dem Treiber wird durch den Low-level-Treiber durchgeführt; der High-level-Treiber greift also nicht direkt auf die Register der Hardware zu. Bei USB werden beispielsweise zwischen Gerät und Treiber Kommandopakete verschickt. Damit ist der High-level-Treiber für die Zusammenstellung der richtigen Pakete und die Auswertung der Antworten verantwortlich. Der eigentliche Pakettransport wird aber durch den Low-level-Treiber initiiert.

Zwischen Low-level-Treiber und High-level-Treiber liegt die Core-Treiberschicht. Diese erweitert die interne Treiberschnittstelle um gerätetypspezifische Funktionen. So stellt im Fall von USB der Core-Treiber Funktionen zum Geräte/Treiber-Management zur Verfügung. In dieser Zwischenschicht ist beispielsweise abgelegt, welche USB-Geräte am USB angeschlossen sind. Der Core-Treiber versucht zudem, den zu einem USB-Gerät passenden Treiber zu finden und zu laden, bzw. wenn ein Treiber geladen wird, ein zugehöriges Gerät ausfindig zu machen und dem Treiber zuzuweisen.

SCSI-, PCI- und auch Parallelport-Treiber stellen Untergruppen der Character- und Blockgeräte-Treiber dar. IrDA, USB und Netzwerktreiber bilden eine eigene Gruppe von Treibern.

Gerätetreiber sind integraler Bestandteil des Betriebssystemkerns. Soll ein Kernel mit einem neuen Treiber versehen werden, muss theoretisch der gesamte Kernel neu generiert werden. Treiber, die auf diese Art mit dem Betriebssystemkern verbunden sind, nennt man Built-in-Treiber oder auch Kerneltreiber.

Daneben bietet Linux auch die Möglichkeit, zu einem bereits aktiven Kernel einen Treiber hinzuzuladen. In einem solchen Fall ist der Treiber als ladbares Modul realisiert. Diese so genannten Modultreiber haben mehrere Vorteile. Der Treiberprogrammierer muss nicht jedes Mal einen neuen Kernel generieren, wenn er eine Version seines Treibers testen möchte. Auch entfallen damit das Runterfahren und der Neustart des Systems. Ist der Treiber fertiggestellt, kann er als Modul weitergegeben werden, und ein Nutzer kann den Treiber einfach – ebenfalls ohne Neugenerierung des Kernels – installieren und verwenden.

Gerätetreiber nutzen spezifische Funktionen des Betriebssystemkerns. Gerade bei Linux kommt es aber vor, dass Funktionen bzw. Schnittstellen unterschiedlicher Kernelversionen voneinander abweichen. Daher kann es im Fall eines Modultreibers dazu kommen, dass ein Treiber für einen älteren Kernel kompiliert wurde, der Nutzer aber einen jüngeren Betriebssystemkern verwendet, dessen interne Schnittstellen gegenüber der früheren Version modifiziert sind. Bei Verwendung des Modultreibers mit einer jüngeren Kernelversion führt dies möglicherweise zu Instabilitäten. Der Anwender benötigt daher einen Modultreiber, der genau zu seiner Kernelversion passt!

Wird der Treiber als Open Source herausgegeben, ist das Problem vergleichsweise einfach lösbar, indem (automatisiert) der Treiber auf dem jeweiligen Zielsystem kompiliert wird.

Nicht jeder Treiber kann in Form eines Moduls realisiert werden. So muss der Linux-Kernel beispielsweise gleich nach dem Start auf die Festplatte zugreifen, um von dort Systemprogramme und Konfigurationen zu lesen. Damit er das kann, benötigt er natürlich Treiber für die Festplatte.

Dennoch geht die Tendenz in die Richtung, dass Treiber grundsätzlich als Module erstellt werden. Die nächste Linux-Generation wird voraussichtlich nur noch einen Ramdisk-Treiber fest in den Kernel integriert haben. In einer Ramdisk (im Hauptspeicher »simulierte« Festplatte) schließlich werden die übrigen Treiber als Module abgelegt. Zum Systemstart werden der Linux-Kernel und die Ramdisk in den Hauptspeicher geladen, im Anschluss wird das System hochgefahren.

Die Funktionen, die ein Gerätetreiber-Entwickler zu kodieren hat, sind:

  1. Funktionen, die zur Einbindung des Gerätetreibers in den Kernel notwendig sind,

  2. Funktionen, die durch die Applikation angestoßen (getriggert) werden und

  3. Funktionen, die durch den Betriebssystemkern getriggert werden.

Abbildung 2-7. Treiberfunktionsübersicht

Der Betriebssystemkern besteht aus einer Vielzahl unterschiedlicher Softwarekomponenten, darunter auch den Gerätetreibern. Damit ein Treiber Teil des Betriebssystemkerns werden kann, müssen bei der Erstellung einige Konventionen eingehalten werden. So müssen – unabhängig davon, ob der Treiber als integraler Bestandteil des Kernels (Built-in-Treiber) oder als Modul geplant ist – eine Initialisierungs- und eine Deinitialisierungsfunktion geschrieben werden. Innerhalb der durch den Kernel aufgerufenen Initialisierungsfunktion klinkt sich der Treiber in den Kernel ein, führt eventuell eine Hardware-Erkennung durch und reserviert Ressourcen. Die Deinitialisierungsfunktion wird aufgerufen, wenn der Treiber wieder deaktiviert wird, beispielsweise beim Entladen des Treibermoduls oder beim Herunterfahren des Kernels. Innerhalb dieser Funktion gibt der Treiber allozierte Ressourcen frei und meldet sich beim Kernel wieder ab.

Wird in der Applikation ein Systemcall aufgerufen, der eine Interaktion mit dem Treiber bedingt, ruft der Betriebssystemkern eine der Applikationsfunktion entsprechende Treiberfunktion auf. So triggert beispielsweise innerhalb einer Applikation das Lesen (read) eine Lesefunktion im Treiber. Das Betriebssystem befindet sich aus Sicht des aktivierten Treibers im Prozess-Kontext. Wichtig daran: Nur im Prozess-Kontext ist der Treiber in der Lage, Daten in die Applikation zu kopieren bzw. von der Applikation abzuholen. Die Funktionen zur Integration des Treibers in den Kernel und die Treiberfunktionen, die durch die Applikation getriggert werden, werden in Kapitel Einfache Treiber vorgestellt.

Bei den in Fortgeschrittene Treiberentwicklung vorzustellenden Treiberfunktionen liegt der Fall anders. Werden Funktionen des Treibers unabhängig von irgendwelchen Applikationen aktiviert – einleuchtend ist der Fall, dass eine Hardware selbst, z.B. per Interrupt, eine Funktion triggert – befindet sich das Betriebssystem im Interrupt-Kontext. Im Interrupt-Kontext kann der Treiber nicht auf die Speicherbereiche einer den Treiber nutzenden Applikation zugreifen.

2.1.2. Sonstige Betriebssystemkomponenten

Ein Betriebssystem besteht nicht nur aus dem Betriebssystemkern, sondern auch aus einer Reihe von Betriebssystem-Applikationen und Bibliotheken (Libraries). Die Bibliotheken sind bereits erwähnt worden, beinhalten diese beispielsweise doch Funktionen, die die Systemcalls des Kernels aufrufen.

Auch bei der Treiberentwicklung sind gegebenenfalls Bibliotheken mit einzuplanen, um dem Anwendungsprogrammierer vereinfachten Zugang zu komplexen Funktionen zu verschaffen.

Betriebssystemapplikationen werden oft auch Dienste genannt. Diese Dienste gilt es jedoch gegenüber den Diensten des Betriebssystemkerns, die über das Systemcall-Interface durch Applikationen genutzt werden können, abzugrenzen. Die Dienste des Betriebssystems auf Anwender-Ebene sind meist ständig aktiv, ohne eine spezifische Ausgabe zu machen. In der Unix-Welt bezeichnet man sie auch als Daemonen, in der Windows-Welt als Services.

Ein solcher Service ist beispielsweise der syslog-Daemon (syslogd), der für die Protokollierung wichtiger Systemzustände eingesetzt wird.


Lizenz