Modul 5: Eingabe, Ausgabe und Geräte – Die periphere Anhalter-Kommunikation

1. Einleitung: Das Universum außerhalb der CPU

Liebe Anhalter, wir haben uns bisher tief im warmen, gemütlichen Kern des Betriebssystems aufgehalten. Wir haben gesehen, wie Prozesse im Arbeitsspeicher jongliert werden und wie der Scheduler die CPU zuteilt. Doch ein Computer, der nicht mit der Außenwelt kommuniziert, ist so nützlich wie ein Handtuch, das man zu Hause vergessen hat.

Ein Betriebssystem muss Tastaturanschläge verarbeiten, Netzwerkpakete empfangen, Bildschirme ansteuern und Daten auf Festplatten schreiben. Die Peripherie ist chaotisch, asynchron und unberechenbar. Jeder Hardware-Hersteller kocht sein eigenes Süppchen. Die Aufgabe des Betriebssystems ist es, dieses Chaos zu bändigen und dem Programmierer eine saubere, einheitliche Schnittstelle zu präsentieren.

2. Die Abstraktion: Everything is a File

Wie versteckt man die gewaltige Komplexität von zehntausenden verschiedenen Hardware-Geräten vor dem Anwender und dem Programmierer? Die Unix-Philosophie hat dafür einen genialen Ansatz gefunden, der auch heute in modernen Linux-Systemen unverändert gilt: „Everything is a file“.

E/A-Geräte werden in der Regel direkt in das Dateisystem eingebunden und aus Benutzersicht wie ganz normale Dateien behandelt[cite: 1]. Wenn ihr auf einem Server das Verzeichnis /dev/ (Device Nodes) öffnet, seht ihr keine Dateien im klassischen Sinne, sondern Einsprungspunkte in den Kernel. Ein Schreibzugriff auf /dev/sda schreibt nicht in ein Dokument, sondern sendet magnetische Impulse an einen Festplatten-Controller.

Natürlich gibt es abweichende Eigenschaften zu regulären Dateien[cite: 1]:

Wir teilen diese Geräte architektonisch in zwei fundamentale Kategorien ein[cite: 1]:

2.1 Block Devices (Blockorientierte Geräte)

Ein Block Device verarbeitet Daten nur in Form von Datenblöcken fester Länge[cite: 1].

2.2 Character Devices (Zeichenorientierte Geräte)

Ein Character Device verarbeitet keine festen Blöcke, sondern einen kontinuierlichen Datenstrom aus einzelnen Bytes (Zeichen)[cite: 1].

*(Anmerkung für Systemadministratoren: Wenn ihr auf der Shell ls -l /dev eingebt, achtet auf den allerersten Buchstaben der Ausgabe. Ein b steht für Block Device, ein c für Character Device. Zudem seht ihr dort statt einer Dateigröße zwei Zahlen: Die Major-Nummer identifiziert den zuständigen Treiber im Kernel, die Minor-Nummer das spezifische Gerät dieses Typs).*

Einige wenige Geräte lassen sich in dieses strikte Raster nicht exakt einordnen, etwa Systemuhren oder spezielle Sensoren, die eher über dedizierte Systemaufrufe (ioctls) angesprochen werden[cite: 1].

3. Die Schichtenarchitektur der I/O-Software

Damit euer C-Code ein einfaches printf(„Don't Panic!“); auf den Bildschirm zaubern kann, durchläuft der Befehl mehrere streng getrennte Software-Schichten. Zur Ansteuerung der Geräte ist extrem systemnahe und zum Teil sehr hardwareabhängige Software des Betriebssystems notwendig[cite: 1]. Diese Software lässt sich in zwei logische Ebenen strukturieren[cite: 1]:

A. Geräteunabhängige Software (Device Independent OS Software)

Dies ist die obere Schicht. In dieser Schicht wird die einheitliche Schnittstelle zum Dateisystem realisiert[cite: 1]. Hier ist nichts über die physische Beschaffenheit der Hardware bekannt. Ihre Aufgaben sind universell[cite: 1]:

B. Gerätetreiber (Device Drivers)

Die Gerätetreiber sind die Betriebssystem-Programmteile für den geräteabhängigen Code[cite: 1]. Hier sind alle Prozeduren vereinigt, die hochgradig hardwarespezifisch sind[cite: 1]. Ein Treiber ist der einzige Code im gesamten System, der exakt weiß, in welches Hardware-Register er wie viele Bits schreiben muss, um den Motor einer Festplatte anlaufen zu lassen oder ein Paket über eine Intel-Netzwerkkarte zu feuern. Treiber laufen (bei monolithischen Kerneln wie Linux) im hochprivilegierten Ring-0 und können bei Fehlern das gesamte System lahmlegen.

4. Gerätesteuerung: Wie sprechen wir mit dem Blech?

Die Peripheriegerätesteuerung erfolgt auf der Hardware-Ebene über den ständigen Austausch von Statussignalen und Daten zwischen den angeschlossenen Geräten und dem Prozessor[cite: 1]. Die Programme, die diese Steuerung übernehmen, nennen wir Treiber (Driver oder Handler)[cite: 1].

Die Komplexität dieser Steuerungsaufgaben ist weitgehend vom Geräteverhalten abhängig[cite: 1]. Es gibt drei fundamentale Methoden des Signal- und Datenaustauschs, die heute zum Einsatz kommen[cite: 1]:

4.1 Programmgesteuerte E/A (Polling)

Die simpelste, aber ineffizienteste Form der I/O. Im einfachsten Fall sind alle Grundfunktionen der Ein- und Ausgabe vollständig direkt in die Treiberprogramme integriert[cite: 1].

4.2 Unterbrechungsgesteuerte E/A (Interrupts)

Um den Prozessor nicht durch unnötige Wartezeiten und ständige Statusabfragen zeitlich zu belasten, erfolgt die Gerätesteuerung in der Praxis i. d. R. unter Verwendung von Unterbrechungen (Interrupts)[cite: 1].

Das Prinzip ist elegant: Die CPU gibt dem Peripheriegerät den Befehl „Lies diese Daten“ und widmet sich danach sofort wieder anderen Prozessen. Sobald das Peripheriegerät die Daten im internen Controller-Puffer bereitliegen hat, legt es eine Spannung an eine Interrupt-Leitung (IRQ) auf dem Mainboard an. Die CPU wird mitten in ihrer aktuellen Berechnung unterbrochen, rettet ihren Zustand auf den Stack und führt die sogenannte Interrupt Service Routine (ISR), also das Interrupt-Service-Programm des Treibers, aus[cite: 1].

Ein Praxisbeispiel aus dem Skript (Tastatureingabe): Ein simples Terminal sendet Daten. Ein Tastendruck am Terminal erzeugt sofort eine Unterbrechung (Interrupt)[cite: 1]. Das entsprechende Interrupt-Service-Programm des OS liest das Zeichen extrem schnell vom Empfangsport der seriellen Schnittstelle aus und speichert es in einem internen Zwischenpuffer des Kernels ab[cite: 1]. Falls das Betriebssystem (etwa eine offene Shell) auf die Eingabe eines Kommandos wartet, wird direkt im Puffer geprüft, ob das Zeichen für das Eingabeende – das Carriage Return (CR) – eingetroffen ist[cite: 1]. Wird das CR-Zeichen erkannt, veranlasst das Interrupt-Service-Programm sofort eine Meldung (zum Beispiel via Mailbox) an ein hierarchisch viel höher liegendes Auswerteprogramm[cite: 1]. Dieses analysiert dann den Inhalt des eingegebenen Textes, um wieder eine Ebene höher die gewünschte Betriebssystem-Funktion (z.B. den Befehl ls oder cd) zu starten[cite: 1].

Da manche Geräte (wie Netzwerk- oder Festplattencontroller) wichtigere Daten senden als andere (wie eine Maus), sollten die Unterbrechungssignale (Interrupts) der verschiedenen Geräte mit unterschiedlichen Prioritäten versehen sein, um dringliche Aufgaben vorrangig abarbeiten zu können[cite: 1].

4.3 Direct Memory Access (DMA-Betrieb)

Interrupts sind fantastisch, um der CPU das ständige Nachfragen zu ersparen. Aber wenn ein Festplatten-Controller 10 Megabyte an Daten in den RAM kopieren soll und für jedes einzelne Byte einen Interrupt auslöst, stirbt die CPU am Interrupt-Overhead.

Hier betritt das Prinzip des direkten Speicherzugriffs (DMA) die Bühne, um den Prozessor von solch massiven, sich ständig wiederholenden Formen der reinen Datenübertragung zu entlasten[cite: 1].

* Wie es funktioniert: Das Mainboard besitzt einen speziellen Chip, den DMA-Controller. Die CPU programmiert diesen Controller nur noch kurz: „Lies 10 Megabyte von Festplatte X und schreibe sie ab RAM-Adresse Y.“ * Danach kümmert sich die CPU wieder um ihre Prozesse. Die gesamte Datenübertragung zwischen dem Hauptspeicher (RAM) und dem Peripheriegerät wird ab diesem Moment vollständig von der DMA-Steuerung durchgeführt[cite: 1]. * Der DMA-Controller arbeitet autark auf dem Datenbus. Die Transferrate wird nicht mehr von der CPU gebremst, sondern wird nur noch durch die Geschwindigkeitsanforderungen des beteiligten Peripheriegeräts, die Länge der Speicherzyklen oder die physikalische Übertragungskapazität des Busses bestimmt[cite: 1]. * Erst wenn die vollen 10 Megabyte erfolgreich im RAM liegen, feuert der DMA-Controller einen einzigen Interrupt an die CPU: „Ich bin fertig, die Daten sind da.“

DMA

5. Deep Dive für Administratoren: Top Halves und Bottom Halves

*(Anmerkung des Kursleiters: Um als zukünftige Systemadministratoren Kernel-Fehler oder hohe Systemauslastungen zu verstehen, müssen wir beim Thema Interrupts noch eine Ebene tiefer gehen, als es die absolute Theorie vorsieht).*

Wenn ein Hardware-Interrupt auftritt, muss die CPU alles stehen und liegen lassen. Während eine Interrupt Service Routine (ISR) läuft, sind andere (gleich- oder niedriger priorisierte) Interrupts blockiert. Läuft die ISR zu lange, friert das System ein, Netzwerkkollisionen entstehen oder Datenpakete werden verworfen.

Moderne Betriebssysteme wie Linux spalten das Interrupt-Handling daher in zwei Phasen:

Wenn ihr auf einem Linux-Server den Befehl top ausführt und seht, dass ein CPU-Kern plötzlich zu 90% in `si` (Software Interrupts) oder `hi` (Hardware Interrupts) hängt, dann wisst ihr nun genau, dass euer Server gerade durch extremen I/O-Traffic (z.B. eine DDoS-Attacke auf die Netzwerkkarte oder sterbende Festplatten-Sektoren) auf der untersten Treiberebene gequält wird.


Quellen & Materialien
Dieses Modul basiert auf dem Lehrskript „Betriebssysteme“ der German-Baltic Management School.[cite: 1]