Die Tiefen von Windows-I/O (Teil 4) — Cache-Manager: Wann erreicht Ihr WriteFile tatsächlich die Festplatte?

· · Windows, Win32, I/O, Cache, Kernel, Dateisystem, .NET, CSharp

WriteFile hat Erfolg gemeldet. Wo befinden sich die Daten jetzt eigentlich?

Die Antwort lautet, fast sicher: noch nicht auf der Festplatte. Sie wurden lediglich in einen Cache im Arbeitsspeicher kopiert. Deshalb passiert „gespeichert, aber nach dem Stromausfall war es weg“, deshalb erreichen Benchmarks von Dateikopien physikalisch unmögliche Geschwindigkeiten, und deshalb rufen Datenbanken pflichtbewusst fsync auf.

Teil 4 der Reihe „Die Tiefen von Windows-I/O“ widmet sich dem, was dazwischensteht: dem Cache-Manager. In Teil 2 hieß es, „liegt es bereits im Cache, wird auch asynchrones I/O synchron abgeschlossen“, und in Teil 1 blieb „die Abkürzung, die kein IRP baut (Fast I/O)“ als Hausaufgabe offen. Diese Folge löst beide Fäden auf.

1. Zunächst das Fazit

  • Windows’ Dateicache ist ein Write-Back-Cache. Lesevorgänge werden zunächst aus dem System-Dateicache bedient, Schreibvorgänge gehen zunächst ebenfalls dorthin. Die Übertragung auf die Festplatte erledigt das Betriebssystem im Nachhinein.1
  • Der Cache ist im Kern eine Dateizuordnung. Der Cache-Manager mappt 256-KB-Abschnitte einer Datei in Slots des System-Adressraums, und cache-fähige Lese- und Schreibvorgänge werden zu Speicherkopien zwischen Slot und Anwendungspuffer (Kapitel 2).1
  • Schreibvorgänge holt ein jede Sekunde laufender Lazy Writer nach. Ein Absturz der Anwendung verliert keine Daten, aber ein Stromausfall oder OS-Absturz verliert dirty Cache-Daten, die noch nicht zurückgeschrieben wurden (Kapitel 4).1
  • Es gibt drei Werkzeuge für „garantiert geschrieben“. FlushFileBuffers (entspricht .NETs Flush(true)), FILE_FLAG_WRITE_THROUGH und FILE_FLAG_NO_BUFFERING. Bei jedem Schreibvorgang zu flushen ist ineffizient; die offizielle Dokumentation empfiehlt stattdessen die Kombination aus NO_BUFFERING und WRITE_THROUGH (Kapitel 5).21
  • NO_BUFFERING bringt Ausrichtungsanforderungen mit sich. Größe und Versatz müssen ganzzahlige Vielfache der Sektorgröße sein, und auch die Pufferadresse muss an einer physischen Sektorgrenze ausgerichtet sein. Und selbst unter NO_BUFFERING bleiben Metadaten weiterhin gecacht (Abschnitt 5.3).31
  • Gemappte Ansicht und Cache teilen dieselben Daten. Speichergemappte Dateien und gewöhnliches cache-fähiges I/O bleiben kohärent, und das dauerhafte Speichern einer Zuordnung erfolgt in zwei Schritten: FlushViewOfFile, gefolgt von FlushFileBuffers (Kapitel 6).45
  • Synchrone Lese- und Schreibvorgänge, die den Cache treffen, erzeugen möglicherweise nicht einmal ein IRP. Die Abkürzung Fast I/O führt direkt zum Cache-Manager – die Antwort auf die Hausaufgabe aus Teil 1 (Kapitel 7).6

2. Was der Cache wirklich ist — Dateien werden in den Speicher gemappt

2.1. 256-KB-Slots und Speicherkopien

Wer sich Windows’ Dateicache als „Behälter für Festplattenblöcke“ vorstellt, kann sich vieles seines Verhaltens nicht mehr erklären. Das richtige Bild ist dieses: Der Cache-Manager mappt 256-KB-Abschnitte einer Datei in „Slots“ des System-Adressraums, und cache-fähige Lese- und Schreibvorgänge werden als Speicherkopien zwischen diesem Slot und dem Puffer der Anwendung ausgeführt.1

System-AdressraumAnwendung (User-Mode)ReadFile/WriteFile =Speicherkopie mit dem SlotEinlesen beim ersten Zugriff undnachgeholtes Zurückschreiben erfolgen seitenweiseSystem-DateicacheSlot, der einen 256-KB-Abschnitt der Datei mapptPuffer der Anwendung(der an ReadFile/WriteFile übergebene Bereich)Datei auf der Festplatte

Abbildung 1: Die Realität von cache-fähigem I/O. Was aus Anwendungssicht „Lesen oder Schreiben einer Datei“ ist, ist meistens nur eine Speicherkopie

Ein leicht misszuverstehender Punkt: 256 KB ist die Granularität der Ansicht (der Zuordnung), nicht die Aussage, dass Festplatten-I/O stets in 256-KB-Einheiten erfolgt. Seiten innerhalb eines Slots werden bei Bedarf eingelesen, und die tatsächliche Menge an Festplatten-I/O hängt von der Anfragegröße und dem Zugriffsmuster ab. Wird ein Abschnitt zum ersten Mal gelesen, löst das Festplatten-I/O zur Füllung aus (hier wandert ein IRP aus Teil 1 in den unteren Speicherstack). Liegt er bereits im Cache, wird der Lesevorgang allein durch Kopieren abgeschlossen. Was wir in Teil 2, Kapitel 5 gesehen haben – „bei einem Cache-Treffer wird auch bei asynchronem Ausgeben synchron abgeschlossen“ – ist genau dieses Verhalten „sofort beantworten, wenn es geht“, das sich hier zeigt. Umgekehrt gilt: Ist die Seite bei aktiviertem Cache nicht im Speicher vorhanden, kann ein asynchroner Lesevorgang mangels asynchronem Mechanismus für die Seitenfehlerbehandlung synchron verarbeitet werden – eine Falle, die wir ebenfalls in Teil 2 gesehen haben.7

2.2. Was hinter „der freie Speicher ist gesunken“ wirklich steckt

Ob Caching und Read-Ahead verwendet werden, wird pro Öffnung (pro Dateiobjekt) verwaltet1, aber die gecachten Daten selbst werden pro Datei (pro Stream) geteilt. Öffnet man dieselbe Datei mehrfach, entstehen keine getrennten Caches – jedes Handle sieht denselben gecachten Inhalt (das ist die Grundlage für die in Kapitel 6 behandelte Kohärenz). Der Cache bleibt, solange Windows läuft, unter der Kontrolle des Cache-Managers.1 Kopiert man eine große Datei oder liest und schreibt viel, wird freier physischer Speicher zunehmend in Cache umgewandelt. Auch wenn der freie Speicher im Task-Manager kleiner erscheint, handelt es sich bei einem Großteil davon um „Standby-Speicher, der wertvoll genutzt wird und sofort zurückgegeben wird, sobald eine Anwendung ihn anfordert“.

Das lässt sich am Bildschirm nachprüfen. Öffnen Sie Task-Manager > Leistung > Arbeitsspeicher, dann ist die untere Leiste „Speicherzusammensetzung“ unterteilt in In Verwendung / Geändert / Standby / Frei. Der Großteil des Dateicaches landet in Standby und wird in der Liste rechts als „Zwischengespeichert“ summiert. Für eine feinere Ansicht zeigt Ressourcenmonitor > Registerkarte Arbeitsspeicher dieselbe Aufteilung mit konkreten Werten. Kopieren Sie eine mehrere GB große Datei und schauen Sie danach erneut hin: Standby steigt, Frei sinkt, während „In Verwendung“ sich kaum ändert – der sichtbare Beweis dafür, dass nicht „der Speicher aufgebraucht wurde“, sondern „freier Speicher als Cache genutzt wurde“. Diese Unterscheidung haben wir auch praktisch bei der Diagnose von Speicherengpässen in „GC-Wartezeit und Speicherleck bei .NET unterscheiden“ behandelt.

3. Read-Ahead — Spekulation beim Lesen

Der Cache-Manager lädt anhand vergangener Zugriffsmuster den Abschnitt vor, der voraussichtlich als Nächstes gelesen wird (Read-Ahead). Bei einer der Reihe nach gelesenen Datei liegen die nächsten Daten oft schon im Cache, bevor die Anwendung sie überhaupt anfordert – das ist das Geheimnis hinter der Geschwindigkeit sequenzieller Lesevorgänge. Die Menge des Vorauslesens ist nicht fest, sondern variiert mit dem erkannten Muster und der Anfragegröße.

Historie der Leseanfragen der Anwendungliest der Reihe nach von vornCache-Manager erkenntein MusterRead-Ahead: liest den folgenden Abschnittein, bevor er angefordert wird(Menge variiert mit Muster und Anfragegröße)Hinweis FILE_FLAG_SEQUENTIAL_SCAN= Read-Ahead aktiv nutzenHinweis FILE_FLAG_RANDOM_ACCESS= Read-Ahead unterdrücken, da nutzlos

Abbildung 2: Read-Ahead. Zusätzlich zur Erkennung des Zugriffsmusters lassen sich über Flags von CreateFile Hinweise geben

Die in Teil 1 tabellierten FileOptions.SequentialScan / RandomAccess sind Hinweise an diese Read-Ahead-Engine. Ersteres eignet sich für Batch-Verarbeitung, die „alles der Reihe nach“ durchgeht, Letzteres für Zugriffe, die wie beim Verfolgen eines Index umherspringen – man versteht ihren Einsatzort am besten als Flags, mit denen man dem Betriebssystem eine Zukunft mitteilt, die nur die Anwendung selbst kennt.

4. Verzögertes Schreiben — Was der „Erfolg“ von WriteFile bedeutet

4.1. Der Lazy Writer kommt jede Sekunde vorbei

Die Schreibseite ist ein Write-Back-Cache. WriteFile gibt Erfolg zurück, sobald die Daten in einen Slot kopiert wurden, und die Übertragung auf die Festplatte wird zurückgestellt. Diese Strategie des „später schreiben“ ist das verzögerte Schreiben (Lazy Writing).1

Für die Übertragung sorgt der Lazy Writer, den der Cache-Manager jede Sekunde anstößt. Er reiht ein Achtel der zuletzt nicht geflushten Seiten zum Schreiben ein und legt bei Bedarf weitere nach. Mit dem Attribut FILE_ATTRIBUTE_TEMPORARY erstellte temporäre Dateien werden dabei vom Flush-Ziel des Lazy Writers ausgenommen – es wäre verschwendet, etwas zu schreiben, das ohnehin bald gelöscht wird.1 Das ist allerdings nur ein durch das Attribut gegebener Hinweis: Bei Speicherdruck können die Seiten dennoch zurückgeschrieben werden, und für eine Datei, deren Name lediglich „temporär wirkt“, greift das nicht.

FestplatteLazy Writer (jede Sekunde)SystemcacheAnwendungFestplatteLazy Writer (jede Sekunde)SystemcacheAnwendungKopiert in den Slot undmarkiert die Seite als dirty (nicht geschrieben)Von hier bis zum Zurückschreiben besteht das "gefährliche Fenster"bei Stromausfall/OS-Absturz gehen diese Daten verlorenErst hier erfolgt die dauerhafte SpeicherungWriteFile(Daten)TRUE wird sofort zurückgegebenWählt 1/8 der dirty Seiten ausSchreibt sie gebündelt zurück

Abbildung 3: Verzögertes Schreiben. Der Erfolg von WriteFile bedeutet „an das Betriebssystem übergeben“, nicht „dauerhaft gespeichert“

4.2. Was passiert, wie viel geht verloren

Präzisieren wir die Bedeutung des „gefährlichen Fensters“. Das Schicksal hängt von der Art des Ausfalls ab.

Daten direkt nach erfolgreichem WriteFile(dirty Seite im Cache)Was ist passiertProzess der Anwendungabgestürzt/erzwungen beendetGesamtes Betriebssystem gestoppt(Stromausfall, Bluescreen)Daten bleiben erhaltender Cache gehört dem Betriebssystem, daherschreibt der Lazy Writer wie geplant zurückDirty Seiten gehen verlorennur was bereits die Festplatte erreicht hatte, bleibt

Abbildung 4: Die Trennlinie zwischen Ausfallarten. Der Cache gehört „dem Betriebssystem“, nicht „dem Prozess“

  • Stirbt die Anwendung, gehen die Daten nicht verloren. Sobald die Kopie in den Cache erfolgt ist, gehört der Besitz der Daten dem Betriebssystem. Dass „die Anwendung direkt nach dem Speichern abstürzte, die Datei aber unversehrt war“, verdankt sich genau diesem Umstand.
  • Stirbt das gesamte Betriebssystem, geht der dirty Anteil verloren. Die Häufigkeit des Flushens wird als Kompromiss zwischen Leistung und Zuverlässigkeit abgestimmt, und die Dokumentation hält ausdrücklich fest, dass „bei plötzlichem Stromverlust gecachte Daten verloren gehen“.1

Die eigentliche Frage beim Entwurf einer Geschäftsanwendung lautet also: „Darf diese Daten im Moment eines Stromausfalls verloren gehen?“ Ein paar Sekunden Protokolldaten mag verzeihlich sein. Ein bestätigter Auftragsdatensatz vermutlich nicht. Setzen Sie die Werkzeuge des nächsten Kapitels nur für das ein, was Sie sich nicht zu verlieren leisten können.

5. Ein Werkzeugkasten für „garantiert geschrieben“

5.1. FlushFileBuffers — jetzt sofort komplett schreiben

FlushFileBuffers schreibt die gepufferten Daten der angegebenen Datei vollständig zum Gerät durch. Da Metadaten des Dateisystems stets gecacht bleiben, ist ein Flush (oder WRITE_THROUGH) auch nötig, um Metadaten zuverlässig zu übertragen – ein Punkt, den man im Blick behalten sollte.12 In .NET entspricht dem FileStream.Flush(true) (Flush() allein gibt nur .NETs internen Puffer an das Betriebssystem weiter, der Cache des Betriebssystems bleibt unangetastet).8

Die offizielle Dokumentation weist allerdings deutlich darauf hin: es bei jedem einzelnen Schreibvorgang aufzurufen ist ineffizient. Bei vielen Schreibvorgängen, die jeweils Dauerhaftigkeit benötigen, sollten stattdessen die weiter unten behandelten NO_BUFFERING und WRITE_THROUGH kombiniert werden.2

5.2. FILE_FLAG_WRITE_THROUGH — nur die Verzögerung entfernen

Öffnet man eine Datei mit FILE_FLAG_WRITE_THROUGH, wird bei jedem Schreibvorgang wie gewohnt in den Cache geschrieben, zugleich aber sofort und ohne auf den Lazy Writer zu warten auch auf die Festplatte.1 Entscheidend ist, dass Lesevorgänge weiterhin vom Cache profitieren – eine direkte Antwort auf „Lesevorgänge sollen schnell bleiben, nur die Verzögerung beim Schreiben soll wegfallen“.

5.3. FILE_FLAG_NO_BUFFERING — am Cache vorbei

FILE_FLAG_NO_BUFFERING nimmt bei Lese- und Schreibvorgängen den Systemcache selbst aus dem Bild. Jeder Lese- und Schreibvorgang wird bei jedem Aufruf zu Geräte-I/O ohne dazwischenliegenden Cache.1 Umgangen wird dabei aber nur Windows’ Systemcache – wie Abbildung 5 zeigt, ist der Schreibcache innerhalb des Speichergeräts eine eigene Stufe. Wird auch Resilienz gegen Stromausfälle benötigt, bleiben WRITE_THROUGH zusätzlich oder FlushFileBuffers erforderlich. Es ist ein Werkzeug für den Massentransfer großer Datenmengen oder für Datenbank-Engines, die ihre Pufferung selbst verwalten – kommt aber mit strengen Regeln.3

  • Größe und Dateiversatz jedes Lese- oder Schreibvorgangs müssen ein ganzzahliges Vielfaches der Sektorgröße des Volumes sein (bei 512-Byte-Sektoren: 512, 1024, 1536 usw.).
  • Auch die Pufferadresse muss an der physischen Sektorgröße ausgerichtet sein (auch „Advanced Format“-Festplatten mit 4096-Byte-Sektoren sind zu berücksichtigen).
  • Metadaten bleiben trotzdem weiterhin gecacht, sodass für vollständige Dauerhaftigkeit zusätzlich WRITE_THROUGH oder FlushFileBuffers nötig ist.12

Über diese „Regeln“ stolpert als Erstes, wer glaubt, das bloße Setzen des Flags reiche aus. Lesen oder Schreiben ohne Einhaltung der Ausrichtung schlägt mit ERROR_INVALID_PARAMETER (87) fehl. Hier die drei einzuhaltenden Punkte im Überblick.3

Auszurichten Bedingung Wie erfüllen
Größe von Lese-/Schreibvorgang Ganzzahliges Vielfaches der Sektorgröße des Volumes lpBytesPerSector über GetDiskFreeSpace abfragen und auf ein Vielfaches davon runden
Dateiversatz Wie oben (gilt auch für Offset in OVERLAPPED) In Vielfachen der Sektorgröße vorrücken
Pufferadresse An der physischen Sektorgröße ausgerichtet Mit VirtualAlloc zuweisen (liefert an Seitengrenzen ausgerichteten Speicher)

Der dritte Punkt wird besonders häufig übersehen. Adressen, die malloc, new oder ein C#-Array liefern, garantieren keine Ausrichtung an einer Sektorgrenze. Verwendet man VirtualAlloc, das an Seitengrenzen zuweist, erfüllt man dabei zugleich die Anforderungen von „Advanced Format“-Festplatten mit 4096-Byte-Sektoren. Die minimale Form sieht so aus.

// C++ / Win32. Fehlerbehandlung auf das Nötigste reduziert
DWORD sectorsPerCluster = 0, bytesPerSector = 0, freeClusters = 0, totalClusters = 0;
if (!GetDiskFreeSpaceW(L"C:\\", &sectorsPerCluster, &bytesPerSector,
                       &freeClusters, &totalClusters))
{
    return GetLastError();
}

// Lese-/Schreibeinheit auf ein ganzzahliges Vielfaches der Sektorgröße bringen (hier etwa 1 MiB)
const DWORD chunk = (1024 * 1024 / bytesPerSector) * bytesPerSector;

// Puffer an einer Seitengrenze ausgerichtet anlegen (malloc/new garantieren das nicht)
BYTE* buffer = static_cast<BYTE*>(
    VirtualAlloc(nullptr, chunk, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
if (buffer == nullptr) { return GetLastError(); }

HANDLE h = CreateFileW(L"C:\\temp\\big.bin", GENERIC_READ, FILE_SHARE_READ, nullptr,
                       OPEN_EXISTING, FILE_FLAG_NO_BUFFERING, nullptr);
if (h == INVALID_HANDLE_VALUE)
{
    // GetLastError liefert das Ergebnis des "zuletzt aufgerufenen Win32-Aufrufs".
    // Würde man zuerst VirtualFree aufrufen, würde der eigentliche Fehlergrund von
    // CreateFileW (Zugriff verweigert, Pfad nicht vorhanden usw.) vom Ergebnis
    // des Aufräumens überschrieben, und es bliebe nur ein aussageloser Fehlercode
    const DWORD err = GetLastError();
    VirtualFree(buffer, 0, MEM_RELEASE);
    return err;
}

// Da jeweils um chunk Bytes vorgerückt wird, bleiben Größe und Versatz ausgerichtet
DWORD read = 0;
while (ReadFile(h, buffer, chunk, &read, nullptr) && read > 0)
{
    // Die ersten read Bytes von buffer verarbeiten
    // (am Dateiende gilt read < chunk - das ist normal)
}

CloseHandle(h);
VirtualFree(buffer, 0, MEM_RELEASE);

Zu beachten ist außerdem, dass .NETs FileOptions keinen Wert kennt, der FILE_FLAG_NO_BUFFERING entspricht. Wird es unbedingt benötigt, muss CreateFile direkt aufgerufen werden, wobei die obigen Ausrichtungsanforderungen dann selbst einzuhalten sind. Gehen Sie in der Reihenfolge „NO_BUFFERING, weil ich die Pufferung selbst verwalte“ vor, nicht „NO_BUFFERING, weil es schneller sein soll“.

5.4. Zusammenfassung der Einsatzbereiche

Standard-WriteFile: Erfolg wird bereits hier zurückgegebenLazy Writer (jede Sekunde) / WRITE_THROUGH (sofort)Zeitpunkt des Geräts /FlushFileBuffers verlangt vollständiges DurchschreibenNO_BUFFERING überspringt den Cache und geht direkt hierhinPuffer der AnwendungSystem-Dateicache(dirty Seiten)Cache im SpeichergerätNichtflüchtiges Speichermedium

Abbildung 5: Die Schichten, die Daten durchlaufen, und wie weit jedes Werkzeug sie vorantreibt. Nicht die letzte Stufe vergessen, „der Cache im Speichergerät“

Methode Was passiert Wofür geeignet
Standard (Cache aktiv) Abschluss mit dem Kopieren in den Cache; die Übertragung übernimmt der Lazy Writer Die meisten Datei-I/O-Vorgänge
FlushFileBuffers / Flush(true) Schreibt die aktuellen Daten plus Metadaten vollständig durch Bestätigung an einem Meilenstein (z. B. Commit einer Transaktion)
FILE_FLAG_WRITE_THROUGH Bei jedem Schreibvorgang sofort auf die Festplatte (Lesevorgänge weiter über den Cache) Ein laufendes Protokoll oder Journal, das nicht verloren gehen darf
FILE_FLAG_NO_BUFFERING (+WRITE_THROUGH) Cache vollständig umgangen; Ausrichtung erforderlich Selbstverwaltete Pufferung, Massentransfers

Die Reihenfolge der Wahl umfasst zwei Schritte: erst festlegen, wie viel bei einem Stromausfall verloren gehen darf, dann prüfen, wie viel Verlangsamung dafür in Kauf genommen werden kann. Überfliegen Sie die Tabelle nicht einfach von oben nach unten, sondern folgen Sie dieser Verzweigung.

hinnehmbar(z. B. die letzten paar Sekunden eines Protokolls)nicht hinnehmbarMeilenstein(z. B. bestätigte Transaktion)jeder einzelne Datensatznein (gewöhnliche Anwendung)ja (DB-Engine usw.)Diese Daten sollen geschrieben werdenIst es hinnehmbar, sie im Momenteines Stromausfalls oder Bluescreens zu verlierenBeim Standard bleiben (Cache aktiv)am schnellsten. Die meisten I/O-Vorgänge gehören hierhinWas nicht verloren gehen darf:ein „Meilenstein“ oder „jeder einzelne Datensatz“?An Meilensteinen FlushFileBuffersin .NET Flush(true)Kosten: nur das Warten am MeilensteinWird die Pufferung selbst verwaltetund lassen sich die Ausrichtungsanforderungen aus 5.3 erfüllenFILE_FLAG_WRITE_THROUGHbei jedem Schreibvorgang sofort auf die FestplatteLesevorgänge bleiben über den Cache schnellFILE_FLAG_NO_BUFFERING+ FILE_FLAG_WRITE_THROUGHdie von den offiziellen Docs genannte Form für häufige Dauerhaftigkeit

Abbildung 6: Wie man ein Werkzeug wählt. Die erste Verzweigung ist die Zuverlässigkeitsanforderung, die zweite die tragbaren Kosten. Dass es keinen Pfad für „bei jedem einzelnen Datensatz FlushFileBuffers“ gibt, liegt daran, dass die offizielle Dokumentation das, wie in 5.1 gesehen, als ineffizient bezeichnet

Nur drei Praxismuster seien noch genannt.

  1. „In eine temporäre Datei schreiben → flushen → umbenennen“ ist das Standardrezept, um keine halb geschriebene Datei zu hinterlassen. Erst den Inhalt vollständig schreiben, dann per Namen bestätigen – diese atomare Übergabe wird ausführlich in „Grundlagen der wechselseitigen Sperrung bei Dateiintegration“ behandelt.
  2. Es einer Datenbank zu überlassen ist ebenfalls ein solides Design. Wie SQLite über WAL und Flushen Dauerhaftigkeit erreicht, ist in „SQLite in C#-Geschäftsanwendungen einsetzen“ nachzulesen. „Keine eigene Flush-Strategie schreiben“ ist immer eine mögliche Wahl.
  3. Bei Benchmarks den Cache im Verdacht haben. Eine Messung „das Lesen ist verdächtig schnell“ fängt meist Cache-Treffer ab dem zweiten Durchlauf ein. Das richtige Vorgehen bei Messungen ist in „Wie man die Geschwindigkeit verschiedener Programmversionen unter Windows korrekt vergleicht“ zusammengefasst.

Und vergessen Sie nicht die letzte Stufe aus Abbildung 5 – den Cache im Speichergerät selbst. FlushFileBuffers verlangt ein Durchschreiben bis dorthin, doch bei USB-Sticks und externen Festplatten spielt zusätzlich die geräteseitige Schreibcache-Richtlinie („Schnelles Entfernen“ versus „Bessere Leistung“) eine Rolle. Zum Umgang mit Wechseldatenträgern siehe auch „Wie man USB-Geräte aus einer Windows-Anwendung heraus behandelt“.

6. Kohärenz mit speichergemappten Dateien

Wer in Teil 1 gehört hat, „der Cache ist im Kern eine Dateizuordnung“, hat sich vermutlich gefragt: Kommt sich dann eine selbst mit MapViewOfFile erzeugte Ansicht nicht mit dem Cache in die Quere, den ReadFile/WriteFile verwenden?

Tut sie nicht. Weil beide auf demselben Mechanismus aufsetzen. Ein Dateizuordnungsobjekt ist von einer Datei gestützt, und das Verdrängen einer Seite erfolgt als Zurückschreiben in diese Datei. Auch wenn mehrere Prozesse Ansichten derselben lokalen Datei erzeugen, ist der sichtbare Inhalt kohärent.4

System-AdressraumAdressraum von Prozess AAnsicht des Cache-Managers(der von ReadFile/WriteFile genutzte Slot)Ansicht von MapViewOfFileDieselben physischen Seiten(von der Datei gestützter Speicher)Datei auf der FestplatteI/O über FILE_FLAG_NO_BUFFERINGliegt außerhalb dieser Kohärenz (direkt zur Festplatte)

Abbildung 7: Sowohl die gemappte Ansicht als auch der Cache blicken auf dieselben “von der Datei gestützten Seiten”. Außerhalb steht nur NO_BUFFERING

Zwei Dinge sind zu beachten.

  • I/O über FILE_FLAG_NO_BUFFERING liegt außerhalb dieser Kohärenz. Lese- und Schreibvorgänge, die den Cache umgehen, werden nicht mit dem über eine gemappte Ansicht oder den Cache sichtbaren Inhalt abgeglichen. Wer beides mischt, muss die Konsistenz selbst herstellen.
  • Das dauerhafte Speichern einer gemappten Ansicht erfolgt in zwei Schritten. FlushViewOfFile stößt das Zurückschreiben der dirty Seiten im angegebenen Bereich an, schreibt dabei aber keine Metadaten und wartet nicht auf den physischen Schreibvorgang aus dem Cache des Speichergeräts. Um es zuverlässig zu übertragen, ist nach FlushViewOfFile zusätzlich FlushFileBuffers aufzurufen.5

Die praktische Seite von Dateizuordnung als gemeinsamem Speicher (benannte Freigabe, Synchronisierung, typische Fehlermuster) wird in „Fallstricke und Best Practices für gemeinsam genutzten Speicher“ behandelt.

7. Fast I/O — die Hausaufgabe aus Teil 1 wird eingelöst

Vorweg in zwei Zeilen. Fast I/O ist eine Abkürzung für synchrone Lese- und Schreibvorgänge auf eine bereits gecachte Datei, die ohne den Aufbau eines IRP (I/O Request Packet – der Behälter, in dem der Kernel eine Anfrage an einen Treiber übergibt) direkt Daten mit dem Cache austauscht. Dass in der Spalte „Operation“ von Procmon IRP_MJ_READ und FASTIO_READ gemischt auftauchen, liegt daran, dass dasselbe „Lesen“ entweder den normalen Pfad oder diese Abkürzung nehmen kann.

In Teil 1, Abschnitt 5.2, stand: „Nicht jedes I/O wird zum IRP.“ Hier die Auflösung.

Bei einer im Cache liegenden Datei ist bekannt, dass Lese- und Schreibvorgänge allein durch eine Speicherkopie mit dem Cache erledigt werden können, ohne den Aufwand, ein IRP aufzubauen und durch den Gerätestack zu schicken. Deshalb stellt Windows für synchrones I/O auf eine gecachte Datei die Abkürzung Fast I/O bereit: Es wird kein IRP erzeugt, stattdessen werden die „Fast-I/O-Einstiegspunkte“ des Dateisystems direkt aufgerufen und direkt vom Cache-Manager kopiert.6 Kann Fast I/O die Anfrage nicht bearbeiten (nicht im Cache, eine Sperre ist beteiligt, ein Filter greift ein usw.), fällt es auf den normalen IRP-Pfad zurück. Zu beachten ist, dass dies ein schneller Pfad speziell für synchrone Anfragen ist – ein Cache-Treffer bedeutet nicht immer Fast I/O. Vorgänge auf einem asynchronen (FILE_FLAG_OVERLAPPED) Handle können weiterhin über den IRP-Pfad laufen, selbst wenn sie sofort aus dem Cache abgeschlossen werden (Teil 2, Kapitel 5).

kannkann nichtSynchrone Lese-/Schreibvorgänge auf ein cache-fähiges HandleKann Fast I/O die Anfrage bearbeiten(z. B. bereits im Cache)Fast I/Okopiert direkt mit dem Cache, baut kein IRPerscheint in Procmon als FASTIO_Normaler Pfadbaut ein IRP auf und sendet es durch den Gerätestack(die Welt aus Abbildung 6 in Teil 1)

Abbildung 8: Die Verzweigung bei Fast I/O. Deshalb tauchen in Procmon FASTIO_READ und IRP_MJ_READ gemischt auf

Damit lässt sich erklären, warum bei der Procmon-Beobachtung in Teil 1, Kapitel 7 Zeilen mit FASTIO_ gemischt auftauchten. Bei einem Cache-Treffer beim Lesen ist selbst ein IRP schon ein Luxus. Die Existenz dieses Pfades wirkt sich auch auf die in Teil 6 behandelten Filtertreiber aus (Minifilter können sich auch in Fast I/O einklinken).

8. Zusammenfassung

  • Windows’ Dateicache ist Write-Back und im Kern eine Zuordnung von 256-KB-Abschnitten der Datei. Cache-fähige Lese- und Schreibvorgänge werden zu Speicherkopien mit einem Slot.1
  • Beim Lesen spekuliert Read-Ahead, und SequentialScan/RandomAccess sind dafür Hinweise.1
  • Beim Schreiben holt der jede Sekunde laufende Lazy Writer nach. Stirbt die Anwendung, bleiben Daten erhalten; stirbt das Betriebssystem selbst, geht nur der dirty Anteil verloren. Die Design-Frage lautet: „Darf diese Daten im Moment eines Stromausfalls verloren gehen?“1
  • Die Werkzeuge für zuverlässiges Schreiben sind FlushFileBuffers (Bestätigung an Meilensteinen) / WRITE_THROUGH (bei jedem Schreibvorgang) / NO_BUFFERING (ohne Cache, mit Ausrichtungsanforderungen). Jedes Mal zu flushen ist ineffizient; für häufige Dauerhaftigkeit empfiehlt die offizielle Dokumentation die Kombination aus NO_BUFFERING und WRITE_THROUGH. Zu beachten bleibt, dass Metadaten stets gecacht werden.231
  • Gemappte Ansicht und Cache teilen dieselben Seiten und bleiben kohärent. Außerhalb steht nur NO_BUFFERING. Das dauerhafte Speichern einer Zuordnung erfolgt in zwei Schritten: FlushViewOfFile, gefolgt von FlushFileBuffers.45
  • Synchrone Lese- und Schreibvorgänge, die den Cache treffen, sparen sich über Fast I/O sogar das IRP – das ist die wahre Identität der FASTIO_-Einträge aus dem Procmon-Trace in Teil 1.6

Weiter geht es mit Teil 5, „Die NTFS-Interna — Das Dateisystem anhand der MFT verstehen“. Bis hierhin haben wir eine Datei als „Versatz und Bytefolge“ behandelt; von hier aus gehen wir tiefer, hinein in die Frage, wie NTFS Daten tatsächlich auf der Festplatte anordnet – die MFT, mehrere Datenströme, das Journal, Hardlinks – die statische Struktur auf der Festplatte selbst.

Verwandte Artikel

Verwandte Beratungsleistungen

Die KomuraSoft LLC übernimmt Design und Ursachenuntersuchung von Datei-I/O in Windows-Geschäftsanwendungen – Fälle wie „vermeintlich gespeicherte Daten sind verschwunden“ oder „Dateischreibvorgänge sind langsam oder verdächtig schnell“.

  1. Microsoft Learn, File Caching. Dazu, dass Windows Dateidaten standardmäßig cacht, Lesevorgänge aus dem System-Dateicache bedient werden und auch Schreibvorgänge in den Cache gehen, wodurch es ein Write-Back-Cache ist; dazu, dass Caching pro Dateiobjekt verwaltet wird und unter der Kontrolle des Cache-Managers abläuft; dazu, dass die Strategie, das Schreiben auf die Festplatte zu verzögern und die Daten im Cache zu halten, verzögertes Schreiben (Lazy Writing) genannt wird; dazu, dass beim Lesen einer Datei ein 256-KB-Abschnitt in einen 256-KB-Slot des System-Adressraums eingelesen wird und der Benutzerprozess Daten zwischen sich und diesem Slot kopiert; dazu, dass der Cache-Manager jede Sekunde den Lazy Writer startet, der ein Achtel der zuletzt nicht geflushten Seiten zum Schreiben auf die Festplatte einreiht und bei Bedarf weitere nachlegt; dazu, dass temporäre Dateien nicht geflusht werden; dazu, dass ungeschriebene gecachte Daten bei einem plötzlichen Systemausfall wie Stromverlust verloren gehen; dazu, dass Dateimetadaten auch bei deaktiviertem Caching über FILE_FLAG_NO_BUFFERING weiterhin gecacht sein können; dazu, dass Daten unter FILE_FLAG_WRITE_THROUGH in den Cache geschrieben werden, während sie zugleich ohne die Verzögerung des Lazy Writers sofort auf die Festplatte geschrieben werden; sowie dazu, dass Dateisystem-Metadaten stets gecacht werden, weshalb ihre dauerhafte Speicherung einen Flush oder FILE_FLAG_WRITE_THROUGH erfordert.  2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

  2. Microsoft Learn, FlushFileBuffers function. Dazu, dass WriteFile normalerweise in einen internen Puffer schreibt, den das Betriebssystem periodisch auf die Festplatte schreibt; dazu, dass FlushFileBuffers alle gepufferten Informationen der angegebenen Datei zum Gerät schreibt; dazu, dass es ineffizient ist, es bei jedem von vielen Schreibvorgängen aufzurufen, und dass Anwendungen, die für wichtige Daten bei häufigen Schreibvorgängen Dauerhaftigkeit benötigen, stattdessen ungepuffertes I/O über FILE_FLAG_NO_BUFFERING und FILE_FLAG_WRITE_THROUGH einsetzen sollten; sowie dazu, dass ein Aufruf auf einem Volume-Handle (mit Administratorrechten) alle offenen Dateien auf diesem Volume flusht.  2 3 4 5

  3. Microsoft Learn, File Buffering. Zu den Zugriffsanforderungen für eine mit FILE_FLAG_NO_BUFFERING geöffnete Datei: Lese-/Schreibgrößen und Dateiversätze (einschließlich der über OVERLAPPED angegebenen) müssen ein ganzzahliges Vielfaches der Sektorgröße des Volumes sein; Lese-/Schreibpufferadressen sollten an der physischen Sektorgröße ausgerichtet sein; und Rücksicht ist bei Advanced-Format-Geräten mit 4.096-Byte-physischen-Sektoren nötig.  2 3 4

  4. Microsoft Learn, File Mapping. Dazu, dass ein Dateizuordnungsobjekt von einer Datei auf der Festplatte gestützt ist, wobei das Auslagern einer Seite als Schreiben des geänderten Inhalts in diese Datei erfolgt; sowie dazu, dass Daten kohärent (identisch mit dem Inhalt der Datei auf der Festplatte) sind, wenn mehrere Prozesse Ansichten derselben lokalen Datei aus demselben Dateizuordnungsobjekt erzeugen.  2 3

  5. Microsoft Learn, FlushViewOfFile function. Dazu, dass FlushViewOfFile das Schreiben der dirty Seiten im Bereich der gemappten Ansicht auf die Festplatte anstößt; dazu, dass die Funktion keine Dateimetadaten flusht und nicht auf den physischen Schreibvorgang aus einem Hardware-Festplattencache wartet; sowie dazu, dass FlushFileBuffers nach FlushViewOfFile aufgerufen werden muss, um alle dirty Seiten und Metadaten vollständig und physisch zu schreiben.  2 3

  6. Microsoft Learn, IRPs Are Different From Fast I/O. Dazu, dass Fast I/O ein schneller Pfad für synchrones I/O auf eine gecachte Datei ist, der Dateisystem- und Cache-Manager-Einstiegspunkte direkt aufruft, ohne ein IRP zu erzeugen; dazu, dass Daten direkt vom Cache in den Benutzerpuffer (oder umgekehrt) übertragen werden; sowie dazu, dass der normale IRP-basierte Pfad verwendet wird, wenn Fast I/O die Anfrage nicht bearbeiten kann.  2 3

  7. Microsoft Learn, Asynchronous disk I/O appears as synchronous on Windows. Dazu, dass eine Anfrage sofort abgeschlossen wird und TRUE zurückgibt, wenn die Daten bereits im Cache sind; sowie dazu, dass Windows’ Cache über eine Dateizuordnung implementiert ist und mangels asynchronem Mechanismus für Seitenfehler ein asynchroner Lesevorgang auf einem cache-fähigen Handle synchron verarbeitet werden kann, wenn die Seite nicht vorhanden ist. 

  8. Microsoft Learn, FileStream.Flush method (.NET). Dazu, dass Flush() den internen Puffer des Streams an das Betriebssystem schreibt, und Flush(true) zusätzlich auch alle zwischengeschalteten Dateipuffer (die eigenen Puffer des Betriebssystems) flusht. 

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Ist die Daten bereits auf der Festplatte, sobald WriteFile Erfolg zurückgibt?
Standardmäßig nicht. Windows' Dateicache ist ein Write-Back-Cache, und WriteFile gibt Erfolg zurück, sobald die Daten in den System-Dateicache kopiert wurden. Das eigentliche Schreiben auf die Festplatte übernimmt anschließend der Lazy Writer des Cache-Managers, der einmal pro Sekunde läuft. Entscheidend ist der Unterschied zwischen den Arten von Ausfällen. Stürzt der Prozess der Anwendung ab, gehen Daten, die es in den Cache geschafft haben, nicht verloren, weil das Betriebssystem sie später zurückschreibt, solange das Betriebssystem selbst läuft. Fällt dagegen das gesamte Betriebssystem aus (Stromausfall, Bluescreen), gehen dirty Cache-Daten verloren, die noch nicht zurückgeschrieben wurden. Die korrekte Lesart von „WriteFile hat Erfolg gemeldet“ lautet nicht „es wurde dauerhaft gespeichert“, sondern „es wurde an das Betriebssystem übergeben“.
Wie stelle ich sicher, dass Daten tatsächlich auf der Festplatte ankommen?
Es gibt drei Werkzeuge. Erstens FlushFileBuffers, das alle gepufferten Daten und Metadaten dieser Datei bis zum Gerät durchschreibt (in .NET entspricht dem FileStream.Flush(true)). Zweitens FILE_FLAG_WRITE_THROUGH, das bei jedem Schreibvorgang in den Cache schreibt und zugleich sofort auf die Festplatte. Drittens FILE_FLAG_NO_BUFFERING, das den Cache vollständig umgeht. Microsofts eigene Dokumentation weist darauf hin, dass es ineffizient ist, FlushFileBuffers bei jedem Schreibvorgang aufzurufen, und dass Anwendungen, die bei häufigen Schreibvorgängen zuverlässige Dauerhaftigkeit benötigen, stattdessen FILE_FLAG_NO_BUFFERING mit FILE_FLAG_WRITE_THROUGH kombinieren sollten. Jedes dieser Werkzeuge gibt einen Teil des Nutzens aus dem Cache zugunsten der Sicherheit auf, sodass es in der Praxis darauf ankommt, sie nicht überall anzuhängen, sondern gezielt für Schreibvorgänge einzusetzen, die Sie sich nicht zu verlieren leisten können.
Worin unterscheiden sich FILE_FLAG_WRITE_THROUGH und FILE_FLAG_NO_BUFFERING?
WRITE_THROUGH bedeutet „weiterhin in den Cache schreiben, aber vor dem Abschluss auch auf die Festplatte schreiben“. Lesevorgänge profitieren weiterhin vom Cache; entfernt wird nur die Verzögerung des Lazy Writers beim Schreiben. NO_BUFFERING bedeutet, dass Lese- und Schreibvorgänge den Systemcache vollständig umgehen, sodass beide bei jedem Aufruf zu Geräte-I/O werden (umgangen wird dabei allerdings nur Windows' eigener Cache – ein im Speichergerät selbst sitzender Schreibcache wird dadurch nicht übersprungen). Im Gegenzug gelten strenge Einschränkungen: Größe und Dateiversatz jedes Lese- oder Schreibvorgangs müssen ein ganzzahliges Vielfaches der Sektorgröße des Volumes sein, und auch die Pufferadresse muss an einer physischen Sektorgrenze ausgerichtet sein. Zudem werden auch bei NO_BUFFERING weiterhin die Metadaten des Dateisystems gecacht, sodass für deren zuverlässige Speicherung zusätzlich FlushFileBuffers oder WRITE_THROUGH nötig ist. Typischerweise wird es von Software eingesetzt, die ihre Pufferung selbst verwaltet, etwa Datenbank-Engines; gewöhnliche Anwendungen sollten zunächst WRITE_THROUGH oder FlushFileBuffers in Betracht ziehen.
Zeigt der Task-Manager wenig freien Speicher, liegt das am Dateicache?
Häufig ja, und das ist normales Verhalten. Windows nutzt freien physischen Speicher aktiv als Dateicache, sodass das Kopieren einer großen Datei oder viel Lesen und Schreiben den Cache anschwellen lässt und die Speichernutzung höher erscheinen lässt. Die meisten der vom Cache belegten Seiten lassen sich jedoch vergleichsweise schnell freigeben, sobald eine Anwendung Speicher anfordert – das ist von einem echten Speichermangel zu unterscheiden. Bei Verdacht auf tatsächlichen Mangel ist es sinnvoller, auf den committeten Speicher und die Häufigkeit von Hard Faults zu achten als allein auf die scheinbare Menge an freiem Speicher.
Kommen sich der Inhalt einer speichergemappten Datei und ReadFile/WriteFile bei derselben Datei in die Quere?
Gegenüber gewöhnlichem, cache-fähigem I/O nicht. Windows' eigener Cache ist selbst als Dateizuordnung implementiert, sodass eine gemappte Ansicht und der Cache derselben lokalen Datei dieselben Daten teilen – eine Änderung über den einen Weg ist über den anderen sichtbar. Auch mehrere Prozesse, die Ansichten aus demselben Dateizuordnungsobjekt erzeugen, sehen kohärente Daten. Lese- und Schreibvorgänge über ein mit FILE_FLAG_NO_BUFFERING geöffnetes Handle umgehen den Cache jedoch und fallen aus dieser Kohärenz heraus. Um Änderungen über eine gemappte Ansicht zuverlässig dauerhaft zu machen, reicht außerdem FlushViewOfFile allein nicht aus – es schreibt keine Metadaten und wartet nicht auf einen Hardware-Cache –, weshalb nach FlushViewOfFile noch FlushFileBuffers aufgerufen werden muss.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog