Inhaltsverzeichnis
Betriebssystemarchitektur
Definition: Unter einem Betriebssystem versteht man alle Softwarekomponenten, die
- die Ausführung der Applikationen und
- die Verteilung der Betriebsmittel (z.B. Interrupts, Speicher, Prozessorzeit)
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.
Komponenten des Kernels
In der obigen Betriebssystem-Architektur ist vereinfacht ein Rechensystem dargestellt. Im unteren Teil befindet sich der Kernel, im oberen die sonstigen Betriebssystemkomponenten (Services und Bibliotheken) .
Der Betriebssystemkern besteht damit im Wesentlichen aus den folgenden Komponenten, die im Anschluss genauer vorgestellt werden:
- Systemcall-Interface
- Prozessmanagement
- Speicher-Management
- IO-Management
- Gerätetreiber
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. 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.
Da auf einer CPU nicht wirklich mehrere Rechenprozesse gleichzeitig ablaufen können, sorgt das Prozessmanagement dafür, dass jeweils nur kurze Abschnitte (ca: 100 ms) 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.
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.
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.
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 Pagingund Swappingzustä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:
- Ein Interface zur systemkonformen Integration von Hardware anzubieten und
- 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 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.
Gerätetreiber
Die fünfte Komponente eines Betriebssystems 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):
- Character-Devices
- Block-Devices
- Netzwerk
- SCSI (Small Computer System Interface)
- USB (Universal Serial Bus)
- IrDA (Infrared Data Association)
- Cardbus und PCMCIA
- Parallelport
- I2C (serielles Kommunikationsprotokoll)
- I2O (Intelligent Input/Output)
Für diese Vielfalt von Subsystemen ist die Applikationsschnittstelle erweitert worden. Nunmehr lassen sich folgende Interfaces differenzieren:
- Das Standard-API (mit Funktionen wie
open
,close
,read
,write
undioctl
) - Kommunikations-API
- Card-Services
- Multimedia-Interfaces (z.B. Video4Linux)
Realisiert sind die Interfaces zumeist auf Basis eines Sets standardisierter Datenstrukturen und IO-Controls (um das Systemcall-Interface nicht erweitern zu müssen).
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
Kernelmodul 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.
Die Funktionen, die ein Gerätetreiber-Entwickler zu kodieren hat, sind:
- Funktionen, die zur Einbindung des Gerätetreibers in den Kernel notwendig sind,
- Funktionen, die durch die Applikation angestoßen (getriggert) werden und
- Funktionen, die durch den Betriebssystemkern getriggert werden.
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.
Quelle: Prof. Jürgen Quade u. Eva-Katharina Kunst