Historique des révisions (première version, publiée le 29 Jul 2026)
- Première publication
Citer cet article(DOI (archive enregistrée): 10.5281/zenodo.22175312)
Les DOI ci-dessous renvoient à des versions déjà archivées et peuvent différer du texte actuel. Pour citer le texte actuel, utilisez l’URL de cette page.
Go Komura (2026). Les profondeurs de l'E/S Windows (partie 4) — Cache Manager : quand votre WriteFile atteint-il vraiment le disque ?. KomuraSoft LLC. https://comcomponent.com/fr/blog/windows-cache-manager-writefile-disk/
- DOI (archive enregistrée)
- 10.5281/zenodo.22175312
- DOI (dernière version enregistrée)
- 10.5281/zenodo.22175313
WriteFile a renvoyé un succès. Alors, où se trouve la donnée à cet instant ?
Dans une écriture ordinaire avec cache activé, la donnée est d’abord remise à un cache dans la mémoire de l’OS. Le succès de WriteFile signifie « remis à l’OS », et non « rendu durable sur le disque ». L’écriture sur le disque, l’OS la fait ensuite.1
Une fois cette différence connue, on peut expliquer des phénomènes comme « on avait enregistré, mais après une coupure de courant, c’était perdu » et « le débit mesuré d’une copie de fichiers dépasse les performances du disque ». Les bases de données écrivent explicitement avec fsync et équivalents pour la même raison : séparer l’acceptation d’une écriture de sa durabilité.
Cet article est la partie 4 de la série « Les profondeurs de l’E/S Windows ». Il prend le Cache Manager qui se tient entre les applications et le disque, et reprend dans l’ordre le mécanisme du cache, le moment de l’écriture, et la façon de sauvegarder les données que l’on ne peut pas se permettre de perdre.
En chemin, il explique le « pourquoi une E/S asynchrone se termine aussi de façon synchrone si la donnée est déjà dans le cache » de la partie 2, et le « fast I/O, le raccourci qui ne crée pas d’IRP » de la partie 1.
1. D’abord la conclusion
- Dans une écriture par défaut, la copie vers le cache et l’écriture sur le disque sont deux choses distinctes. Windows utilise un cache en écriture différée (write-back), et le lazy writer écrit plus tard. Les données passées au cache de l’OS survivent au plantage de l’application seule, mais une coupure de courant ou un plantage de l’OS perd les pages sales pas encore écrites.1
- Pour les données que l’on ne peut pas perdre, on choisit la méthode qui correspond au grain des sauvegardes. Si l’on confirme à des jalons,
FlushFileBuffers(FileStream.Flush(true)en .NET) est la base ; si l’on veut supprimer le délai à chaque écriture,FILE_FLAG_WRITE_THROUGH.FILE_FLAG_NO_BUFFERINGest un autre outil, qui contourne le cache système, porte des exigences d’alignement, et laisse encore à traiter le cache interne au périphérique et les métadonnées. Pour une durabilité fréquente, la documentation officielle cite la combinaison de NO_BUFFERING et WRITE_THROUGH.231 - La vitesse des lectures et le chemin des E/S se comprennent aussi à partir du mécanisme du cache. Le cache est en réalité un mappage de fichiers, et la lecture anticipée agit sur les lectures. On traite séparément la cohérence avec une vue mappée et la durabilité, et on voit le fast I/O comme un raccourci disponible quand on peut omettre l’IRP.456
Selon le but, on peut commencer à la section suivante.
| Ce que vous voulez savoir | Section à lire |
|---|---|
| La réalité du cache, pourquoi la mémoire libre diminue, la lecture anticipée | Chapitres 2 et 3 |
| Après le succès de WriteFile, ce qu’une défaillance fait disparaître | Chapitre 4 |
| Choix entre Flush, WRITE_THROUGH et NO_BUFFERING, et exigences d’alignement | Chapitre 5 |
| Cohérence des vues mappées et flush en deux étapes | Chapitre 6 |
| Différence entre un cache hit et le fast I/O | Chapitre 7 |
Dans le diagramme, un trait continu marque une relation qui vaut toujours et un trait pointillé une relation conditionnelle (les conditions figurent dans l’explication de chaque relation sur la page de détail). La liste complète des relations (32 au total, avec preuve et niveau de certitude) et les définitions des concepts principaux sont rassemblées sur la page de détail de la carte des connaissances (en japonais). Données : JSON-LD / Turtle
2. La réalité du cache — le fichier est mappé en mémoire
2.1. Des slots de 256 Ko et une copie mémoire
Le cache, dans les faits, est une vue qui mappe une partie du fichier en mémoire. Le Cache Manager mappe des intervalles de 256 Ko du fichier dans des « slots » de l’espace d’adressage système. Les lectures et écritures avec cache activé s’exécutent comme une copie mémoire entre ce slot et le tampon de l’application.1
flowchart TB
subgraph U["Application (mode utilisateur)"]
BUF["Tampon de l'application<br/>(zone passée à ReadFile/WriteFile)"]
end
subgraph S["Espace d'adressage système"]
SLOT["Cache de fichiers système<br/>slot mappant un intervalle de 256 Ko du fichier"]
end
DISK[("Fichier sur le disque")]
BUF <-->|"ReadFile/WriteFile =<br/>copie mémoire avec le slot"| SLOT
SLOT <-->|"Lecture au premier accès et<br/>écriture différée, par pages"| DISK
Figure 1 : Réalité d’une E/S avec cache activé. La « lecture/écriture de fichier » vue depuis l’application n’est, le plus souvent, qu’une copie mémoire.
256 Ko n’est pas l’unité de lecture/écriture vers le disque
256 Ko est la granularité de la vue (du mappage), pas le signe que les E/S disque se font toujours par blocs de 256 Ko. Les pages à l’intérieur du slot sont lues selon le besoin, et le volume réel d’E/S dépend de la taille de la requête et du motif d’accès.
Pour un intervalle lu pour la première fois, une E/S disque se produit pour remplir le cache, et l’IRP vu dans la partie 1 descend dans la pile de stockage. S’il est déjà dans le cache, la lecture se termine par une simple copie mémoire.
Même émise en asynchrone, une requête peut être traitée sur place
Le « un cache hit fait qu’une émission asynchrone se termine aussi de façon synchrone » du chapitre 5 de la partie 2 est précisément ce mouvement : terminer sur place une requête à laquelle on peut répondre tout de suite.
À l’inverse, même avec le cache activé, s’il n’y a pas de page en mémoire, il n’existe pas de mécanisme asynchrone pour le traitement du défaut de page, et une lecture asynchrone peut donc être traitée de façon synchrone. L’achèvement immédiat par cache hit et le traitement synchrone faute de page sont deux choses à distinguer.7
2.2. L’identité du « la mémoire libre a diminué »
Séparer les réglages par façon d’ouvrir, et les données partagées
Le fait d’utiliser le cache, et l’état de la lecture anticipée, se gèrent par façon d’ouvrir (par objet fichier).1 En revanche, le corps des données mises en cache est partagé par fichier (par flux). Ouvrir plusieurs fois le même fichier ne crée pas des caches distincts. Que chaque handle voie le même contenu de cache est le socle de la cohérence du chapitre 6.
Le Cache Manager gère le cache de façon continue tant que Windows tourne.1 Lors d’une copie de gros fichier ou d’un grand volume de lectures/écritures, la mémoire physique libre est utilisée comme cache. Une grande part de cette mémoire est en attente, et peut être réaffectée assez vite si une application demande de la mémoire.
Donc « la mémoire libre a diminué » et « la mémoire ne suffit plus » ne sont pas la même chose. La pratique d’observation qui tient compte de cette distinction est aussi traitée dans « Distinguer l’attente du GC d’une fuite mémoire en .NET ».
À l’écran, regarder l’évolution de « veille » et de « libre »
Dans le Gestionnaire des tâches, sous « Performances > Mémoire », la barre « composition de la mémoire » en bas affiche en cours d’utilisation / modifié / veille / libre. La majeure partie du cache de fichiers entre en veille, et dans la liste de droite elle est totalisée comme « en cache ». L’onglet « Mémoire » du Moniteur de ressources permet de vérifier les mêmes catégories avec les capacités.
Copiez un fichier de quelques Go et comparez avant et après. Le mouvement « la veille augmente, le libre diminue, alors que “en cours d’utilisation” ne change presque pas » confirme que la mémoire qui était libre a été utilisée comme cache.
3. Lecture anticipée — la spéculation sur les lectures
Le Cache Manager lit à l’avance, d’après le motif d’accès passé, l’intervalle qui a des chances d’être lu ensuite. C’est la lecture anticipée (read-ahead).
Quand on lit un fichier depuis le début, dans l’ordre, si les données suivantes sont déjà dans le cache avant la requête suivante, la lecture est d’autant plus rapide. La quantité de lecture anticipée n’est pas fixe : elle varie selon le motif détecté et la taille des requêtes.
flowchart LR
A["Historique des requêtes de lecture de l'application<br/>lecture depuis le début, dans l'ordre"]
D{"Le Cache Manager<br/>détecte un motif"}
R["Lecture anticipée : lire l'intervalle suivant<br/>avant qu'il soit demandé<br/>(quantité variable selon le motif et la taille)"]
H1["Indice FILE_FLAG_SEQUENTIAL_SCAN<br/>= lecture anticipée plus agressive"]
H2["Indice FILE_FLAG_RANDOM_ACCESS<br/>= la lecture anticipée serait du gaspillage, la réduire"]
A --> D
D --> R
H1 -.-> D
H2 -.-> D
Figure 2 : Lecture anticipée. Outre la détection du motif d’accès, on peut donner un indice via les indicateurs de CreateFile.
Les FileOptions.SequentialScan / RandomAccess de la table de correspondance de la partie 1 sont précisément ces indices vers la lecture anticipée. L’application communique à l’OS le programme d’accès qu’elle connaît.
| Programme d’accès | Indicateur Win32 | Spécification .NET | Indice pour la lecture anticipée |
|---|---|---|---|
| Traitement par lots qui lit le fichier entier dans l’ordre | FILE_FLAG_SEQUENTIAL_SCAN |
FileOptions.SequentialScan |
Faire la lecture anticipée de façon agressive |
| Accès irrégulier, comme le parcours d’un index | FILE_FLAG_RANDOM_ACCESS |
FileOptions.RandomAccess |
Réduire une lecture anticipée trop souvent inutile |
Choisir l’indice qui correspond au traitement est l’essentiel : ni l’un ni l’autre ne fixe la quantité de lecture anticipée.
4. Écriture différée — ce que signifie le « succès » de WriteFile
4.1. Le lazy writer passe toutes les secondes
Dans une écriture par défaut, WriteFile renvoie un succès dès qu’il a copié les données dans le slot du cache, et l’écriture sur le disque est reportée. Cette politique de cache en écriture différée (write-back) s’appelle écriture différée (lazy writing).1
Celui qui l’exécute est le lazy writer, que le Cache Manager démarre toutes les secondes. Il met en file un huitième des pages non vidées récemment, et en ajoute si les données à écrire sont trop nombreuses.1
Le « toutes les secondes » ici est l’intervalle de démarrage du traitement. Il ne faut pas le lire comme une garantie que chaque écriture individuelle est rendue durable en moins d’une seconde.
L’attribut de fichier temporaire est un indice pour retenir l’écriture
Un fichier temporaire avec l’attribut FILE_ATTRIBUTE_TEMPORARY est retiré des cibles de vidage du lazy writer, dans l’idée qu’il sera bientôt supprimé.1 L’attribut n’est toutefois qu’un indice. Si la mémoire est sous pression, une écriture reste possible, et un nom qui « a l’air » d’un fichier temporaire ne suffit pas à l’appliquer.
sequenceDiagram
participant App as Application
participant C as Cache système
participant LW as lazy writer (démarré chaque seconde)
participant D as Disque
App->>C: WriteFile(données)
Note over C: Copie dans le slot et<br/>marque la page dirty (non écrite)
C-->>App: TRUE revient tout de suite
Note over App,C: De là jusqu'à l'écriture, la « fenêtre dangereuse »<br/>coupure de courant ou plantage OS : ces données disparaissent
LW->>C: Choisit 1/8 des pages dirty
LW->>D: Les écrit groupées
Note over D: C'est ici seulement qu'elles deviennent durables
Figure 3 : Écriture différée. Le succès de WriteFile signifie « remis à l’OS », pas « rendu durable ».
4.2. Que se passe-t-il, et jusqu’où cela disparaît-il ?
Entre le succès de WriteFile et la fin de l’écriture, il reste une période où les données sont encore dans le cache. Ce qu’il advient de ces données se sépare selon que seule l’application s’est arrêtée, ou l’OS tout entier.
flowchart TB
W["Données juste après le succès de WriteFile<br/>(pages dirty dans le cache)"]
Q{"Que s'est-il passé"}
A1["Le processus de l'application<br/>plante / est tué"]
A2["Arrêt de l'OS tout entier<br/>(coupure de courant, écran bleu)"]
S["Les données restent<br/>le cache appartient à l'OS, donc<br/>le lazy writer les écrit comme prévu"]
L["Les pages dirty sont perdues<br/>il ne reste que ce qui avait atteint le disque"]
W --> Q
Q --> A1
Q --> A2
A1 --> S
A2 --> L
Figure 4 : Type de défaillance et ligne de partage de la survie. Le cache n’est pas « la propriété du processus », c’est « la propriété de l’OS ».
| Défaillance | Qu’advient-il des données passées au cache de l’OS |
|---|---|
| Plantage ou arrêt forcé de l’application | Le cache appartient à l’OS : tant que l’OS tourne, il les écrit plus tard |
| Coupure de courant, plantage de l’OS | Les pages dirty pas encore écrites sont perdues ; il ne reste que ce qui avait atteint le disque |
« L’application est tombée juste après l’enregistrement, et pourtant le fichier était intact » vient de ce que, une fois copié dans le cache, c’est l’OS qui détient les données. En revanche, en cas de perte soudaine d’alimentation, la documentation officielle indique clairement que les données de cache non encore écrites sont perdues. La fréquence des vidages se règle comme un compromis entre performance et fiabilité.1
Ce qu’il faut décider d’abord dans la conception, c’est « ces données, peut-on les perdre à l’instant d’une coupure de courant ? ». Quelques secondes de journal récent, on peut les tolérer ; un enregistrement de commande confirmé, non. Pour les écritures que l’on ne peut pas perdre, on utilise les méthodes du chapitre suivant.
5. La boîte à outils pour « avoir vraiment écrit »
Ce chapitre distingue trois méthodes : vider le tampon actuel, supprimer le délai d’écriture, contourner le cache système. La section 5.4, en dernier, résume l’ordre de choix à partir de l’exigence de fiabilité et du coût que l’on peut payer.
5.1. FlushFileBuffers — tout écrire tout de suite
FlushFileBuffers écrit vers le périphérique les données déjà en tampon du fichier indiqué. Les métadonnées du système de fichiers sont toujours mises en cache, donc pour les faire arriver jusqu’au périphérique il faut un flush, ou WRITE_THROUGH.12
En .NET, distinguer Flush() et Flush(true)
| Appel | Portée de l’écriture |
|---|---|
FileStream.Flush() |
Remet le tampon interne .NET à l’OS. Ne demande pas le vidage du cache de l’OS |
FileStream.Flush(true) |
En plus de l’interne .NET, vide aussi les tampons de fichiers intermédiaires, y compris ceux de l’OS |
Sous Windows, l’équivalent de FlushFileBuffers est FileStream.Flush(true). Il faut être conscient de la différence avec un simple Flush().8
Penser aussi au coût d’un flush à chaque fois
La documentation officielle souligne qu’appeler FlushFileBuffers à chaque écriture, quand elles sont nombreuses, devient inefficace. Quand une durabilité à chaque écriture fréquente est nécessaire, c’est la combinaison de NO_BUFFERING et WRITE_THROUGH, vue plus loin, qui est citée.2
5.2. FILE_FLAG_WRITE_THROUGH — ne retirer que le délai
Ouvert avec FILE_FLAG_WRITE_THROUGH, une écriture va aussi dans le cache, tout en étant répercutée sur le disque sans attendre le lazy writer.1
Le cache système lui-même n’est pas retiré, donc les lectures continuent d’en bénéficier. C’est la méthode pour « garder le cache en lecture, et seulement supprimer le délai d’écriture ».
5.3. FILE_FLAG_NO_BUFFERING — ne pas passer par le cache
FILE_FLAG_NO_BUFFERING est l’indicateur qui retire le cache système de Windows des lectures et écritures. Lectures comme écritures deviennent des E/S vers le périphérique disque, sans passer par le cache.1
Cela dit, le cache d’écriture interne au périphérique est un autre étage. NO_BUFFERING ne le contourne pas jusqu’à là, donc si l’on a besoin de tenir une coupure de courant, on continue d’envisager la combinaison avec WRITE_THROUGH ou FlushFileBuffers.
Cela convient aux transferts groupés de gros volumes, ou aux moteurs de bases de données qui gèrent eux-mêmes leurs tampons, mais l’application doit satisfaire les exigences d’alignement suivantes.3
- La taille et le décalage fichier des lectures/écritures sont un multiple entier de la taille de secteur du volume (pour un secteur de 512 octets : 512, 1024, 1536…).
- L’adresse du tampon est aussi alignée sur la taille de secteur physique (il faut aussi tenir compte des disques « Advanced Format » à secteur physique de 4096 octets).
- Même alors, les métadonnées continuent d’être mises en cache, donc une durabilité complète demande la combinaison avec WRITE_THROUGH ou
FlushFileBuffers.12
Aligner les trois points : taille, décalage, adresse
On ne bascule pas vers NO_BUFFERING en ajoutant seulement l’indicateur. Une lecture/écriture qui ne respecte pas les exigences d’alignement échoue avec ERROR_INVALID_PARAMETER (87). Les trois points à vérifier sont les suivants.3
| Ce qu’il faut aligner | Condition | Comment la satisfaire |
|---|---|---|
| Taille de lecture/écriture | Multiple entier de la taille de secteur du volume | Obtenir lpBytesPerSector de GetDiskFreeSpace et arrondir à ce multiple |
| Décalage fichier | Idem (y compris quand on le spécifie dans Offset de OVERLAPPED) |
Avancer par multiples de la taille de secteur |
| Adresse du tampon | Alignée sur la taille de secteur physique | Allouer avec VirtualAlloc (une zone alignée sur une frontière de page, en général 4096 octets, est renvoyée) |
Un exemple C++ minimal pour vérifier l’alignement du tampon
Le plus facile à manquer est le troisième, l’adresse du tampon. Les adresses renvoyées par malloc, new ou un tableau C# n’offrent aucune garantie d’alignement sur une frontière de secteur.
L’exemple suivant alloue avec VirtualAlloc une zone alignée sur une frontière de page. Une frontière de page habituelle de 4096 octets satisfait aussi l’exigence des disques Advanced Format à secteur physique de 4096 octets. La taille et le décalage avancent par multiples de la taille de secteur obtenue.
// C++ / Win32. Le traitement d'erreur est réduit au minimum
DWORD sectorsPerCluster = 0, bytesPerSector = 0, freeClusters = 0, totalClusters = 0;
if (!GetDiskFreeSpaceW(L"C:\\", §orsPerCluster, &bytesPerSector,
&freeClusters, &totalClusters))
{
return GetLastError();
}
// Unité de lecture/écriture = multiple entier de la taille de secteur (ici l'équivalent de 1 Mio)
const DWORD chunk = (1024 * 1024 / bytesPerSector) * bytesPerSector;
// Le tampon est une zone alignée sur une frontière de page (malloc/new ne le garantissent pas)
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 renvoie le résultat de « l'appel Win32 précédent ». Si l'on
// appelle VirtualFree d'abord, la raison de l'échec de CreateFileW
// (accès refusé, chemin inexistant, etc.) est écrasée par le résultat
// du nettoyage, et il ne reste qu'un code dont on ignore la cause
const DWORD err = GetLastError();
VirtualFree(buffer, 0, MEM_RELEASE);
return err;
}
// On avance à chaque fois de chunk octets, donc taille et décalage restent alignés
DWORD read = 0;
while (ReadFile(h, buffer, chunk, &read, nullptr) && read > 0)
{
// Traiter les read premiers octets de buffer
// (en fin de fichier, read < chunk. C'est normal)
}
CloseHandle(h);
VirtualFree(buffer, 0, MEM_RELEASE);
En .NET aussi, la gestion de l’alignement ne s’omet pas
FileOptions de .NET n’a pas de valeur correspondant à FILE_FLAG_NO_BUFFERING. S’il le faut, on appelle CreateFile directement, mais les exigences d’alignement restent à respecter soi-même.
L’ordre de réflexion n’est pas « NO_BUFFERING pour aller plus vite », c’est « NO_BUFFERING parce que l’on gère soi-même les tampons ».
5.4. Mise en ordre des usages
flowchart TB
A["Tampon de l'application"]
B["Cache de fichiers système<br/>(pages dirty)"]
C["Cache interne au périphérique disque"]
D[("Support d'enregistrement non volatil")]
A -->|"WriteFile par défaut : le succès revient ici"| B
B -->|"lazy writer (chaque seconde) / WRITE_THROUGH (immédiat)"| C
C -->|"Cadence du périphérique /<br/>FlushFileBuffers exige d'écrire jusqu'au bout"| D
A -.->|"NO_BUFFERING saute le cache et va tout droit"| C
Figure 5 : Couches de données, et jusqu’où chaque outil pousse. Attention aussi au dernier étage, le « cache interne au périphérique disque ».
| Méthode | Ce qui se passe | Cas où elle convient |
|---|---|---|
| Par défaut (cache activé) | Termine à la copie dans le cache. L’écriture est le fait du lazy writer | La plupart des E/S fichiers |
FlushFileBuffers / Flush(true) |
Écrit jusqu’au bout les données + métadonnées du moment | Confirmation à un jalon (commit de transaction, etc.) |
FILE_FLAG_WRITE_THROUGH |
Vers le disque à chaque écriture (les lectures restent en cache) | Journal ou log d’écritures que l’on ne peut pas perdre |
FILE_FLAG_NO_BUFFERING (+WRITE_THROUGH) |
Sans passer par le cache. Exigences d’alignement | Gestion de tampon maison, E/S groupées en masse |
Décider d’abord la fiabilité, puis vérifier le coût
On décide d’abord « jusqu’à combien d’éléments peut-on perdre en cas de coupure de courant ». Puis on sépare : ce que l’on ne peut pas perdre, est-ce un « jalon » (confirmation d’une transaction, etc.), ou « chaque écriture » ?
Ensuite, on vérifie combien on peut se permettre de ralentir pour cela, et si l’on peut gérer soi-même les tampons et l’alignement. On ne choisit pas d’après le nom de la méthode, on suit les branches suivantes.
flowchart TB
S["On s'apprête à écrire ces données"]
Q1{"À l'instant d'une coupure de courant ou d'un écran bleu,<br/>peut-on les perdre"}
A0["Laisser le défaut (cache activé)<br/>le plus rapide. La plupart des E/S sont ici"]
Q2{"Ce que l'on ne peut pas perdre,<br/>est-ce un « jalon » ou « chaque écriture »"}
A1["FlushFileBuffers au jalon<br/>en .NET, Flush(true)<br/>coût : seulement l'attente du jalon"]
Q3{"Peut-on gérer soi-même les tampons<br/>et satisfaire les exigences d'alignement de 5.3"}
A2["FILE_FLAG_WRITE_THROUGH<br/>vers le disque à chaque écriture<br/>les lectures restent rapides grâce au cache"]
A3["FILE_FLAG_NO_BUFFERING<br/>+ FILE_FLAG_WRITE_THROUGH<br/>la forme de « durabilité fréquente » citée officiellement"]
S --> Q1
Q1 -->|"Oui<br/>(quelques secondes de journal, etc.)"| A0
Q1 -->|"Non"| Q2
Q2 -->|"Jalon<br/>(confirmation d'une transaction, etc.)"| A1
Q2 -->|"Chaque écriture"| Q3
Q3 -->|"Non (application ordinaire)"| A2
Q3 -->|"Oui (moteur de base de données, etc.)"| A3
Figure 6 : Choix de l’outil. La première branche est l’exigence de fiabilité, la deuxième le coût que l’on peut payer. S’il n’y a pas de chemin « FlushFileBuffers à chaque écriture », c’est, comme on l’a vu en 5.1, que la documentation officielle le juge inefficace.
Le ramener à la conception de la sauvegarde et à la mesure de performance
En pratique, le choix devient plus facile si on le relie aux trois schémas suivants.
- « Écrire dans un fichier temporaire → flusher → renommer » est la règle pour ne pas laisser un fichier à moitié cassé. Écrire le contenu jusqu’au bout, puis confirmer par le nom — ce passage atomique a été traité en détail dans « Les bases du contrôle d’exclusion pour l’intégration par fichiers ».
- S’en remettre à une base de données est aussi une conception tout à fait valable. Le récit de la façon dont SQLite construit la durabilité avec WAL et flush se trouve dans « Utiliser SQLite en C# dans une application métier ». L’option « ne pas écrire soi-même une stratégie de flush » est toujours là.
- Un benchmark, il faut suspecter le cache. Une mesure « trop rapide en lecture » mesure le plus souvent un cache hit à partir de la deuxième fois. La façon de mesurer est rassemblée dans « Comment comparer correctement la vitesse de différentes versions d’un programme sous Windows ».
Penser aussi jusqu’au cache interne au périphérique
Au bout de la figure 5, il reste le cache interne au périphérique disque. FlushFileBuffers exige d’écrire jusqu’à y compris cet étage.
Sur une clé USB ou un disque externe, les politiques de cache d’écriture côté périphérique, « retrait rapide » et « performances élevées », entrent aussi en jeu. Le traitement des périphériques amovibles est aussi traité dans « Comment gérer les périphériques USB dans une application Windows ».
6. Cohérence avec les fichiers mappés en mémoire
6.1. Vue mappée et cache voient les mêmes données
Si le cache est en réalité un mappage de fichiers, que deviennent le contenu d’une vue que l’on a soi-même créée avec MapViewOfFile, et celui du cache qu’utilisent ReadFile / WriteFile ?
Une E/S ordinaire avec cache activé et une vue mappée partagent des données adossées au même fichier. Quand une page de l’objet de mappage est évincée, les modifications sont réécrites vers le fichier. Même si plusieurs processus créent des vues du même fichier local à partir du même objet de mappage, le contenu visible reste cohérent.4
flowchart TB
subgraph P1["Espace d'adressage du processus A"]
V1["Vue de MapViewOfFile"]
end
subgraph SYS["Espace d'adressage système"]
SC["Vue du Cache Manager<br/>(slot utilisé par ReadFile/WriteFile)"]
end
PAGES["Mêmes groupes de pages physiques<br/>(mémoire adossée au fichier)"]
DISK[("Fichier sur le disque")]
V1 --> PAGES
SC --> PAGES
PAGES --> DISK
NB["Les E/S FILE_FLAG_NO_BUFFERING sont<br/>hors de ce partage (directement vers le disque)"]
NB -.-> DISK
Figure 7 : Vue mappée comme cache voient les mêmes « pages adossées au fichier ». Seul NO_BUFFERING est hors cadre.
6.2. Vérifier séparément cohérence et durabilité
Les E/S NO_BUFFERING sont hors de ce cadre de cohérence. Une lecture/écriture qui ne passe pas par le cache n’est pas recoupée avec le contenu d’une vue mappée ou du cache. Si on les mélange, c’est à l’application de rétablir la cohérence.
De plus, voir une modification dans la vue mappée et la voir rendue durable sur le disque sont deux choses. La durabilité d’une vue mappée se fait dans l’ordre suivant.5
| Ordre | Appel | Rôle et points d’attention |
|---|---|---|
| 1 | FlushViewOfFile |
Démarre l’écriture des pages dirty de la plage. N’écrit pas les métadonnées, et n’attend pas non plus la fin de l’écriture physique hors du cache interne au périphérique |
| 2 | FlushFileBuffers |
Exige d’écrire jusqu’au bout, y compris les métadonnées du fichier et le cache interne au périphérique |
La pratique du mappage de fichiers comme mémoire partagée (partage nommé, synchronisation, schémas d’accident) est traitée dans « Pièges de la mémoire partagée et bonnes pratiques concrètes ».
7. Fast I/O — on reprend le devoir de la partie 1
Le fast I/O est un raccourci qui, pour une lecture/écriture synchrone vers un fichier mis en cache, traite sans créer d’IRP. Un IRP est un « I/O Request Packet », la structure dans laquelle le noyau place la requête qu’il passe aux pilotes.6
7.1. Quand on peut omettre l’IRP, et quand on revient au chemin normal
L’explication de la section 5.2 de la partie 1, « ce n’est pas toute E/S qui devient un IRP », désigne précisément ce chemin.
Si une lecture/écriture peut se traiter par une simple copie mémoire avec le cache, il n’est pas nécessaire de construire un IRP et de le faire descendre la pile de périphériques. En fast I/O, on appelle directement le point d’entrée du système de fichiers, et on échange les données avec le Cache Manager.6
Cela dit, si le fast I/O n’est pas utilisable — pas dans le cache, un verrou en jeu, un filtre qui intervient, etc. — on revient au chemin IRP normal.
Il faut aussi noter que « un cache hit n’est pas toujours du fast I/O ». Une opération sur un handle asynchrone (FILE_FLAG_OVERLAPPED) peut être traitée par le chemin IRP même quand elle se termine sur place depuis le cache. Le « se termine-t-elle sur place » du chapitre 5 de la partie 2 et le « omet-on l’IRP » de ce chapitre sont deux discussions distinctes.
flowchart TB
REQ["Lecture/écriture synchrone vers un handle avec cache activé"]
Q{"Peut-on traiter en fast I/O<br/>(déjà dans le cache, etc.)"}
FAST["Fast I/O<br/>copie directe avec le cache, sans créer d'IRP<br/>Procmon affiche FASTIO_"]
IRP["Chemin normal<br/>on construit un IRP et on le fait descendre la pile<br/>(le monde de la figure 6 de la partie 1)"]
REQ --> Q
Q -->|oui| FAST
Q -->|non| IRP
Figure 8 : Branchement du fast I/O. C’est pour cela que Procmon mélange FASTIO_READ et IRP_MJ_READ.
7.2. Dans l’observation, distinguer le chemin de la même lecture
Si, dans l’observation Procmon du chapitre 7 de la partie 1, des lignes FASTIO_ se mêlaient, c’est que la même lecture emprunte des chemins différents. On lit FASTIO_READ et IRP_MJ_READ comme la différence entre fast I/O et chemin IRP normal.
Ce raccourci concerne aussi les pilotes filtres de la partie 6. Un minifiltre peut intervenir non seulement sur le chemin IRP normal, mais aussi sur le fast I/O.
8. Résumé
On reprend l’article entier, dans l’ordre mécanisme, défaillance, jugement de conception.
| Angle | Ce qu’il faut retenir |
|---|---|
| Réalité du cache | Une vue mappant un intervalle de 256 Ko du fichier. Les lectures/écritures avec cache activé sont des copies mémoire avec le slot ; 256 Ko n’est pas une taille fixe d’E/S disque |
| Lectures | L’intervalle suivant est lu à l’avance. SequentialScan / RandomAccess sont des indices qui communiquent le motif d’accès |
| Écritures et défaillances | Par défaut, le lazy writer écrit plus tard. Les données passées au cache de l’OS survivent au plantage de l’application seule, mais une coupure de courant ou un plantage de l’OS perd les pages dirty pas encore écrites |
| Choix de la méthode de sauvegarde | Confirmation à un jalon : FlushFileBuffers ; supprimer le délai à chaque écriture : WRITE_THROUGH. NO_BUFFERING a des exigences d’alignement ; pour une durabilité fréquente, envisager la combinaison avec WRITE_THROUGH |
| Cohérence et durabilité | Vue mappée et E/S cache ordinaires partagent les données. NO_BUFFERING est hors cadre ; pour la durabilité d’un mappage, FlushFileBuffers après FlushViewOfFile |
| Chemin d’E/S | Le fast I/O est le chemin qui omet l’IRP pour une E/S synchrone vers un fichier mis en cache. Un cache hit ne devient toutefois pas toujours du fast I/O |
En tenant compte du write-back, de la lecture anticipée et du lazy writer, décidez d’abord « ces données, peut-on les perdre en cas de coupure de courant ». Ensuite seulement, choisissez la méthode de sauvegarde en pensant au coût du flush, aux métadonnées toujours mises en cache, et jusqu’au cache interne au périphérique.123
La cohérence d’une vue mappée et l’achèvement de l’écriture, le cache hit et l’omission de l’IRP, sont aussi des jugements distincts. Cette distinction devient le fil à tirer pour enquêter sur un incident de sauvegarde ou un benchmark anormalement rapide.456
La suite est la partie 5, « Structure interne de NTFS — comprendre le système de fichiers à travers la MFT ». Jusqu’ici, on a traité un fichier comme « un décalage et une suite d’octets » ; derrière, comment NTFS dispose-t-il les données — MFT, flux de données multiples, journal, liens physiques — on descend vers la structure statique sur le disque.
Articles connexes
- Les profondeurs de l’E/S Windows (partie 1) — Chaque lecture et écriture devient un IRP : la vue d’ensemble du système d’E/S
- Les profondeurs de l’E/S Windows (partie 2) — E/S synchrones et asynchrones : ce que signifie vraiment OVERLAPPED
- Les profondeurs de l’E/S Windows (partie 3) — Les ports d’achèvement d’E/S (IOCP) et le pool de threads .NET : le sous-sol d’async/await
- Les bases du contrôle d’exclusion pour l’intégration par fichiers - bonnes pratiques de verrouillage de fichiers et de claim atomique
- Pièges de la mémoire partagée et bonnes pratiques concrètes
- Utiliser SQLite en C# dans une application métier — mode WAL, contrôle d’accès exclusif, prévention de la corruption, et quand choisir EF Core
- Comment comparer correctement la vitesse de différentes versions d’un programme sous Windows
- Comment gérer les périphériques USB dans une application Windows — Choisir entre COM virtuel, HID, WinUSB et SDK dédié
Domaines de conseil associés
KomuraSoft LLC prend en charge la conception et l’investigation d’incidents d’E/S fichiers dans les applications métier Windows, pour des problèmes du type « des données que l’on croyait enregistrées ont disparu » ou « les écritures de fichiers sont lentes, ou suspicieusement rapides ».
- Développement d’applications Windows
- Investigation de bugs et analyse de cause racine
- Réutilisation des actifs existants et accompagnement à la migration
- Contact
Références
-
Microsoft Learn, File Caching. Sur le fait que Windows met en cache par défaut les données de fichiers, que les lectures se font depuis le cache de fichiers système et que les écritures y vont aussi, ce qui en fait un cache en écriture différée ; que le cache est géré par objet fichier et fonctionne sous la direction du Cache Manager ; que la politique consistant à retarder l’écriture sur le disque et à conserver les données dans le cache s’appelle lazy writing ; que lors d’une lecture, un intervalle de 256 Ko est lu dans un slot de 256 Ko de l’espace d’adressage système, et que le processus utilisateur copie les données vers et depuis ce slot ; que le Cache Manager démarre le lazy writer une fois par seconde, met en file un huitième des pages non vidées récemment pour écriture sur le disque et en ajoute si nécessaire ; que les fichiers temporaires ne sont pas vidés ; que les données de cache non écrites sont perdues en cas de défaillance système soudaine telle qu’une perte d’alimentation ; que les métadonnées de fichier peuvent encore être mises en cache même lorsque le cache est désactivé avec FILE_FLAG_NO_BUFFERING ; qu’avec FILE_FLAG_WRITE_THROUGH les données sont écrites dans le cache et aussi immédiatement sur le disque, sans le délai du lazy writer ; et que, les métadonnées du système de fichiers étant toujours mises en cache, leur durabilité exige un flush ou FILE_FLAG_WRITE_THROUGH. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15
-
Microsoft Learn, FlushFileBuffers function. Sur le fait que WriteFile écrit d’ordinaire dans un tampon interne et que l’OS écrit périodiquement sur le disque ; que FlushFileBuffers écrit vers le périphérique toutes les informations en tampon du fichier indiqué ; que l’appeler après chacune de nombreuses écritures est inefficace, et que les applications qui font des écritures fréquentes nécessitant la durabilité de données importantes doivent utiliser des E/S non tamponnées avec FILE_FLAG_NO_BUFFERING et FILE_FLAG_WRITE_THROUGH ; et que l’appeler sur un handle de volume (avec des privilèges d’administrateur) vide tous les fichiers ouverts du volume. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, File Buffering. Sur les exigences d’accès à un fichier ouvert avec FILE_FLAG_NO_BUFFERING : que la taille et le décalage fichier d’une lecture ou écriture (y compris quand ils sont donnés via OVERLAPPED) doivent être un multiple entier de la taille de secteur du volume ; que l’adresse du tampon de lecture/écriture doit être alignée sur la taille de secteur physique ; et qu’il faut tenir compte des périphériques Advanced Format à secteurs physiques de 4 096 octets. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, File Mapping. Sur le fait qu’un objet de mappage de fichiers est adossé à un fichier sur disque, et que l’éviction d’une page s’effectue comme une écriture des modifications vers le fichier ; et que lorsque plusieurs processus créent des vues d’un fichier local à partir du même objet de mappage, les données sont cohérentes, c’est-à-dire identiques au contenu du fichier sur le disque. ↩ ↩2 ↩3
-
Microsoft Learn, FlushViewOfFile function. Sur le fait que FlushViewOfFile démarre l’écriture sur disque des pages dirty dans la plage d’une vue mappée ; que cette fonction ne vide pas les métadonnées du fichier et n’attend pas non plus la fin de l’écriture physique hors du cache disque matériel ; et que pour écrire physiquement jusqu’au bout toutes les pages dirty et les métadonnées, il faut appeler FlushFileBuffers après FlushViewOfFile. ↩ ↩2 ↩3
-
Microsoft Learn, IRPs Are Different From Fast I/O. Sur le fait que le fast I/O est un chemin rapide pour les E/S synchrones vers des fichiers mis en cache, qui appelle directement les points d’entrée du système de fichiers et du Cache Manager sans générer d’IRP ; que les données sont transférées directement du cache vers le tampon utilisateur ou l’inverse ; et que le chemin normal fondé sur les IRP est utilisé quand le fast I/O ne peut pas traiter la requête. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Asynchronous disk I/O appears as synchronous on Windows. Sur le fait que, lorsque les données sont dans le cache, la requête se termine sur place et TRUE est renvoyé ; et que, le cache de Windows étant implémenté par mappage de fichiers et n’ayant pas de mécanisme de défaut de page asynchrone quand une page manque, une lecture asynchrone avec cache activé peut être traitée de façon synchrone. ↩
-
Microsoft Learn, FileStream.Flush method (.NET). Sur le fait que Flush() écrit les tampons internes du flux vers l’OS, et que Flush(true) vide en plus tous les tampons de fichiers intermédiaires, c’est-à-dire ceux de l’OS. ↩
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Les profondeurs de l'E/S Windows (partie 2) — E/S synchrone et asynchrone : ce que signifie vraiment OVERLAPPED
Deuxième partie d'une série qui explique en schémas l'E/S synchrone et l'E/S asynchrone (Overlapped I/O) de Windows. Elle structure le se...
Les profondeurs de l'E/S Windows (partie 1) — Chaque lecture et écriture devient un IRP : la vue d'ensemble du système d'E/S
Premier volet d'une série qui explique le système d'E/S de Windows en partant de la base. Nous cartographions l'espace de noms de l'Objec...
Les profondeurs de l'E/S Windows (partie 5) — Structure interne de NTFS : comprendre le système de fichiers à travers la MFT
Cinquième partie de la série qui explique la structure interne de NTFS à l'aide de schémas. MFT et enregistrements de fichiers, flux de d...
Les profondeurs de l'E/S Windows (partie 3) — Les ports d'achèvement d'E/S (IOCP) et le pool de threads .NET : le sous-sol d'async/await
Troisième partie de la série qui explique le port d'achèvement d'E/S (IOCP) à l'aide de schémas. Conception qui unifie la file d'achèveme...
Les profondeurs de l'E/S Windows (partie 6, dernière partie) — Fonctionnement des minifilters et enquête de latence avec Procmon
Explique comment les minifilters surveillent et contrôlent les E/S fichiers : FltMgr, altitudes, rappels pre/post, fltmc, repérage des op...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Développement d'applications Windows
Applications métier, intégration d'équipements et outils de communication, des besoins au développement.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Au moment où WriteFile retourne un succès, la donnée est-elle déjà écrite sur le disque ?
- Par défaut, non. Le cache de fichiers de Windows fonctionne en écriture différée (write-back), et WriteFile retourne un succès dès que la donnée a été copiée dans le cache système de fichiers. L'écriture sur le disque est effectuée plus tard par le lazy writer, déclenché chaque seconde par le Cache Manager. Ce qui compte, c'est la différence selon le type de défaillance. Même si le processus de l'application plante, les données déjà dans le cache ne sont pas perdues : elles seront écrites plus tard tant que l'OS reste en vie. En revanche, en cas de défaillance de l'OS lui-même (coupure de courant, écran bleu), le cache dirty pas encore écrit est perdu. Il est exact de comprendre que « WriteFile a réussi » signifie « remis à l'OS », et non « rendu persistant ».
- Comment écrire de façon garantie sur le disque ?
- Il existe trois outils. D'abord, FlushFileBuffers, qui écrit intégralement sur le périphérique les données et métadonnées mises en tampon du fichier (en .NET, FileStream.Flush(true) équivaut à cela). Ensuite, FILE_FLAG_WRITE_THROUGH, qui écrit dans le cache à chaque écriture tout en écrivant aussi immédiatement sur le disque. Enfin, FILE_FLAG_NO_BUFFERING, qui ne passe pas du tout par le cache. La documentation Microsoft indique qu'appeler FlushFileBuffers à chaque écriture est inefficace, et que si une persistance garantie est nécessaire lors d'écritures fréquentes, il vaut mieux combiner FILE_FLAG_NO_BUFFERING et FILE_FLAG_WRITE_THROUGH. Chacun de ces outils ralentit d'autant qu'il renonce aux bénéfices du cache ; en pratique, l'astuce consiste donc à ne pas « tout équiper », mais à réserver leur usage aux écritures de données qu'on ne peut pas se permettre de perdre.
- Quelle est la différence entre FILE_FLAG_WRITE_THROUGH et FILE_FLAG_NO_BUFFERING ?
- WRITE_THROUGH signifie « écrire dans le cache, mais aussi sur le disque avant que l'opération ne se termine ». La lecture continue de bénéficier du cache : seul le délai de l'écriture différée est supprimé. NO_BUFFERING signifie que « la lecture et l'écriture ne passent pas par le cache système », et deviennent à chaque fois une E/S vers le périphérique disque (mais ce qui est court-circuité s'arrête au cache de Windows ; cela ne saute pas jusqu'au cache d'écriture interne au périphérique). En contrepartie, des contraintes strictes s'appliquent : la taille et le décalage de lecture/écriture doivent être des multiples entiers de la taille de secteur du volume, et l'adresse du tampon doit aussi être alignée sur une frontière de secteur physique. De plus, même avec NO_BUFFERING, les métadonnées du système de fichiers continuent d'être mises en cache, donc écrire de façon garantie même les métadonnées nécessite de combiner FlushFileBuffers ou WRITE_THROUGH. C'est typiquement utilisé par des logiciels qui gèrent eux-mêmes leurs tampons, comme les moteurs de base de données ; pour une application ordinaire, il est plus raisonnable d'envisager d'abord WRITE_THROUGH ou FlushFileBuffers.
- Si le Gestionnaire des tâches affiche peu de mémoire disponible, est-ce à cause du cache de fichiers ?
- C'est souvent le cas, et c'est même un comportement normal. Windows utilise activement la mémoire physique libre comme cache de fichiers ; copier un gros fichier ou effectuer de nombreuses lectures/écritures fait gonfler d'autant le cache, ce qui donne l'impression que l'utilisation de la mémoire augmente. Cependant, la plupart des pages utilisées par le cache sont d'un type relativement vite cédé dès qu'une application demande de la mémoire, ce qu'il faut distinguer d'un état où « la mémoire est réellement engloutie et insuffisante ». Pour diagnostiquer un manque de mémoire, il est plus pertinent de ne pas se fier uniquement à la capacité disponible apparente, mais d'examiner aussi des indicateurs comme la mémoire engagée (commit) ou la fréquence des hard faults.
- En manipulant le même fichier à la fois via un fichier mappé en mémoire et via ReadFile/WriteFile, le contenu peut-il diverger ?
- Non, pas avec une E/S classique en cache activé. Le cache de Windows lui-même est implémenté via un mappage de fichiers, et pour un même fichier local, la vue mappée et le cache partagent les mêmes données : une modification de l'un est donc visible depuis l'autre. Même lorsque plusieurs processus créent des vues à partir du même objet de mappage de fichiers, les données restent cohérentes. Cependant, les lectures/écritures sur un handle ouvert avec FILE_FLAG_NO_BUFFERING ne passent pas par le cache et se trouvent donc hors de cette cohérence. Par ailleurs, pour écrire de façon garantie les modifications d'une vue mappée sur le disque, FlushViewOfFile seul ne suffit pas — il n'écrit pas les métadonnées et n'attend pas non plus le cache matériel — il faut donc appeler FlushFileBuffers après FlushViewOfFile.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.