In diesem Blogbeitrag geht es hauptsächlich darum, wie I/O auf der untersten Ebene funktioniert. Dieser Artikel richtet sich an Leser, die gerne verstehen möchten, wie Java-I/O-Vorgänge auf Maschinenebene abgebildet werden und was die Hardware tut, wenn die Anwendung ausgeführt wird. Es wird davon ausgegangen, dass Sie mit grundlegenden E/A-Vorgängen vertraut sind, z. B. dem Lesen und Schreiben von Dateien über die Java-E/A-API. Diese Inhalte würden den Rahmen dieses Artikels sprengen.
Cache-Verarbeitung und Kernel vs. User Space
Die Verarbeitungsmethode des Pufferns und Pufferns ist die Grundlage aller E/A-Vorgänge . Die Begriffe „Eingabe, Ausgabe“ sind nur für das Verschieben von Daten in und aus dem Cache sinnvoll. Denken Sie immer daran. Typischerweise beinhaltet ein Prozess, der eine E/A-Anfrage an das Betriebssystem ausführt, das Entleeren von Daten aus einem Puffer (ein Schreibvorgang) und das Füllen des Puffers mit Daten (ein Lesevorgang). Das ist das gesamte Konzept von I/O. Die Mechanismen, die diese Übertragungsvorgänge innerhalb des Betriebssystems durchführen, können sehr komplex sein, sind aber konzeptionell sehr einfach. Wir werden es in einem kleinen Teil des Artikels besprechen.
Das obige Bild zeigt ein vereinfachtes „logisches“ Diagramm, das darstellt, wie Blockdaten von einer externen Quelle, z. B. einer Festplatte, in den Speicherbereich eines Prozesses (z. B RAM) Mitte. Zunächst muss der Puffer des Prozesses durch den Systemaufruf read() gefüllt werden. Dieser Systemaufruf veranlasst den Kernel, einen Befehl an die Festplattensteuerungshardware auszugeben, um Daten von der Festplatte abzurufen. Der Festplattencontroller schreibt Daten über DMA direkt in den Speicherpuffer des Kernels, ohne weitere Unterstützung durch die Haupt-CPU. Wenn eine read()-Operation angefordert wird und der Festplattencontroller den Cache vollständig gefüllt hat, kopiert der Kernel die Daten aus dem temporären Cache im Kernel-Speicherplatz in den prozessspezifischen Cache.
Zu beachten ist, dass die vom Prozess im Kernelraum angeforderten Daten möglicherweise bereits bereit sind, wenn der Kernel versucht, Daten zwischenzuspeichern und vorab abzurufen. Wenn ja, werden die vom Prozess angeforderten Daten kopiert. Wenn die Daten nicht verfügbar sind, wird der Prozess ausgesetzt. Der Kernel liest die Daten in den Speicher.
Virtueller Speicher
Vielleicht haben Sie schon oft von virtuellem Speicher gehört. Lassen Sie mich es noch einmal vorstellen.
Alle modernen Betriebssysteme verwenden virtuellen Speicher. Unter virtuellem Speicher versteht man künstliche oder virtuelle Adressen anstelle von physischen (Hardware-RAM) Speicheradressen. Virtuelle Adressen haben zwei wichtige Vorteile:
Mehrere virtuelle Adressen können derselben physischen Adresse zugeordnet werden.
Ein virtueller Adressraum kann größer sein als der tatsächlich verfügbare Hardwarespeicher.
In der obigen Beschreibung scheint das Kopieren vom Kernel-Speicherplatz in den Endbenutzer-Cache zusätzliche Arbeit zu verursachen. Warum weisen Sie den Festplattencontroller nicht an, Daten direkt an den User-Space-Cache zu senden? Nun, dies wird durch den virtuellen Speicher implementiert. Nutzen Sie die oben genannten Vorteile 1.
Durch die Zuordnung einer Kernel-Space-Adresse zu derselben physischen Adresse wie einer virtuellen User-Space-Adresse kann die DMA-Hardware (die nur physische Speicheradressen adressieren kann) den Cache füllen. Dieser Cache ist sowohl für Kernel- als auch für User-Space-Prozesse sichtbar.
Dadurch entfällt die Kopie zwischen Kernel und Benutzerbereich, es ist jedoch erforderlich, dass der Kernel und die Benutzerpuffer dieselbe Seitenausrichtung verwenden. Der Puffer muss ein Vielfaches der Blockgröße des Festplattencontrollers verwenden (normalerweise 512-Byte-Festplattensektoren). Das Betriebssystem unterteilt seinen Speicheradressraum in Seiten, bei denen es sich um Bytegruppen fester Größe handelt. Diese Speicherseiten sind immer ein Vielfaches der Plattenblockgröße und normalerweise 2x (vereinfachte Adressierung). Typische Speicherseitengrößen sind 1024, 2048 und 4096 Byte. Die Seitengrößen des virtuellen und physischen Speichers sind immer gleich.
Speicher-Paging
Um den zweiten Vorteil des virtuellen Speichers (einen adressierbaren Raum, der größer als der physische Speicher ist) zu unterstützen, ist das Paging des virtuellen Speichers (oft als Seitenaustausch bezeichnet) erforderlich. Dieser Mechanismus basiert auf der Tatsache, dass Seiten im virtuellen Speicherbereich im externen Festplattenspeicher beibehalten werden können, wodurch Platz für die Platzierung anderer virtueller Seiten im physischen Speicher bereitgestellt wird. Im Wesentlichen fungiert der physische Speicher als Cache für Paging-Bereiche. Der Paging-Bereich ist der Speicherplatz auf der Festplatte, in dem der Inhalt von Speicherseiten gespeichert wird, wenn diese zwangsweise aus dem physischen Speicher ausgelagert werden.
Passen Sie die Speicherseitengröße auf ein Vielfaches der Festplattenblockgröße an, damit der Kernel Anweisungen direkt an die Festplattencontroller-Hardware senden kann, um die Speicherseite auf die Festplatte zu schreiben oder sie bei Bedarf neu zu laden. Es stellt sich heraus, dass alle Festplatten-E/A-Vorgänge auf Seitenebene ausgeführt werden. Dies ist die einzige Möglichkeit, Daten auf modernen ausgelagerten Betriebssystemen zwischen Festplatte und physischem Speicher zu verschieben.
Moderne CPUs enthalten ein Subsystem namens Memory Management Unit (MMU). Dieses Gerät befindet sich logisch zwischen der CPU und dem physischen Speicher. Es enthält Zuordnungsinformationen von virtuellen Adressen zu physischen Speicheradressen. Wenn die CPU auf einen Speicherort verweist, bestimmt die MMU, welche Seiten gespeichert werden müssen (normalerweise durch Verschieben oder Maskieren bestimmter Bits der Adresse) und wandelt die virtuelle Seitennummer in eine physische Seitennummer um (in Hardware implementiert, was extrem schnell ist).
Dateiorientiert, Block-E/A
Datei-E/A erfolgt immer bei einem Dateisystem-Kontextwechsel. Dateisysteme und Festplatten sind völlig unterschiedliche Dinge. Festplatten speichern Daten in Segmenten, jedes Segment ist 512 Bytes groß. Es handelt sich um ein Hardwaregerät und weiß nichts über die Semantik der gespeicherten Dateien. Sie stellen lediglich eine bestimmte Anzahl von Slots bereit, in denen Daten gespeichert werden können. In dieser Hinsicht ähnelt ein Festplattensegment dem Speicher-Paging. Sie haben alle eine einheitliche Größe und bilden ein großes adressierbares Array.
Andererseits ist das Dateisystem eine Abstraktion höherer Ebene. Ein Dateisystem ist eine spezielle Methode zum Ordnen und Übersetzen von Daten, die auf einer Festplatte (oder einem anderen blockorientierten Gerät mit wahlfreiem Zugriff) gespeichert sind. Der von Ihnen geschriebene Code interagiert fast immer mit dem Dateisystem und nicht direkt mit der Festplatte. Das Dateisystem definiert Abstraktionen wie Dateinamen, Pfade, Dateien und Dateiattribute.
Ein Dateisystem organisiert (auf einer Festplatte) eine Reihe von Datenblöcken einheitlicher Größe. Einige Blöcke enthalten Metainformationen, wie z. B. Zuordnungen freier Blöcke, Verzeichnisse, Indizes usw. Andere Blöcke enthalten tatsächliche Dateidaten. Metainformationen für eine einzelne Datei beschreiben, welche Blöcke die Daten der Datei enthalten, wo die Daten enden, wann sie zuletzt aktualisiert wurden usw. Wenn ein Benutzerprozess eine Anfrage zum Lesen von Dateidaten sendet, lokalisiert das Dateisystem genau den Speicherort der Daten auf der Festplatte. Anschließend werden Maßnahmen ergriffen, um diese Festplattensektoren im Speicher zu platzieren.
Auch in Dateisystemen gibt es das Konzept von Seiten, die die gleiche Größe wie eine Basisspeicherseite oder ein Vielfaches davon haben können. Typische Dateisystemseitengrößen reichen von 2048 bis 8192 Byte und sind immer ein Vielfaches der Basisspeicherseitengröße.
Die Durchführung von E/A-Vorgängen in einem ausgelagerten Dateisystem läuft auf die folgenden logischen Schritte hinaus:
Bestimmen Sie, welche Dateisystemseiten (Sammlungen von Festplattensegmenten) die Anforderung umfasst. Dateiinhalte und Metadaten auf der Festplatte können über mehrere Dateisystemseiten verteilt sein, und diese Seiten sind möglicherweise nicht zusammenhängend.
Weisen Sie genügend Kernel-Speicherseiten zu, um identische Dateisystemseiten aufzunehmen.
Erstellen Sie eine Zuordnung dieser Speicherseiten zu Dateisystemseiten auf der Festplatte.
Erzeugt einen Seitenfehler für jede Speicherseite.
Das virtuelle Speichersystem gerät in Paging-Fehler und plant Paginierungen, um diese Seiten zu überprüfen, indem es den Inhalt von der Festplatte liest.
Sobald die Pageins abgeschlossen sind, zerlegt das Dateisystem die Rohdaten, um den angeforderten Dateiinhalt oder die Attributinformationen zu extrahieren.
Es ist zu beachten, dass diese Dateisystemdaten wie andere Speicherseiten zwischengespeichert werden. Bei nachfolgenden E/A-Anfragen verbleiben einige oder alle Dateidaten im physischen Speicher und können direkt wiederverwendet werden, ohne dass sie erneut von der Festplatte gelesen werden müssen.
Dateisperre
Dateisperre ist ein Mechanismus, mit dem ein Prozess den Zugriff anderer Prozesse auf eine Datei verhindern oder den Zugriff anderer Prozesse auf die Datei einschränken kann. Obwohl es „Dateisperre“ genannt wird, bedeutet es das Sperren der gesamten Datei (was oft durchgeführt wird). Die Sperrung kann normalerweise auf einer detaillierteren Ebene erfolgen. Wenn die Granularität auf die Byte-Ebene sinkt, werden Bereiche der Datei normalerweise gesperrt. Eine Sperre ist einer bestimmten Datei zugeordnet, beginnend an einer angegebenen Byteposition in der Datei und bis zu einem angegebenen Bytebereich. Dies ist wichtig, da dadurch mehrere Prozesse beim Zugriff auf bestimmte Bereiche der Datei zusammenarbeiten können, ohne andere Prozesse an der Arbeit an anderer Stelle in der Datei zu hindern.
Es gibt zwei Formen von Dateisperren: gemeinsam genutzte und exklusive. Mehrere gemeinsame Sperren können gleichzeitig für denselben Dateibereich gültig sein. Eine exklusive Sperre hingegen erfordert, dass keine andere Sperre für die angeforderte Region gültig ist.
Stream-E/A
Nicht alle E/A sind blockorientiert. Es gibt auch Stream-E/A, den Prototyp einer Pipe, und auf die Bytes des E/A-Datenstroms muss sequentiell zugegriffen werden. Zu den gängigen Datenflüssen gehören TTY-Geräte (Konsolen), Druckanschlüsse und Netzwerkverbindungen.
Datenströme sind normalerweise, aber nicht unbedingt, langsamer als Blockgeräte und liefern intermittierende Eingaben. Die meisten Betriebssysteme ermöglichen das Arbeiten im nicht blockierenden Modus. Ermöglicht einem Prozess, zu prüfen, ob Eingaben in einen Datenstrom verfügbar sind, ohne zu blockieren, wenn dies nicht der Fall ist. Diese Verwaltung ermöglicht es Prozessen, Eingaben bei ihrem Eintreffen zu verarbeiten und andere Funktionen auszuführen, während der Eingabestream inaktiv ist.
Einen Schritt weiter als der nicht blockierende Modus ist die Bereitschaftsauswahl. Es ähnelt dem nicht blockierenden Modus (und baut häufig auf dem nicht blockierenden Modus auf), entlastet das Betriebssystem jedoch von der Last, zu prüfen, ob der Stream bereit ist. Das Betriebssystem kann angewiesen werden, eine Sammlung von Streams zu beobachten und Anweisungen an den Prozess zurückzugeben, welcher Stream bereit ist. Diese Funktion ermöglicht es einem Prozess, mehrere Aktivitätsströme unter Verwendung von gemeinsamem Code und einem einzelnen Thread wiederzuverwenden, indem er die vom Betriebssystem zurückgegebenen Vorbereitungsinformationen nutzt. Diese Methode wird häufig von Netzwerkservern verwendet, um eine große Anzahl von Netzwerkverbindungen zu verwalten. Die Vorbereitung auf die Auswahl ist für eine großvolumige Expansion von entscheidender Bedeutung.