Die Tiefen von Windows I/O (Teil 4) — Cache-Manager: Wann erreicht Ihr WriteFile den Datenträger?

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

Änderungsverlauf (Erstfassung, veröffentlicht am 29. Jul 2026)
Erstveröffentlichung
Diesen Artikel zitieren(DOI (registriertes Archiv): 10.5281/zenodo.22175308)

Die folgenden DOIs verweisen auf bereits archivierte Versionen, die vom aktuellen Text abweichen können. Verwenden Sie die URL dieser Seite, um auf den aktuellen Text zu verweisen.

Go Komura (2026). Die Tiefen von Windows I/O (Teil 4) — Cache-Manager: Wann erreicht Ihr WriteFile den Datenträger?. KomuraSoft LLC. https://comcomponent.com/de/blog/windows-cache-manager-writefile-disk/

DOI (registriertes Archiv)
10.5281/zenodo.22175308
DOI (zuletzt registrierte Version)
10.5281/zenodo.22175309

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

Bei einem gewöhnlichen cache-fähigen Schreibvorgang werden die Daten zuerst an den Cache im Speicher des Betriebssystems übergeben. Ein Erfolg von WriteFile bedeutet „an das Betriebssystem übergeben“, nicht „auf dem Datenträger dauerhaft gespeichert“. Die Übernahme auf den Datenträger erledigt das Betriebssystem später.1

Kennt man diesen Unterschied, lassen sich Phänomene wie „gespeichert, und nach dem Stromausfall war es weg“ oder „die Messwerte einer Dateikopie sind schneller als die Datenträgerleistung“ erklären. Dass Datenbanken mit fsync und Ähnlichem ausdrücklich schreiben, dient genau dazu, Annahme und Dauerhaftigkeit zu trennen.

Dieser Artikel ist Teil 4 der Reihe „Die Tiefen von Windows I/O“. Er nimmt den Cache-Manager zwischen Anwendung und Datenträger auf und ordnet der Reihe nach den Mechanismus des Caches, den Zeitpunkt des Zurückschreibens und die Speicherung von Daten, die man sich nicht zu verlieren leisten kann.

Zugleich erklärt er den Grund aus Teil 2, warum asynchrones I/O bei einem Cache-Treffer synchron abschließt, und den aus Teil 1, Fast I/O als Abkürzung, die kein IRP baut.

1. Zuerst das Fazit

  • Beim Standardschreiben sind das Kopieren in den Cache und die Übernahme auf den Datenträger getrennt. Windows verwendet Write-Back, und der Lazy Writer schreibt später. Daten, die den Cache des Betriebssystems erreicht haben, überstehen einen Absturz allein der Anwendung; bei Stromausfall oder OS-Absturz gehen dirty Pages verloren, die noch nicht übernommen wurden.1
  • Für Daten, die man nicht verlieren darf, wählt man ein Verfahren, das zur Speichergrenze passt. Zum Festschreiben an einem Einschnitt dient FlushFileBuffers (in .NET FileStream.Flush(true)), zum Entfernen der Verzögerung je Schreibvorgang FILE_FLAG_WRITE_THROUGH. FILE_FLAG_NO_BUFFERING ist ein anderes Werkzeug, das den Systemcache umgeht, Ausrichtungsanforderungen hat und den geräteinternen Cache sowie Metadaten weiter berücksichtigen muss. Für häufige Dauerhaftigkeit nennt die Dokumentation die Kombination aus NO_BUFFERING und WRITE_THROUGH.231
  • Auch die Lesegeschwindigkeit und der I/O-Pfad lassen sich aus dem Cache-Mechanismus verstehen. Der Cache ist in Wirklichkeit eine Dateizuordnung, beim Lesen arbeitet Read-Ahead. Kohärenz mit einer gemappten Ansicht und Dauerhaftigkeit denkt man getrennt, Fast I/O als Abkürzung, die ein IRP weglassen kann.456

Je nach Ziel kann man bei den folgenden Abschnitten einsteigen.

Was Sie wissen wollen Leseabschnitt
Was der Cache wirklich ist, warum freier Speicher sinkt, Read-Ahead Abschnitte 2 und 3
Was nach einem Erfolg von WriteFile bei einem Ausfall verloren geht Abschnitt 4
Die Wahl zwischen Flush, WRITE_THROUGH, NO_BUFFERING und die Ausrichtungsanforderungen Abschnitt 5
Kohärenz der gemappten Ansicht und das zweistufige Flush Abschnitt 6
Der Unterschied zwischen Cache-Treffer und Fast I/O Abschnitt 7

In der Abbildung kennzeichnet eine durchgezogene Linie eine stets geltende Beziehung und eine gestrichelte Linie eine bedingte (die Bedingungen stehen bei jeder Beziehung auf der Detailseite). Die vollständige Liste der Beziehungen (32 insgesamt, mit Beleg und Sicherheitsgrad) und die Definitionen der wichtigsten Konzepte sind auf der Detailseite der Wissenskarte (auf Japanisch) zusammengestellt. Daten: JSON-LD / Turtle

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

2.1. 256-KB-Slots und Speicherkopien

Der Cache ist in Wirklichkeit eine Ansicht, die einen Teil einer Datei in den Speicher mappt. Der Cache-Manager mappt 256-KB-Abschnitte einer Datei in „Slots“ des Systemadressraums. Cache-fähige Lese- und Schreibvorgänge laufen als Speicherkopien zwischen diesem Slot und dem Puffer der Anwendung.1

SystemadressraumAnwendung (Benutzermodus)ReadFile/WriteFile =Speicherkopie mit dem SlotEinlesen beim ersten Zugriff undnachgeholtes Zurückschreiben erfolgen seitenweiseSystemdateicacheSlot, der einen 256-KB-Abschnitt der Datei mapptPuffer der Anwendung(der an ReadFile/WriteFile übergebene Bereich)Datei auf dem Datenträger

Abbildung 1: Die Wirklichkeit cache-fähigen I/O. Was aus Anwendungssicht Lesen oder Schreiben einer Datei ist, ist meistens nur eine Speicherkopie

256 KB ist nicht die Einheit, in der auf den Datenträger gelesen und geschrieben wird

256 KB ist die Granularität der Ansicht (des Mappings), nicht die Aussage, Datenträger-I/O erfolge immer in 256-KB-Einheiten. Seiten im Slot werden nach Bedarf eingelesen, und die tatsächliche I/O-Menge hängt von Anforderungsgröße und Zugriffsmuster ab.

Für einen Abschnitt, der zum ersten Mal gelesen wird, entsteht Datenträger-I/O, um den Cache zu füllen, und das in Teil 1 gesehene IRP läuft in den Speicherstapel darunter. Liegt er bereits im Cache, endet das Lesen allein mit einer Speicherkopie.

Auch asynchron ausgegeben kann es an Ort und Stelle verarbeitet werden

„Bei einem Cache-Treffer schließt auch asynchrones I/O synchron ab“ aus Abschnitt 5 von Teil 2 ist das Verhalten, eine Anforderung an Ort und Stelle abzuschließen, wenn sie sich sofort beantworten lässt.

Umgekehrt kann ein asynchrones Lesen auch bei eingeschaltetem Cache synchron verarbeitet werden, wenn die Seite nicht im Speicher liegt, weil die Page-Fault-Verarbeitung keinen asynchronen Mechanismus hat. Sofortigen Abschluss durch einen Cache-Treffer und synchrone Verarbeitung wegen fehlender Seite hält man getrennt.7

2.2. Was „freier Speicher ist gesunken“ wirklich bedeutet

Einstellungen je Öffnungsvorgang und geteilte Daten trennen

Ob der Cache genutzt wird und der Zustand des Read-Ahead werden je Öffnungsvorgang (je Dateiobjekt) verwaltet.1 Die gecachten Daten selbst werden dagegen je Datei (je Stream) geteilt. Mehrfaches Öffnen derselben Datei erzeugt keine getrennten Caches. Dass jedes Handle denselben Cache-Inhalt sieht, ist die Grundlage der Kohärenz in Abschnitt 6.

Der Cache-Manager verwaltet den Cache durchgehend, solange Windows läuft.1 Beim Kopieren einer großen Datei oder bei viel Lesen und Schreiben wird freier physischer Speicher als Cache genutzt. Viel davon ist Standby-Speicher, der sich vergleichsweise schnell umwidmen lässt, sobald eine Anwendung Speicher anfordert.

Gesunkener freier Speicher und Speichermangel sind also nicht dasselbe. Die Beobachtungspraxis mit dieser Unterscheidung behandelt auch „GC-Wartezeit von einem Speicherleck in .NET unterscheiden“.

Am Bildschirm die Veränderung von Standby und Frei beobachten

Im Task-Manager unter Leistung > Speicher zeigt der Balken „Speicherzusammensetzung“ unten In Verwendung / Geändert / Standby / Frei. Der Großteil des Dateicaches landet in Standby, und die Liste rechts summiert ihn als „Zwischengespeichert“. Die Registerkarte Speicher des Ressourcenmonitors zeigt dieselbe Aufteilung mit Kapazitäten.

Kopieren Sie eine Datei von einigen GB und vergleichen Sie vorher und nachher. Standby steigt und Frei sinkt, während In Verwendung kaum wechselt — daran erkennt man, dass frei gewesener Speicher als Cache genutzt wurde.

3. Read-Ahead — Spekulation beim Lesen

Der Cache-Manager liest aus vergangenen Zugriffsmustern den nächsten Abschnitt, der gelesen werden dürfte, vorab ein. Das ist Read-Ahead.

Liest man eine Datei von vorn der Reihe nach, wird das Lesen schneller, wenn die Fortsetzung schon im Cache liegt, bevor die nächste Anforderung kommt. Die Menge des Read-Ahead ist nicht fest; sie ändert sich mit dem erkannten Muster und der Anforderungsgröße.

Verlauf der Leseanforderungen der Anwendungliest von vorn der Reihe nachDer Cache-Managererkennt das MusterRead-Ahead: den folgenden Abschnitteinlesen, bevor er angefordert wird(Menge je nach Muster und Anforderungsgröße variabel)Hinweis FILE_FLAG_SEQUENTIAL_SCAN= Read-Ahead aktiv betreibenHinweis FILE_FLAG_RANDOM_ACCESS= Read-Ahead wäre verschwendet, deshalb drosseln

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

FileOptions.SequentialScan / RandomAccess aus der Zuordnungstabelle in Teil 1 sind Hinweise an dieses Read-Ahead. Die Anwendung teilt dem Betriebssystem mit, welchen Zugriff sie plant.

Geplanter Zugriff Win32-Flag Angabe in .NET Hinweis an das Read-Ahead
Stapelverarbeitung, die die ganze Datei der Reihe nach liest FILE_FLAG_SEQUENTIAL_SCAN FileOptions.SequentialScan Read-Ahead aktiv betreiben
Unregelmäßiger Zugriff, etwa dem Index nach FILE_FLAG_RANDOM_ACCESS FileOptions.RandomAccess Read-Ahead drosseln, das leicht verschwendet wäre

Wichtig ist, einen Hinweis zu wählen, der zur Verarbeitung passt; keines von beiden legt die Read-Ahead-Menge fest.

4. Verzögertes Schreiben — was der Erfolg von WriteFile bedeutet

4.1. Der Lazy Writer kommt einmal pro Sekunde

Beim Standardschreiben gibt WriteFile Erfolg zurück, sobald die Daten in den Cache-Slot kopiert sind, und die Übernahme auf den Datenträger wird nachgeholt. Diese Write-Back-Cache-Politik heißt verzögertes Schreiben (lazy writing).1

Die Übernahme trägt der Lazy Writer, den der Cache-Manager einmal pro Sekunde startet. Er legt ein Achtel der kürzlich nicht geflushten Seiten in die Warteschlange und legt nach, wenn mehr zu schreiben ist.1

„Einmal pro Sekunde“ ist der Startabstand der Verarbeitung. Es ist keine Garantie, dass jeder einzelne Schreibvorgang innerhalb einer Sekunde dauerhaft wird.

Das Attribut einer temporären Datei ist ein Hinweis, das Zurückschreiben zu drosseln

Temporäre Dateien mit dem Attribut FILE_ATTRIBUTE_TEMPORARY werden aus dem Flush-Ziel des Lazy Writers genommen, weil man davon ausgeht, dass sie bald gelöscht werden.1 Das Attribut ist jedoch nur ein Hinweis. Bei Speichermangel kann trotzdem zurückgeschrieben werden, und ein bloß temporär klingender Name reicht nicht.

DatenträgerLazy Writer (startet jede Sekunde)SystemcacheAnwendungDatenträgerLazy Writer (startet jede Sekunde)SystemcacheAnwendungIn den Slot kopieren unddie Seite dirty (ungeschrieben) markierenVon hier bis zum Zurückschreiben ist das gefährliche Fensterbei Stromausfall oder OS-Absturz verschwinden diese DatenHier werden sie erstmals dauerhaftWriteFile(Daten)TRUE kommt sofort zurückEin Achtel der dirty Pages wählenGesammelt zurückschreiben

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

4.2. Was verschwindet, je nachdem, was passiert

Zwischen dem Erfolg von WriteFile und dem Ende des Zurückschreibens bleibt ein Zeitraum, in dem die Daten noch im Cache liegen. Was in dieser Zeit aus den Daten wird, teilt sich danach, ob nur die Anwendung stehen geblieben ist oder das Betriebssystem insgesamt.

Daten unmittelbar nach Erfolg von WriteFile(dirty Pages im Cache)Was ist passiertDer Prozess der Anwendungstürzt ab oder wird beendetDas Betriebssystem bleibt stehen(Stromausfall, Bluescreen)Die Daten bleibender Cache gehört dem Betriebssystem,der Lazy Writer schreibt planmäßig zurückDirty Pages gehen verlorenes bleibt nur, was den Datenträger erreicht hatte

Abbildung 4: Die Trennlinie zwischen Art des Ausfalls und Überleben. Der Cache ist nicht Besitz des Prozesses, sondern des Betriebssystems

Ausfall Was aus Daten wird, die den Cache des Betriebssystems erreicht haben
Absturz oder erzwungenes Beenden der Anwendung Der Cache gehört dem Betriebssystem, also wird zurückgeschrieben, solange das Betriebssystem läuft
Stromausfall oder OS-Absturz Dirty Pages, die noch nicht zurückgeschrieben wurden, gehen verloren; es bleibt nur, was den Datenträger erreicht hatte

„Direkt nach dem Speichern ist die Anwendung abgestürzt, die Datei war aber heil“ kommt daher, dass das Betriebssystem die Daten nach dem Kopieren in den Cache hält. Bei plötzlichem Stromverlust gehen nicht übernommene Cache-Daten verloren, das steht auch in der offiziellen Dokumentation. Die Flush-Häufigkeit wird als Abwägung zwischen Leistung und Zuverlässigkeit justiert.1

Was man im Entwurf zuerst festlegt, ist: „Dürfen diese Daten im Moment eines Stromausfalls verloren gehen?“ Die letzten paar Sekunden eines Protokolls mag man hinnehmen, den festgeschriebenen Datensatz eines Auftrags nicht. Für Schreibvorgänge, die man nicht verlieren darf, verwendet man die Verfahren des nächsten Abschnitts.

5. Der Werkzeugkasten für „sicher geschrieben“

In diesem Abschnitt unterscheidet man drei Verfahren: den vorhandenen Puffer jetzt schreiben, die Schreibverzögerung entfernen, den Systemcache umgehen. Abschnitt 5.4 fasst die Reihenfolge zusammen, in der man nach Zuverlässigkeitsanforderung und tragbaren Kosten wählt.

5.1. FlushFileBuffers — jetzt durchschreiben

FlushFileBuffers schreibt die gepufferten Daten der angegebenen Datei auf das Gerät. Metadaten des Dateisystems werden immer gecacht, deshalb braucht man für Metadaten ein Flush oder WRITE_THROUGH.12

In .NET Flush() und Flush(true) unterscheiden

Aufruf Was geschrieben wird
FileStream.Flush() Den internen Puffer von .NET an das Betriebssystem übergeben. Ein Flush des OS-Caches wird nicht verlangt
FileStream.Flush(true) Zusätzlich zu .NET intern auch Zwischenpuffer des Betriebssystems und Ähnliches flushen

Die Angabe, die unter Windows FlushFileBuffers entspricht, ist FileStream.Flush(true). Den Unterschied zu einem bloßen Flush() sollte man im Blick behalten.8

Auch die Kosten eines Flush bei jedem Schreibvorgang bedenken

Die offizielle Dokumentation merkt an, dass FlushFileBuffers bei vielen Schreibvorgängen jedes Mal ineffizient wird. Für häufige Schreibvorgänge, bei denen jede einzelne Dauerhaftigkeit nötig ist, wird die Kombination aus NO_BUFFERING und WRITE_THROUGH genannt, die weiter unten kommt.2

5.2. FILE_FLAG_WRITE_THROUGH — nur die Verzögerung entfernen

Öffnet man mit FILE_FLAG_WRITE_THROUGH, gehen Schreibvorgänge zugleich in den Cache und ohne Warten auf den Lazy Writer auf den Datenträger.1

Der Systemcache selbst wird nicht umgangen, Lesevorgänge nutzen ihn weiter. Das ist das Verfahren für „den Cache beim Lesen behalten, nur die Verzögerung beim Schreiben entfernen“.

5.3. FILE_FLAG_NO_BUFFERING — ohne den Cache

FILE_FLAG_NO_BUFFERING nimmt den Systemcache von Windows aus den Lese- und Schreibvorgängen. Lesen und Schreiben werden zu I/O auf das Datenträgergerät, ohne den Cache.1

Der Schreibcache im Gerät selbst ist jedoch eine andere Stufe. NO_BUFFERING umgeht ihn nicht, deshalb bleiben WRITE_THROUGH in Kombination oder FlushFileBuffers im Blick, wenn Stromausfallfestigkeit nötig ist.

Es eignet sich für den gebündelten Transfer großer Datenmengen und für Datenbank-Engines, die ihre Puffer selbst verwalten, verlangt aber, dass die Anwendung die folgenden Ausrichtungsanforderungen erfüllt.3

  • Größe und Dateiversatz von Lesen und Schreiben müssen ganzzahlige Vielfache der Sektorgröße des Volumes sein (bei 512-Byte-Sektoren 512, 1024, 1536 …).
  • Auch die Pufferadresse muss an der physischen Sektorgröße ausgerichtet sein (auch mit Blick auf Advanced-Format-Datenträger mit 4096 Byte physischer Sektorgröße).
  • Metadaten werden trotzdem weiter gecacht, vollständige Dauerhaftigkeit braucht also WRITE_THROUGH in Kombination oder FlushFileBuffers.12

Größe, Versatz und Adresse in den drei Punkten angleichen

Ein bloßes Hinzufügen des Flags reicht nicht, um auf NO_BUFFERING umzustellen. Lesen und Schreiben, die die Ausrichtungsanforderungen nicht einhalten, scheitern mit ERROR_INVALID_PARAMETER (87). Zu prüfen sind die folgenden drei Punkte.3

Was anzugleichen ist Bedingung Wie man sie erfüllt
Größe von Lesen und Schreiben Ganzzahliges Vielfaches der Sektorgröße des Volumes lpBytesPerSector von GetDiskFreeSpace holen und auf dieses Vielfache runden
Dateiversatz Dasselbe (auch bei Angabe über Offset in OVERLAPPED) In Vielfachen der Sektorgröße fortschreiten
Pufferadresse An der physischen Sektorgröße ausgerichtet Mit VirtualAlloc belegen (es kommt ein Bereich zurück, der an der Seitengrenze, üblicherweise 4096 Byte, ausgerichtet ist)

An einem minimalen C++-Beispiel die Ausrichtung des Puffers prüfen

Besonders leicht übersieht man den dritten Punkt, die Pufferadresse. Die Adresse, die malloc, new oder ein C#-Array zurückgibt, ist nicht an einer Sektorgrenze ausgerichtet.

Das folgende Beispiel belegt mit VirtualAlloc einen an der Seitengrenze ausgerichteten Bereich. Eine gewöhnliche 4096-Byte-Seitengrenze erfüllt auch die Anforderung eines Advanced-Format-Datenträgers mit 4096 Byte physischer Sektorgröße. Größe und Versatz schreitet man in Vielfachen der geholten Sektorgröße fort.

// C++ / Win32. Die Fehlerbehandlung ist auf das Minimum beschränkt
DWORD sectorsPerCluster = 0, bytesPerSector = 0, freeClusters = 0, totalClusters = 0;
if (!GetDiskFreeSpaceW(L"C:\\", &sectorsPerCluster, &bytesPerSector,
                       &freeClusters, &totalClusters))
{
    return GetLastError();
}

// Die Einheit von Lesen und Schreiben auf ein ganzzahliges Vielfaches der Sektorgröße setzen (hier rund 1 MiB)
const DWORD chunk = (1024 * 1024 / bytesPerSector) * bytesPerSector;

// Den Puffer als an der Seitengrenze ausgerichteten Bereich nehmen (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 gibt das Ergebnis des unmittelbar vorherigen Win32-Aufrufs zurück. Ruft man
    // zuerst VirtualFree auf, wird der Grund für das Scheitern von CreateFileW (Zugriff verweigert,
    // Pfad fehlt usw.) vom Ergebnis der Aufräumarbeit überschrieben, und es kommt nur ein
    // undurchsichtiger Code zurück
    const DWORD err = GetLastError();
    VirtualFree(buffer, 0, MEM_RELEASE);
    return err;
}

// Weil jedes Mal chunk Byte fortgeschritten wird, bleiben Größe und Versatz ausgerichtet
DWORD read = 0;
while (ReadFile(h, buffer, chunk, &read, nullptr) && read > 0)
{
    // Die ersten read Byte von buffer verarbeiten
    // (am Dateiende wird read < chunk. Das ist normal)
}

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

Auch in .NET lässt sich die Verwaltung der Ausrichtung nicht weglassen

In den FileOptions von .NET gibt es keinen Wert, der FILE_FLAG_NO_BUFFERING entspricht. Wenn man ihn braucht, ruft man CreateFile direkt, muss die Ausrichtungsanforderungen dann aber selbst einhalten.

Die Reihenfolge der Überlegung ist nicht „NO_BUFFERING, weil es schneller sein soll“, sondern „NO_BUFFERING, weil wir die Puffer selbst verwalten“.

5.4. Die Wahl ordnen

Standard-WriteFile: bis hier ErfolgLazy Writer (jede Sekunde) / WRITE_THROUGH (sofort)Zeitpunkt des Geräts /FlushFileBuffers verlangt DurchschreibenNO_BUFFERING überspringt den Cache und geht direktPuffer der AnwendungSystemdateicache(dirty Pages)Cache im Datenträgergerätnichtflüchtiges Medium

Abbildung 5: Die Schichten der Daten und wie weit jedes Werkzeug schiebt. Auch die letzte Stufe, der Cache im Datenträgergerät, verdient Aufmerksamkeit

Verfahren Was geschieht Geeignet für
Standard (Cache aktiv) Abschluss mit Cache-Kopie. Übernahme durch den Lazy Writer Die allermeiste Datei-I/O
FlushFileBuffers / Flush(true) Daten plus Metadaten dieses Zeitpunkts durchschreiben Festschreiben an einem Einschnitt (Commit einer Transaktion und Ähnliches)
FILE_FLAG_WRITE_THROUGH Bei jedem Schreibvorgang sofort auf den Datenträger (Lesen bleibt im Cache) Protokoll und Journal, bei denen Schreibvorgänge nicht verloren gehen dürfen
FILE_FLAG_NO_BUFFERING (+WRITE_THROUGH) Ohne Cache. Mit Ausrichtungsanforderungen Eigene Pufferverwaltung, große gebündelte I/O

Zuerst die Zuverlässigkeit festlegen, dann die Kosten prüfen

Zuerst legt man fest, „wie viele Vorgänge bei einem Stromausfall verloren gehen dürfen“. Dann trennt man, ob das, was nicht verloren gehen darf, ein „Einschnitt“ wie das Festschreiben eines Geschäfts ist oder „jeder einzelne Schreibvorgang“.

Dann prüft man, wie viel langsamer das werden darf und ob man eigene Pufferverwaltung und Ausrichtung tragen kann. Man wählt nicht nach dem Namen des Verfahrens, sondern entlang der folgenden Verzweigung.

Ja(Protokoll der letzten Sekunden und Ähnliches)NeinEinschnitt(Festschreiben eines Geschäfts und Ähnliches)Jeder einzelne VorgangNein (gewöhnliche Anwendung)Ja (DB-Engine und Ähnliches)Diese Daten sollen geschrieben werdenDarf es im Moment von Stromausfalloder Bluescreen verloren gehenStandard belassen (Cache aktiv)am schnellsten. Die allermeiste I/O liegt hierWas nicht verloren gehen darf:Einschnitt oder jeder einzelne VorgangAm Einschnitt FlushFileBuffersin .NET Flush(true)Kosten: nur das Warten am EinschnittKann man Puffer selbst verwaltenund die Ausrichtung aus 5.3 erfüllenFILE_FLAG_WRITE_THROUGHbei jedem Schreibvorgang sofort auf den DatenträgerLesen bleibt schnell über den CacheFILE_FLAG_NO_BUFFERING+ FILE_FLAG_WRITE_THROUGHdie Form häufiger Dauerhaftigkeit, die die Dokumentation nennt

Abbildung 6: Die Wahl der Werkzeuge. Die erste Verzweigung ist die Zuverlässigkeitsanforderung, die zweite die tragbaren Kosten. Dass es keinen Weg gibt, bei jedem Vorgang FlushFileBuffers zu rufen, liegt daran, dass die offizielle Dokumentation das, wie in 5.1 gesehen, als ineffizient einstuft

In Speicherentwurf und Leistungsmessung übersetzen

In der Praxis lässt sich die Wahl leichter treffen, wenn man sie an die folgenden drei Muster bindet.

  1. „In eine temporäre Datei schreiben, flushen, umbenennen“ ist der klassische Weg, keine halb zerbrochene Datei zurückzulassen. Den Inhalt durchschreiben und dann über den Namen festschreiben — diese atomare Übergabe behandelt „Grundlagen des wechselseitigen Ausschlusses bei der Dateiintegration“ ausführlich.
  2. Der Datenbank zu überlassen ist ebenfalls ein vollwertiger Entwurf. Wie SQLite Haltbarkeit mit WAL und Flush herstellt, steht in „SQLite aus C# in Business-Apps nutzen“. Die Option, „keine eigene Flush-Strategie zu schreiben“, gibt es immer.
  3. Benchmarks sollte man dem Cache gegenüber misstrauen. Messungen, die „zu schnell lesen“, messen meist den Cache-Treffer ab dem zweiten Mal. Die Messpraxis steht in „Wie man die Geschwindigkeit verschiedener Programmversionen unter Windows korrekt vergleicht“.

Den Cache im Gerät mitdenken

Am Ende von Abbildung 5 bleibt der Cache im Datenträgergerät. FlushFileBuffers verlangt das Durchschreiben einschließlich dieser Stufe.

Bei USB-Sticks und externen Datenträgern spielen auch die geräteseitigen Richtlinien „Schnelles Entfernen“ und „Bessere Leistung“ mit. Zum Umgang mit Wechseldatenträgern siehe auch „USB-Geräte aus einer Windows-App ansprechen“.

6. Kohärenz mit speicherabgebildeten Dateien

6.1. Gemappte Ansicht und Cache sehen dieselben Daten

Wenn der Cache in Wirklichkeit eine Dateizuordnung ist, was wird aus einer selbst mit MapViewOfFile erzeugten Ansicht und dem Cache, den ReadFile / WriteFile nutzen?

Gewöhnliches cache-fähiges I/O und eine gemappte Ansicht teilen Daten, die von derselben Datei hinterlegt sind. Werden Seiten des Dateizuordnungsobjekts verdrängt, werden Änderungen in die Datei zurückgeschrieben. Auch wenn mehrere Prozesse Ansichten derselben lokalen Datei aus demselben Dateizuordnungsobjekt erzeugen, ist der sichtbare Inhalt kohärent (einheitlich).4

SystemadressraumAdressraum von Prozess AAnsicht des Cache-Managers(Slot, den ReadFile/WriteFile nutzen)Ansicht von MapViewOfFileDieselben physischen Seiten(von der Datei hinterlegter Speicher)Datei auf dem DatenträgerI/O mit FILE_FLAG_NO_BUFFERING liegtaußerhalb dieser Teilung (direkt auf den Datenträger)

Abbildung 7: Gemappte Ansicht und Cache sehen dieselben von der Datei hinterlegten Seiten. Außerhalb des Rahmens liegt nur NO_BUFFERING

6.2. Kohärenz und Dauerhaftigkeit getrennt prüfen

I/O mit NO_BUFFERING liegt außerhalb dieser Kohärenz. Lesen und Schreiben ohne Cache und der Inhalt über gemappte Ansicht oder Cache werden nicht gegeneinander gehalten. Mischt man sie, muss die Anwendung die Übereinstimmung selbst herstellen.

Außerdem ist sichtbar in der gemappten Ansicht nicht dasselbe wie dauerhaft auf dem Datenträger. Die Dauerhaftigkeit einer gemappten Ansicht stellt man in dieser Reihenfolge her.5

Reihenfolge Aufruf Rolle und Hinweise
1 FlushViewOfFile Das Schreiben der dirty Pages im Bereich beginnen. Metadaten werden nicht geschrieben, und auf den Abschluss des physischen Schreibens aus dem Gerätecache wird nicht gewartet
2 FlushFileBuffers Das Durchschreiben einschließlich Dateimetadaten und Gerätecache verlangen

Die Praxis der Dateizuordnung als Shared Memory (benannte Freigabe, Synchronisierung, Unfallmuster) behandelt „Fallstricke bei Shared Memory und Best Practices für die Praxis“.

7. Fast I/O — die Hausaufgabe aus Teil 1

Fast I/O ist eine Abkürzung für synchrones Lesen und Schreiben auf eine gecachte Datei, die ohne IRP verarbeitet. Ein IRP ist ein „I/O Request Packet“, die Struktur, in der der Kernel eine Anforderung an einen Treiber übergibt.6

7.1. Wann sich ein IRP weglassen lässt und wann der normale Weg zurückkehrt

Die Erklärung in Abschnitt 5.2 von Teil 1, „nicht jedes I/O wird zum IRP“, meint genau diesen Pfad.

Lassen sich Lesen und Schreiben allein mit einer Speicherkopie zum Cache erledigen, muss kein IRP gebaut und durch den Gerätestapel geschickt werden. Fast I/O ruft den Einstiegspunkt des Dateisystems direkt und tauscht Daten mit dem Cache-Manager.6

Lässt sich Fast I/O nicht nutzen — weil es nicht im Cache liegt, Sperren eine Rolle spielen, ein Filter eingreift —, kehrt man auf den gewöhnlichen IRP-Pfad zurück.

Zu beachten ist auch: „Cache-Treffer heißt nicht immer Fast I/O“. Vorgänge auf einem asynchronen Handle (FILE_FLAG_OVERLAPPED) können auch dann über den IRP-Pfad laufen, wenn sie aus dem Cache an Ort und Stelle abschließen. Ob es in Abschnitt 5 von Teil 2 „an Ort und Stelle abschließt“ und ob dieses Kapitel „das IRP weglässt“, sind verschiedene Fragen.

janeinSynchrones Lesen und Schreiben auf ein cache-fähiges HandleLässt sich mit Fast I/O verarbeiten(liegt im Cache und Ähnliches)Fast I/Oohne IRP direkt mit dem Cache kopierenin Procmon als FASTIO_ angezeigtGewöhnlicher PfadIRP bauen und in den Gerätestapel(die Welt von Abbildung 6 in Teil 1)

Abbildung 8: Die Verzweigung von Fast I/O. Deshalb sieht man in Procmon FASTIO_READ und IRP_MJ_READ gemischt

7.2. In der Beobachtung denselben Lesepfad unterscheiden

Dass in der Procmon-Beobachtung von Abschnitt 7 in Teil 1 Zeilen mit FASTIO_ gemischt waren, liegt daran, dass derselbe Lesevorgang verschiedene Pfade nimmt. FASTIO_READ und IRP_MJ_READ liest man als Unterschied zwischen Fast I/O und gewöhnlichem IRP-Pfad.

Diese Abkürzung betrifft auch die Filtertreiber in Teil 6. Minifilter können nicht nur in den gewöhnlichen IRP-Pfad, sondern auch in Fast I/O eingreifen.

8. Zusammenfassung

Der Artikel insgesamt in der Reihenfolge Mechanismus, Ausfall, Entwurfsentscheidung.

Blickwinkel Was festzuhalten ist
Was der Cache ist Eine Ansicht, die 256-KB-Abschnitte einer Datei mappt. Cache-fähiges Lesen und Schreiben ist eine Speicherkopie mit dem Slot; 256 KB ist keine feste Datenträger-I/O-Größe
Lesen Den nächsten Abschnitt vorab einlesen. SequentialScan / RandomAccess sind Hinweise, die das Zugriffsmuster mitteilen
Schreiben und Ausfall Standardmäßig schreibt der Lazy Writer nach. Daten, die den OS-Cache erreicht haben, überstehen einen Absturz allein der Anwendung; bei Stromausfall oder OS-Absturz gehen dirty Pages verloren, die noch nicht übernommen wurden
Wahl des Speicherverfahrens Festschreiben am Einschnitt mit FlushFileBuffers, Verzögerung je Schreibvorgang entfernen mit WRITE_THROUGH. NO_BUFFERING hat Ausrichtungsanforderungen; bei häufiger Dauerhaftigkeit die Kombination mit WRITE_THROUGH erwägen
Kohärenz und Dauerhaftigkeit Gemappte Ansicht und gewöhnliches Cache-I/O teilen Daten. NO_BUFFERING liegt außerhalb; für die Dauerhaftigkeit einer Zuordnung nach FlushViewOfFile noch FlushFileBuffers
I/O-Pfad Fast I/O ist der Pfad, der bei synchronem I/O auf eine gecachte Datei das IRP weglässt. Ein Cache-Treffer wird jedoch nicht immer zu Fast I/O

Auf Write-Back, Read-Ahead und Lazy Writer aufbauend legt man zuerst fest, „dürfen diese Daten bei einem Stromausfall verloren gehen?“. Dann wählt man das Speicherverfahren mit Blick auf Flush-Kosten, stets gecachte Metadaten und den Cache im Gerät.123

Kohärenz der gemappten Ansicht und Abschluss des Schreibens sowie Cache-Treffer und Weglassen des IRP sind jeweils eigene Entscheidungen. Diese Unterscheidung ist der Einstieg, wenn Speicherfehler oder unnatürlich schnelle Benchmarks zu untersuchen sind.456

Weiter geht es in Teil 5, „NTFS-Interna — Das Dateisystem anhand des MFT verstehen“. Bis hier haben wir eine Datei als Versatz und Bytefolge behandelt; dahinter, wie NTFS die Daten legt — MFT, mehrere Datenströme, Journale, Hardlinks — steigt die Reihe in die statische Struktur auf dem Datenträger hinab.

Verwandte Artikel

Verwandte Beratungsleistungen

Die KomuraSoft LLC übernimmt Entwurf und Fehleruntersuchung der Datei-I/O in Windows-Geschäftsanwendungen, etwa „gespeicherte Daten waren weg“ oder „das Dateischreiben ist langsam bzw. verdächtig schnell“.

Quellen

  1. Microsoft Learn, File Caching. Dazu, dass Windows Dateidaten standardmäßig cached, Lesen aus dem Systemdateicache erfolgt und Schreiben ebenfalls in den Cache geht (Write-Back-Cache); dass der Cache je Dateiobjekt verwaltet wird und unter Leitung des Cache-Managers läuft; dass die Politik, Schreiben auf den Datenträger zu verzögern und im Cache zu halten, verzögertes Schreiben (lazy writing) heißt; dass beim Lesen einer Datei ein 256-KB-Abschnitt in einen 256-KB-Slot des Systemadressraums gelesen wird und der Benutzerprozess Daten mit diesem Slot kopiert; dass der Cache-Manager einmal pro Sekunde den Lazy Writer startet, ein Achtel der kürzlich nicht geflushten Seiten in die Datenträger-Schreibwarteschlange legt und bei Bedarf nachlegt; dass temporäre Dateien nicht geflusht werden; dass bei einem plötzlichen Systemausfall wie Stromverlust ungeschriebene Cache-Daten verloren gehen; dass sich Metadaten auch bei deaktiviertem Cache mit FILE_FLAG_NO_BUFFERING weiterhin cachen lassen; dass bei FILE_FLAG_WRITE_THROUGH Daten zugleich in den Cache und ohne Verzögerung des Lazy Writers sofort auf den Datenträger geschrieben werden; und dass Dateisystemmetadaten stets gecacht werden, weshalb ihre Dauerhaftigkeit ein Flush oder FILE_FLAG_WRITE_THROUGH braucht. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15

  2. Microsoft Learn, FlushFileBuffers function. Dazu, dass WriteFile gewöhnlich in einen internen Puffer schreibt und das Betriebssystem regelmäßig auf den Datenträger schreibt; dass FlushFileBuffers alle gepufferten Informationen der angegebenen Datei auf das Gerät schreibt; dass ein Aufruf bei jedem von vielen Schreibvorgängen ineffizient ist und Anwendungen, die bei häufigen Schreibvorgängen die Dauerhaftigkeit wichtiger Daten brauchen, ungepuffertes I/O mit FILE_FLAG_NO_BUFFERING und FILE_FLAG_WRITE_THROUGH verwenden sollten; und dass sich der Aufruf auf ein Volume-Handle (mit Administratorrechten) anwenden lässt, um alle offenen Dateien auf dem Volume zu flushen. ↩ ↩2 ↩3 ↩4 ↩5

  3. Microsoft Learn, File Buffering. Zu den Zugriffsanforderungen an eine mit FILE_FLAG_NO_BUFFERING geöffnete Datei: dass Größe und Dateiversatz von Lesen und Schreiben (einschließlich Angabe über OVERLAPPED) ganzzahlige Vielfache der Sektorgröße des Volumes sein müssen, dass die Adresse des Lese- und Schreibpuffers an der physischen Sektorgröße ausgerichtet sein sollte, und dass Advanced-Format-Geräte mit 4096 Byte physischer Sektorgröße zu berücksichtigen sind. ↩ ↩2 ↩3 ↩4

  4. Microsoft Learn, File Mapping. Dazu, dass ein Dateizuordnungsobjekt von einer Datei auf dem Datenträger hinterlegt ist und das Auslagern einer Seite als Schreiben der Änderungen in die Datei erfolgt; und dazu, dass Daten kohärent sind (derselbe Inhalt wie die Datei auf dem Datenträger), 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 Pages im Bereich einer gemappten Ansicht auf den Datenträger beginnt; dass diese Funktion Dateimetadaten nicht flusht und den Abschluss des physischen Schreibens aus dem Hardware-Datenträgercache nicht abwartet; und dass man nach FlushViewOfFile FlushFileBuffers aufrufen sollte, um dirty Pages und Metadaten vollständig physisch durchzuschreiben. ↩ ↩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 gecachte Dateien ist, der ohne Erzeugen eines IRP den Einstiegspunkt von Dateisystem oder Cache-Manager direkt aufruft; dass Daten direkt vom Cache in den Benutzerpuffer (oder umgekehrt) übertragen werden; und dass der gewöhnliche IRP-Pfad verwendet wird, wenn Fast I/O nicht verarbeiten kann. ↩ ↩2 ↩3 ↩4

  7. Microsoft Learn, Asynchronous disk I/O appears as synchronous on Windows. Dazu, dass eine Anforderung an Ort und Stelle mit TRUE abschließt, wenn die Daten im Cache liegen; und dazu, dass der Cache von Windows als Dateizuordnung umgesetzt ist und es keinen asynchronen Page-Fault-Mechanismus gibt, wenn die Seite fehlt, weshalb cache-fähiges asynchrones Lesen synchron verarbeitet werden kann. ↩

  8. Microsoft Learn, FileStream.Flush method (.NET). Dazu, dass Flush() den internen Puffer des Streams an das Betriebssystem schreibt, und dass Flush(true) zusätzlich alle Zwischen-Dateipuffer (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.

Sind die Daten bereits auf dem Datenträger, sobald WriteFile Erfolg zurückgibt?
Standardmäßig nicht. Der Windows-Dateicache ist ein Write-Back-Cache, und WriteFile gibt Erfolg zurück, sobald die Daten in den Systemdateicache kopiert wurden. Das eigentliche Schreiben auf den Datenträger ü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 dem Datenträger 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 den Datenträger. Drittens FILE_FLAG_NO_BUFFERING, das den Cache vollständig umgeht. Die Microsoft-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 den Datenträger 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 der Cache von Windows selbst – ein im Speichergerät 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 speicherabgebildeten Datei und ReadFile/WriteFile bei derselben Datei in die Quere?
Gegenüber gewöhnlichem, cache-fähigem I/O nicht. Der Cache von Windows ist selbst als Dateizuordnung umgesetzt, 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