WriteFile acaba de devolver éxito. Y ahora, ¿dónde están realmente los datos?
La respuesta es que, casi con toda seguridad, todavía no están en el disco. Solo se han copiado a una caché en memoria. Por eso ocurre eso de «guardé el archivo, pero al volver tras un corte de energía había desaparecido»; por eso los benchmarks de copia de archivos arrojan velocidades físicamente imposibles; y por eso las bases de datos llaman religiosamente a fsync.
En esta cuarta entrega de la serie «Las profundidades de la E/S de Windows» toca el turno de quien se interpone en medio de todo esto: el administrador de caché. En la segunda parte escribí que «si los datos están en la caché, incluso la E/S asíncrona se completa de forma síncrona», y en la primera parte dejé pendiente el atajo que no crea un IRP (fast I/O). En esta entrega recojo todos esos hilos sueltos.
1. La conclusión, primero
- La caché de archivos de Windows funciona con el método write-back (escritura diferida). Las lecturas se sirven primero desde la caché de archivos del sistema, y las escrituras también van primero a la caché. El sistema operativo se encarga después de reflejarlas en el disco.1
- La caché, en realidad, es una asignación de archivos (file mapping). El administrador de caché mapea tramos de 256 KB del archivo en el espacio de direcciones del sistema, y las lecturas y escrituras se convierten en «copias de memoria hacia y desde esa vista» (capítulo 2).1
- Las escrituras se reflejan más tarde gracias al escritor diferido (lazy writer), que se activa cada segundo. Un bloqueo de la aplicación no hace perder datos, pero un corte de energía o un fallo del sistema operativo sí hace perder la caché sucia (capítulo 4).1
- Hay tres herramientas para «escribir con garantías».
FlushFileBuffers(equivalente aFlush(true)en .NET),FILE_FLAG_WRITE_THROUGHyFILE_FLAG_NO_BUFFERING. Vaciar (flush) la caché en cada escritura es ineficiente cuando las escrituras son frecuentes, y la documentación oficial recomienda combinar NO_BUFFERING con WRITE_THROUGH (capítulo 5).21 - NO_BUFFERING impone requisitos de alineación. El tamaño y el desplazamiento deben ser múltiplos del tamaño de sector, y la dirección del búfer debe estar alineada a un límite de sector físico. Y aun con NO_BUFFERING, los metadatos se siguen almacenando en caché (apartado 5.3).31
- La vista mapeada y la caché comparten los mismos datos. Los archivos mapeados en memoria y la E/S normal con caché son coherentes entre sí, y la persistencia de una asignación se hace en dos pasos:
FlushViewOfFileseguido deFlushFileBuffers(capítulo 6).45 - La lectura y la escritura síncronas cuando los datos ya están en caché pueden no llegar ni siquiera a crear un IRP. Un atajo llamado fast I/O va directo al administrador de caché: esta es la respuesta a lo que quedó pendiente en la primera parte (capítulo 7).6
2. Qué es realmente la caché — el archivo se mapea en memoria
2.1. Ranuras de 256 KB y copias de memoria
Si concibe la caché de archivos de Windows como «un contenedor de bloques de disco», muchos de sus comportamientos dejan de tener explicación. La imagen correcta es esta: el administrador de caché mapea tramos de 256 KB del archivo en «ranuras» del espacio de direcciones del sistema, y la lectura y escritura con caché habilitada se ejecutan como una copia de memoria entre esa ranura y el búfer de la aplicación.1
flowchart TB
subgraph U["Aplicación (modo usuario)"]
BUF["Búfer de la aplicación<br/>(región pasada a ReadFile/WriteFile)"]
end
subgraph S["Espacio de direcciones del sistema"]
SLOT["Caché de archivos del sistema<br/>ranura que mapea un tramo de 256 KB del archivo"]
end
DISK[("Archivo en el disco")]
BUF <-->|"ReadFile/WriteFile =<br/>copia de memoria con la ranura"| SLOT
SLOT <-->|"Carga en el primer acceso y<br/>escritura diferida, por página"| DISK
Figura 1: la realidad de la E/S con caché habilitada. Lo que la aplicación ve como «leer o escribir un archivo» es, en muchos casos, una simple copia de memoria.
Un punto que se malinterpreta con facilidad: los 256 KB son la granularidad de la vista (del mapeo), no significa que la E/S de disco se realice siempre en unidades de 256 KB. Las páginas dentro de la ranura se cargan según se necesitan, y la cantidad de E/S que realmente llega al disco varía según el tamaño de la solicitud y el patrón de acceso. Si es la primera vez que se lee un tramo, se genera E/S de disco para rellenarlo (aquí es donde el IRP de la primera parte baja hacia la pila de almacenamiento). Si ya está en la caché, la lectura se completa con solo una copia. Lo que vimos en el capítulo 5 de la segunda parte —que un acierto de caché («cache hit») hace que incluso una E/S emitida como asíncrona se complete de forma síncrona— era precisamente una manifestación de este comportamiento de «completar en el acto las solicitudes que se pueden responder de inmediato». Y a la inversa, cuando con la caché habilitada la página no está en memoria, como el manejo de fallos de página no tiene un mecanismo asíncrono, puede ocurrir que una lectura asíncrona se procese de forma síncrona: la misma trampa que ya vimos en la segunda parte.7
2.2. La verdad detrás de «la memoria libre ha disminuido»
Si se usa la caché o no, y el estado de la lectura anticipada, se gestionan por cada apertura (por objeto de archivo)1, pero los datos en caché propiamente dichos se comparten a nivel de archivo (stream). Abrir el mismo archivo varias veces no crea cachés separadas: todos los identificadores (handles) ven el mismo contenido de caché (esta es la base de la coherencia que veremos en el capítulo 6). La caché funciona bajo la dirección del administrador de caché mientras Windows esté en marcha.1 Si copia archivos grandes o realiza muchas lecturas y escrituras, la memoria física libre se va reasignando progresivamente a la caché. Aunque la memoria libre del Administrador de tareas parezca disminuir, en gran medida se trata de «memoria en espera que se usa de forma valiosa y que se cede con rapidez en cuanto una aplicación la solicita». Para no confundir esta distinción al diagnosticar una falta de memoria, la práctica de observación también se trató en «Cómo distinguir la espera de GC de una fuga de memoria en .NET».
Este comportamiento se puede comprobar en pantalla. Si abre Administrador de tareas > Rendimiento > Memoria, la barra de «Composición de memoria» de la parte inferior se divide en En uso / Modificada / En espera / Libre. La mayor parte de la caché de archivos cae en esta categoría En espera, y en la lista de la derecha se suma como «En caché». Para verlo con más detalle, en el Monitor de recursos > pestaña Memoria aparecen las mismas categorías con sus capacidades. Si copia un solo archivo de varios GB y vuelve a mirar, verá que «En espera» aumenta y «Libre» disminuye, mientras que «En uso» apenas cambia: es decir, se puede comprobar visualmente que no es que «se haya agotado la memoria», sino que «la memoria que estaba libre se ha usado para la caché».
3. Lectura anticipada — la especulación de la lectura
A partir de los patrones de acceso anteriores, el administrador de caché lee por adelantado el tramo que probablemente se leerá a continuación (read-ahead, lectura anticipada). Si un archivo se está leyendo de forma secuencial, los datos que siguen ya están en la caché antes de que la aplicación los solicite: este es el secreto de la velocidad de las lecturas secuenciales. La cantidad de lectura anticipada no es fija, sino que varía según el patrón detectado y el tamaño de las solicitudes.
flowchart LR
A["Historial de solicitudes de lectura<br/>de la aplicación, leyendo en orden desde el inicio"]
D{"El administrador de caché<br/>detecta el patrón"}
R["Lectura anticipada: carga el tramo<br/>siguiente antes de que se solicite<br/>(la cantidad varía según el patrón y el tamaño)"]
H1["Pista FILE_FLAG_SEQUENTIAL_SCAN<br/>= lectura anticipada más agresiva"]
H2["Pista FILE_FLAG_RANDOM_ACCESS<br/>= se reduce, ya que sería inútil"]
A --> D
D --> R
H1 -.-> D
H2 -.-> D
Figura 2: la lectura anticipada. Además de detectar el patrón de acceso, se le pueden dar pistas mediante los indicadores de CreateFile.
FileOptions.SequentialScan / RandomAccess, que aparecían en la tabla de equivalencias de la primera parte, son pistas para este motor de lectura anticipada. El primero es adecuado para un procesamiento por lotes que «recorre todo», y el segundo para un acceso similar al de recorrer un índice. Su uso queda claro si los concibe como indicadores para contarle al sistema operativo un futuro que solo la aplicación conoce.
4. Escritura diferida — qué significa el «éxito» de WriteFile
4.1. El lazy writer llega cada segundo
Del lado de la escritura, se trata de una caché write-back (de escritura diferida). WriteFile devuelve éxito en el momento en que copia los datos a la ranura, y su reflejo en el disco se pospone. Esta política de «escribir con retraso» es la escritura diferida (lazy writing).1
El encargado de este reflejo es el lazy writer, que el administrador de caché activa cada segundo. Pone en cola una octava parte de las páginas que no se han vaciado (flush) recientemente y las escribe, y si hay más datos pendientes, añade más a la cola. Cabe señalar que los archivos temporales creados con el atributo FILE_ATTRIBUTE_TEMPORARY quedan excluidos del vaciado del lazy writer, porque escribir algo que se supone que se va a borrar enseguida sería un desperdicio.1 Ahora bien, esto es solo una pista dada por el atributo: si la memoria escasea, se puede terminar escribiendo de todos modos, y no se aplica a archivos que «solo tienen nombre de temporal».
sequenceDiagram
participant App as Aplicación
participant C as Caché del sistema
participant LW as lazy writer (cada segundo)
participant D as Disco
App->>C: WriteFile(datos)
Note over C: Copia a la ranura y<br/>marca la página como sucia (sin escribir)
C-->>App: Devuelve TRUE de inmediato
Note over App,C: Desde aquí hasta la escritura es la «ventana de riesgo»,<br/>estos datos se pierden con un corte de energía o un fallo del SO
LW->>C: Selecciona 1/8 de las páginas sucias
LW->>D: Las escribe en bloque
Note over D: Aquí es cuando por fin se persisten
Figura 3: la escritura diferida. El éxito de WriteFile significa «se entregó al sistema operativo», no «se persistió».
4.2. Qué se pierde y hasta dónde, según lo que ocurra
Precisemos qué significa esa «ventana de riesgo». El destino de los datos depende del tipo de fallo.
flowchart TB
W["Datos justo tras el éxito de WriteFile<br/>(páginas sucias en la caché)"]
Q{"¿Qué ha ocurrido?"}
A1["El proceso de la aplicación<br/>se bloquea o se cierra a la fuerza"]
A2["Se detiene todo el sistema operativo<br/>(corte de energía, pantalla azul)"]
S["Los datos sobreviven<br/>como la caché es del SO,<br/>el lazy writer los escribe según lo previsto"]
L["Se pierden las páginas sucias<br/>solo queda lo que ya había llegado al disco"]
W --> Q
Q --> A1
Q --> A2
A1 --> S
A2 --> L
Figura 4: el tipo de fallo determina qué sobrevive. La caché no es «propiedad del proceso», sino «propiedad del sistema operativo».
- Aunque la aplicación muera, los datos no se pierden. En cuanto la copia a la caché se ha completado, el propietario de los datos pasa a ser el sistema operativo. Gracias a esto ocurre eso de «la aplicación se cayó justo después de guardar, pero el archivo estaba intacto».
- Si muere todo el sistema operativo, se pierde la parte sucia. La frecuencia de vaciado (flush) se ajusta como un compromiso entre rendimiento y fiabilidad, y la propia documentación indica explícitamente que «si se produce una pérdida repentina de energía, los datos en caché se pierden».1
En resumen, la pregunta clave al diseñar una aplicación empresarial es: «¿se puede permitir perder estos datos en el instante de un corte de energía?». Unos segundos de registro (log) quizá sí se puedan permitir. Un registro confirmado de un pedido, probablemente no. Las herramientas del siguiente capítulo se reservan solo para lo que no se puede permitir perder.
5. La caja de herramientas para «escribir con garantías»
5.1. FlushFileBuffers — escríbalo todo ahora mismo
FlushFileBuffers escribe hasta el final, en el dispositivo, todos los datos en búfer del archivo indicado. Como los metadatos del sistema de archivos siempre están en caché, otro punto clave es que hace falta un vaciado (o WRITE_THROUGH) para que también los metadatos lleguen con garantías al disco.12 En .NET, esto equivale a FileStream.Flush(true) (con solo Flush(), únicamente se pasa el búfer interno de .NET al sistema operativo, y la caché del sistema operativo permanece intacta).8
Sin embargo, la documentación oficial lo advierte con claridad: llamarlo en cada escritura es ineficiente. Si hace falta persistir con garantías en cada una de muchas escrituras, se debería usar la combinación NO_BUFFERING+WRITE_THROUGH que se describe más adelante.2
5.2. FILE_FLAG_WRITE_THROUGH — elimina solo el retraso
Al abrir con FILE_FLAG_WRITE_THROUGH, la escritura se hace en la caché y, sin esperar al lazy writer, también de inmediato en el disco.1 Lo importante es que la lectura sigue beneficiándose de la caché: es la respuesta directa a «quiero que las lecturas sigan siendo rápidas y solo eliminar el retraso de la escritura».
5.3. FILE_FLAG_NO_BUFFERING — no pasa por la caché
FILE_FLAG_NO_BUFFERING elimina por completo la caché del sistema de la lectura y la escritura. Todas las operaciones se convierten cada vez en E/S al dispositivo de disco, sin pasar por la caché.1 Pero lo que se evita llega solo hasta la caché del sistema de Windows; como muestra la figura 5, la caché de escritura interna del dispositivo es un paso aparte. Si además se necesita resistencia a un corte de energía, sigue haciendo falta combinarlo con WRITE_THROUGH o usar FlushFileBuffers. Es una herramienta pensada para transferencias masivas de datos o para motores de bases de datos que gestionan su propio búfer, pero viene con condiciones estrictas.3
- El tamaño de la operación y el desplazamiento (offset) dentro del archivo deben ser múltiplos exactos del tamaño de sector del volumen (con sectores de 512 bytes: 512, 1024, 1536…).
- La dirección del búfer también debe estar alineada al tamaño de sector físico (también hay que tener en cuenta los discos «Advanced Format» con sectores físicos de 4096 bytes).
- Aun así, los metadatos se siguen almacenando en caché, así que para una persistencia completa hace falta combinarlo con WRITE_THROUGH o usar
FlushFileBuffers.12
Estas «condiciones» son justo donde tropieza primero quien piensa que basta con añadir el indicador. Si lee o escribe sin respetar la alineación, la operación falla con ERROR_INVALID_PARAMETER (87). A continuación resumo los tres puntos que hay que respetar.3
| Qué alinear | Condición | Cómo cumplirla |
|---|---|---|
| Tamaño de lectura/escritura | Múltiplo del tamaño de sector del volumen | Obtenga lpBytesPerSector con GetDiskFreeSpace y redondee a ese múltiplo |
| Desplazamiento (offset) en el archivo | Igual que el anterior (también aplica al Offset de OVERLAPPED) |
Avance en múltiplos del tamaño de sector |
| Dirección del búfer | Alineada al tamaño de sector físico | Resérvela con VirtualAlloc (devuelve una región alineada al límite de página, normalmente 4096 bytes) |
El tercer punto es el que más se pasa por alto. Las direcciones que devuelven malloc, new o un array de C# no garantizan alineación a un límite de sector. Si usa VirtualAlloc, que reserva memoria alineada a un límite de página, también cumple de paso el requisito de los discos «Advanced Format» con sector físico de 4096 bytes. La forma mínima queda así.
// C++ / Win32. El manejo de errores se ha reducido al mínimo
DWORD sectorsPerCluster = 0, bytesPerSector = 0, freeClusters = 0, totalClusters = 0;
if (!GetDiskFreeSpaceW(L"C:\\", §orsPerCluster, &bytesPerSector,
&freeClusters, &totalClusters))
{
return GetLastError();
}
// El tamaño de lectura/escritura se fija como múltiplo del tamaño de sector (aquí, equivalente a 1 MiB)
const DWORD chunk = (1024 * 1024 / bytesPerSector) * bytesPerSector;
// El búfer se reserva alineado a un límite de página (malloc/new no lo garantizan)
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 devuelve el resultado de «la última llamada a Win32». Si se llama
// primero a VirtualFree, el motivo del fallo de CreateFileW (acceso denegado,
// ruta inexistente, etc.) queda sobrescrito por el resultado de la limpieza,
// y solo se obtiene un código sin poder saber la causa
const DWORD err = GetLastError();
VirtualFree(buffer, 0, MEM_RELEASE);
return err;
}
// Como se avanza en bloques de chunk bytes cada vez, tanto el tamaño como el desplazamiento se mantienen alineados
DWORD read = 0;
while (ReadFile(h, buffer, chunk, &read, nullptr) && read > 0)
{
// Procesar los primeros read bytes de buffer
// (al final del archivo, read < chunk; esto es normal)
}
CloseHandle(h);
VirtualFree(buffer, 0, MEM_RELEASE);
Cabe señalar que FileOptions de .NET no tiene ningún valor que corresponda a FILE_FLAG_NO_BUFFERING. Si de verdad lo necesita, tendrá que llamar directamente a CreateFile, y en ese caso también deberá respetar usted mismo los requisitos de alineación anteriores. Considérelo en este orden: no «uso NO_BUFFERING porque quiero velocidad», sino «uso NO_BUFFERING porque gestiono mi propio búfer».
5.4. Resumen de cuándo usar cada opción
flowchart TB
A["Búfer de la aplicación"]
B["Caché de archivos del sistema<br/>(páginas sucias)"]
C["Caché interna del dispositivo de disco"]
D[("Medio de almacenamiento no volátil")]
A -->|"WriteFile por defecto: el éxito se devuelve hasta aquí"| B
B -->|"lazy writer (cada segundo) / WRITE_THROUGH (inmediato)"| C
C -->|"Según el propio dispositivo /<br/>FlushFileBuffers exige escribirlo todo"| D
A -.->|"NO_BUFFERING salta la caché y va directo"| C
Figura 5: las capas de datos y hasta dónde empuja cada herramienta. Preste atención también al último escalón: la «caché interna del dispositivo de disco».
| Método | Qué ocurre | Escenario adecuado |
|---|---|---|
| Por defecto (caché habilitada) | Se completa con la copia a caché; el reflejo lo hace el lazy writer | La mayoría de la E/S de archivos |
FlushFileBuffers / Flush(true) |
Escribe hasta el final los datos y metadatos de ese momento | Confirmación en un punto clave (p. ej., el commit de una transacción) |
FILE_FLAG_WRITE_THROUGH |
Va al disco de inmediato en cada escritura (la lectura sigue en caché) | Registros o diarios con escrituras continuas que no se pueden perder |
FILE_FLAG_NO_BUFFERING (+WRITE_THROUGH) |
No pasa por la caché; tiene requisitos de alineación | Gestión propia de búfer, E/S masiva en bloque |
El orden para elegir tiene dos pasos: primero decida cuánto se puede permitir perder en un corte de energía, y después compruebe cuánta lentitud está dispuesto a aceptar a cambio. En lugar de mirar la tabla de arriba a abajo, siga esta bifurcación.
flowchart TB
S["Va a escribir estos datos"]
Q1{"¿Se puede perder en el instante<br/>de un corte de energía o pantalla azul?"}
A0["Déjelo por defecto (caché habilitada)<br/>Lo más rápido. Aquí va la mayoría de la E/S"]
Q2{"Lo que no se puede perder,<br/>¿es «por punto clave» o «cada registro»?"}
A1["FlushFileBuffers en cada punto clave<br/>en .NET, Flush(true)<br/>Coste: solo la espera de ese punto"]
Q3{"¿Gestiona su propio búfer y puede<br/>cumplir los requisitos de alineación de 5.3?"}
A2["FILE_FLAG_WRITE_THROUGH<br/>Va al disco de inmediato en cada escritura<br/>la lectura sigue rápida, en caché"]
A3["FILE_FLAG_NO_BUFFERING<br/>+ FILE_FLAG_WRITE_THROUGH<br/>La forma de «persistencia frecuente» que recomienda la documentación oficial"]
S --> Q1
Q1 -->|"Se puede permitir<br/>(p. ej., el log de los últimos segundos)"| A0
Q1 -->|"No se puede permitir"| Q2
Q2 -->|"Por punto clave<br/>(p. ej., confirmar una transacción)"| A1
Q2 -->|"Cada registro"| Q3
Q3 -->|"No (aplicación normal)"| A2
Q3 -->|"Sí (motor de BD, etc.)"| A3
Figura 6: cómo elegir la herramienta. La primera bifurcación es el requisito de fiabilidad; la segunda, el coste que se puede pagar. No existe la opción de «FlushFileBuffers en cada registro» porque, como vimos en 5.1, la documentación oficial la considera ineficiente.
Menciono también solo tres patrones prácticos.
- «Escribir en un archivo temporal → vaciar (flush) → renombrar» es la fórmula clásica para no dejar archivos a medio escribir. Escribir el contenido por completo y luego confirmarlo mediante el nombre: esta entrega atómica se trató en detalle en «Conocimientos básicos del control de exclusión en la integración de archivos».
- Delegarlo en una base de datos también es un diseño perfectamente válido. Sobre cómo SQLite construye su durabilidad con WAL y vaciados (flush), consulte «Cómo usar SQLite en aplicaciones empresariales con C#». La opción de «no escribir yo mismo la estrategia de flush» siempre está disponible.
- En un benchmark, sospeche siempre de la caché. Una medición donde «la lectura es demasiado rápida» casi siempre está midiendo un acierto de caché en la segunda pasada o posteriores. La manera correcta de medir está recopilada en «Cómo comparar correctamente la velocidad de versiones de un programa en Windows».
Y no olvide tampoco el último escalón de la figura 5: la caché interna del dispositivo de disco. FlushFileBuffers exige escribirlo todo incluyendo hasta ese punto, pero en memorias USB y discos externos entra en juego la política de caché de escritura del propio dispositivo («Retirada rápida» y «Alto rendimiento»). Sobre el manejo de dispositivos extraíbles, consulte también «Cómo gestionar dispositivos USB en aplicaciones de Windows».
6. Coherencia con los archivos mapeados en memoria
Al leer en la primera parte que «la caché, en realidad, es una asignación de archivos», seguramente algún lector se preguntó lo siguiente: entonces, ¿no entran en conflicto la vista que uno mismo crea con MapViewOfFile y la caché que usan ReadFile/WriteFile?
No entran en conflicto. Porque están montadas sobre el mismo mecanismo. El objeto de asignación de archivos está respaldado por el archivo, y el desalojo de páginas se realiza como una escritura de vuelta al archivo. Aunque varios procesos creen vistas sobre el mismo archivo local, el contenido que se ve es coherente.4
flowchart TB
subgraph P1["Espacio de direcciones del proceso A"]
V1["Vista de MapViewOfFile"]
end
subgraph SYS["Espacio de direcciones del sistema"]
SC["Vista del administrador de caché<br/>(la ranura que usan ReadFile/WriteFile)"]
end
PAGES["El mismo grupo de páginas físicas<br/>(memoria respaldada por el archivo)"]
DISK[("Archivo en el disco")]
V1 --> PAGES
SC --> PAGES
PAGES --> DISK
NB["La E/S con FILE_FLAG_NO_BUFFERING<br/>queda fuera de este reparto (va directa al disco)"]
NB -.-> DISK
Figura 7: tanto la vista mapeada como la caché ven las mismas «páginas respaldadas por el archivo». Lo único que queda fuera es NO_BUFFERING.
Hay dos puntos a tener en cuenta.
- La E/S con
FILE_FLAG_NO_BUFFERINGqueda fuera de esta coherencia. La lectura y escritura que no pasan por la caché no se contrastan con el contenido de la vista mapeada o de la caché. Si mezcla ambas, tendrá que mantener usted mismo la consistencia. - La persistencia de una vista mapeada se hace en dos pasos.
FlushViewOfFileinicia la escritura de las páginas sucias dentro del rango, pero no escribe los metadatos ni espera a la escritura física desde la caché del dispositivo de disco. Para entregarlo con garantías, llame aFlushFileBuffersdespués deFlushViewOfFile.5
La práctica del uso de la asignación de archivos como memoria compartida (memoria compartida con nombre, sincronización, patrones de fallo) se trata en «Trampas de la memoria compartida y buenas prácticas para producción».
7. Fast I/O — recogiendo lo pendiente de la primera parte
Primero, en dos líneas. Fast I/O es un atajo preparado para la lectura y escritura síncronas de archivos que ya están en caché, que intercambia datos directamente con la caché sin construir un IRP (I/O Request Packet, el paquete que el núcleo entrega a los controladores). Que en la columna Operation de Procmon aparezcan mezclados IRP_MJ_READ y FASTIO_READ es simplemente la diferencia de si una misma «lectura» pasó por la ruta normal o por el atajo.
En el apartado 5.2 de la primera parte escribí que «no toda E/S se convierte en un IRP». Aquí está la comprobación.
Se sabe que la lectura y escritura de un archivo que ya está en caché se resuelven con una simple copia de memoria con la caché, sin necesidad de construir un IRP ni de recorrer la pila de dispositivos. Por eso Windows dispone de un atajo llamado fast I/O para la E/S síncrona sobre archivos en caché: sin crear un IRP, llama directamente al «punto de entrada de fast I/O» del sistema de archivos, una ruta que copia directamente desde el administrador de caché.6 Cuando fast I/O no puede procesar la solicitud (no está en caché, hay un bloqueo de por medio, un filtro interviene, etc.), se recurre de vuelta a la ruta normal con IRP. Cabe señalar que se trata de una ruta rápida para solicitudes síncronas, y no significa que «acierto de caché = siempre fast I/O». Las operaciones sobre un identificador asíncrono (FILE_FLAG_OVERLAPPED) pueden procesarse por la ruta con IRP incluso cuando se completan en el acto desde la caché (capítulo 5 de la segunda parte).
flowchart TB
REQ["Lectura o escritura síncrona sobre un identificador con caché habilitada"]
Q{"¿Se puede procesar con fast I/O?<br/>(está en caché, etc.)"}
FAST["Fast I/O<br/>copia directa con la caché, sin crear un IRP<br/>en Procmon aparece como FASTIO_"]
IRP["Ruta normal<br/>construye un IRP y lo envía a la pila de dispositivos<br/>(el mundo de la figura 6 de la primera parte)"]
REQ --> Q
Q -->|Sí| FAST
Q -->|No| IRP
Figura 8: la bifurcación de fast I/O. Por esto en Procmon se ven mezclados FASTIO_READ e IRP_MJ_READ.
Esto explica por qué, en la observación con Procmon del capítulo 7 de la primera parte, aparecían mezcladas líneas con FASTIO_. Para una lectura con acierto de caché, hasta el IRP es un lujo. La existencia de esta ruta también afecta a los controladores de filtro que se tratarán en la sexta parte (los minifiltros también pueden interceptar fast I/O).
8. Resumen
- La caché de archivos de Windows es de tipo write-back, y en realidad es una asignación de tramos de 256 KB del archivo. La lectura y escritura con caché habilitada se convierten en una copia de memoria con la ranura.1
- En la lectura, la lectura anticipada especula el futuro, y
SequentialScan/RandomAccessson las pistas para orientarla.1 - En la escritura, el lazy writer, cada segundo, la refleja después. Aunque muera la aplicación, los datos sobreviven, y si muere todo el sistema operativo, solo se pierde la parte sucia. La pregunta de diseño es: «¿se puede perder este dato en el instante de un corte de energía?».1
- Las herramientas para escribir con garantías son
FlushFileBuffers(confirmación en puntos clave) /WRITE_THROUGH(en cada escritura) /NO_BUFFERING(sin pasar por la caché, con requisitos de alineación). Vaciar en cada escritura es ineficiente, y para una persistencia frecuente la recomendación oficial es combinar NO_BUFFERING con WRITE_THROUGH. Tenga en cuenta también que los metadatos siempre están en caché.231 - La vista mapeada y la caché comparten las mismas páginas y son coherentes entre sí. Lo único que queda fuera es NO_BUFFERING. La persistencia de una asignación se hace en dos pasos:
FlushViewOfFileseguido deFlushFileBuffers.45 - La lectura y escritura síncronas con acierto de caché se resuelven mediante fast I/O, que omite hasta el IRP. Esta es la verdadera identidad de las líneas
FASTIO_que vimos con Procmon en la primera parte.6
La continuación es la quinta parte: «La estructura interna de NTFS — entender el sistema de archivos a partir de la MFT». Hasta ahora hemos tratado el archivo como «un desplazamiento y una secuencia de bytes», pero en esa entrega bajaremos a la estructura estática sobre el disco para ver cómo NTFS organiza los datos por debajo: la MFT, los múltiples flujos de datos, el diario (journal), los enlaces duros.
Artículos relacionados
- Las profundidades de la E/S de Windows (parte 1) — Toda lectura y escritura se convierte en un IRP: panorama general del sistema de E/S
- Las profundidades de la E/S de Windows (Parte 2) ── E/S síncrona y E/S asíncrona: el verdadero significado de OVERLAPPED
- Los entresijos de E/S de Windows (parte 3) — Puertos de finalización de E/S (IOCP) y el grupo de subprocesos de .NET: el sótano de async/await
- Conocimientos básicos del control de exclusión en la integración de archivos — Mejores prácticas de bloqueo de archivos y claim atómico
- Trampas de la memoria compartida y buenas prácticas para producción
- Cómo usar SQLite en aplicaciones empresariales con C# — Modo WAL, control de exclusión, prevención de corrupción y cuándo usar EF Core
- Cómo comparar correctamente la velocidad de versiones de un programa en Windows
- Cómo gestionar dispositivos USB en aplicaciones de Windows — cómo elegir entre COM virtual, HID, WinUSB y SDK propietario
Áreas de consultoría relacionadas
KomuraSoft LLC se ocupa del diseño y la investigación de fallos de la E/S de archivos en aplicaciones empresariales de Windows, en casos como «los datos que creía guardados desaparecieron» o «la escritura de archivos es lenta, o sospechosamente rápida».
- Desarrollo de aplicaciones Windows
- Investigación de fallos y análisis de causas
- Aprovechamiento de activos existentes y apoyo a la migración
- Contacto
Referencias
-
Microsoft Learn, File Caching. Sobre que Windows almacena en caché los datos de archivo por defecto y se trata de una caché write-back en la que las lecturas se sirven desde la caché de archivos del sistema y las escrituras también van a la caché; que la caché se gestiona por objeto de archivo y funciona bajo la dirección del administrador de caché; que la política de retrasar la escritura en disco manteniendo los datos en caché se denomina escritura diferida (lazy writing); que, al leer un archivo, un tramo de 256 KB se carga en una ranura de 256 KB del espacio de direcciones del sistema, y el proceso de usuario copia datos hacia y desde esa ranura; que el administrador de caché activa el lazy writer cada segundo, pone en cola para escribir en disco una octava parte de las páginas que no se han vaciado recientemente, y añade más si es necesario; que los archivos temporales no se vacían; que, si ocurre un fallo repentino del sistema como una pérdida de energía, se pierden los datos en caché que no se habían escrito; que, aunque FILE_FLAG_NO_BUFFERING desactive la caché, los metadatos del archivo pueden seguir almacenándose en caché; que con FILE_FLAG_WRITE_THROUGH los datos se escriben tanto en la caché como, de inmediato y sin el retraso del lazy writer, en el disco; y que, como los metadatos del sistema de archivos siempre están en caché, para persistirlos hace falta un vaciado o FILE_FLAG_WRITE_THROUGH. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19
-
Microsoft Learn, FlushFileBuffers function. Sobre que WriteFile normalmente escribe en un búfer interno y el sistema operativo lo vuelca al disco de forma periódica; que FlushFileBuffers escribe en el dispositivo toda la información en búfer del archivo indicado; que llamarlo en cada una de muchas escrituras es ineficiente, y que una aplicación que necesite persistir datos importantes con escrituras frecuentes debería usar E/S sin búfer mediante FILE_FLAG_NO_BUFFERING y FILE_FLAG_WRITE_THROUGH; y que, si se llama sobre un identificador de volumen (con privilegios de administrador), se pueden vaciar todos los archivos abiertos del volumen. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, File Buffering. Sobre los requisitos de acceso a un archivo abierto con FILE_FLAG_NO_BUFFERING: que el tamaño de la operación y el desplazamiento (offset) dentro del archivo (incluido cuando se especifica mediante OVERLAPPED) deben ser múltiplos exactos del tamaño de sector del volumen; que la dirección del búfer de lectura/escritura debe estar alineada al tamaño de sector físico; y que hay que tener en cuenta los dispositivos Advanced Format con sector físico de 4096 bytes. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, File Mapping. Sobre que el objeto de asignación de archivos está respaldado por el archivo en disco y que el intercambio de salida (swap-out) de páginas se realiza como una escritura de los cambios al archivo; y que, cuando varios procesos crean vistas de un archivo local a partir del mismo objeto de asignación de archivos, los datos son coherentes (con un contenido idéntico al del archivo en disco). ↩ ↩2 ↩3
-
Microsoft Learn, FlushViewOfFile function. Sobre que FlushViewOfFile inicia la escritura en disco de las páginas sucias dentro del rango de la vista mapeada; que esta función no vacía los metadatos del archivo ni espera a que se complete la escritura física desde la caché de disco por hardware; y que, para escribir físicamente hasta el final todas las páginas sucias y los metadatos, se debería llamar a FlushFileBuffers después de FlushViewOfFile. ↩ ↩2 ↩3
-
Microsoft Learn, IRPs Are Different From Fast I/O. Sobre que fast I/O es una ruta rápida de E/S síncrona para archivos en caché que llama directamente a los puntos de entrada del sistema de archivos o del administrador de caché sin generar un IRP; que los datos se transfieren directamente desde la caché al búfer de usuario (o en sentido inverso); y que, cuando fast I/O no puede procesar la solicitud, se usa la ruta normal basada en IRP. ↩ ↩2 ↩3
-
Microsoft Learn, Asynchronous disk I/O appears as synchronous on Windows. Sobre que, cuando los datos están en caché, la solicitud se completa en el acto y devuelve TRUE; y que, como la caché de Windows está implementada mediante asignación de archivos y no existe un mecanismo asíncrono de fallo de página, una lectura asíncrona con caché habilitada puede llegar a procesarse de forma síncrona cuando la página no está presente. ↩
-
Microsoft Learn, FileStream.Flush method (.NET). Sobre que Flush() vuelca al sistema operativo el búfer interno del stream, y que al especificar Flush(true) además se vacían todos los búferes de archivo intermedios (los búferes del sistema operativo). ↩
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Las profundidades de la E/S de Windows (Parte 2) ── E/S síncrona y E/S asíncrona: el verdadero significado de OVERLAPPED
Segunda entrega de la serie que explica con diagramas la E/S síncrona y la E/S asíncrona (E/S superpuesta) de Windows: el significado de ...
Las profundidades de la E/S de Windows (parte 1) — Toda lectura y escritura se convierte en un IRP: panorama general del sistema de E/S
Primera entrega de la serie que explica desde la base el sistema de E/S de Windows: el espacio de nombres del Administrador de objetos, l...
Las profundidades de la E/S de Windows (parte 5) — Estructura interna de NTFS: comprender el sistema de archivos a partir de la MFT
Quinta entrega sobre la estructura interna de NTFS: la MFT, los flujos de datos, los enlaces físicos, los puntos de análisis y los dos di...
Los entresijos de E/S de Windows (parte 3) — Puertos de finalización de E/S (IOCP) y el grupo de subprocesos de .NET: el sótano de async/await
Parte 3 de una serie que explica con diagramas el puerto de finalización de E/S (IOCP): la cola de finalización integrada con el control ...
Las profundidades del I/O en Windows (6.ª entrega, final) ── Controladores de filtro y minifiltros: por qué Procmon y el análisis antivirus pueden interceptar el I/O
Última entrega de la serie sobre controladores de filtro y minifiltros de Windows: el Filter Manager, las altitudes, los callbacks pre/po...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Aplicaciones empresariales, integración de dispositivos y herramientas de comunicación, de los requisitos al desarrollo.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿En el momento en que WriteFile devuelve éxito, los datos ya están escritos en el disco?
- No, por defecto no lo están. La caché de archivos de Windows funciona con el método write-back (escritura diferida): WriteFile devuelve éxito en el momento en que copia los datos a la caché de archivos del sistema. La escritura en el disco la realiza después el escritor diferido (lazy writer), que el administrador de caché pone en marcha cada segundo. Lo importante es la diferencia según el tipo de fallo. Aunque el proceso de la aplicación se bloquee, los datos que ya están en la caché no se pierden, porque se escribirán más tarde mientras el sistema operativo siga en marcha. En cambio, en un fallo que detiene todo el sistema operativo (corte de energía, pantalla azul), se pierde la caché sucia («dirty») que todavía no se había escrito. Es más exacto entender que «WriteFile tuvo éxito» significa «se entregó al sistema operativo», y no «se persistió».
- ¿Cómo puedo garantizar que los datos se escriban en el disco?
- Hay tres herramientas. La primera es FlushFileBuffers, que escribe hasta el final en el dispositivo tanto los datos en búfer de ese archivo como sus metadatos (en .NET esto equivale a FileStream.Flush(true)). La segunda es FILE_FLAG_WRITE_THROUGH, que en cada escritura escribe a la caché y, al mismo tiempo, de inmediato también al disco. La tercera es FILE_FLAG_NO_BUFFERING, que no pasa por la caché en absoluto. La documentación de Microsoft indica que llamar a FlushFileBuffers en cada escritura es ineficiente, y que si se necesita una persistencia garantizada con escrituras frecuentes conviene combinar FILE_FLAG_NO_BUFFERING con FILE_FLAG_WRITE_THROUGH. Todas ellas renuncian a los beneficios de la caché y, por tanto, son más lentas, así que el criterio práctico no es «ponerlas en todo», sino reservarlas para las escrituras de datos que no se pueden permitir perder.
- ¿En qué se diferencian FILE_FLAG_WRITE_THROUGH y FILE_FLAG_NO_BUFFERING?
- WRITE_THROUGH significa «escribir en la caché, pero también escribir en el disco antes de completar la operación». Las lecturas siguen beneficiándose de la caché; lo único que se elimina es el retraso de la escritura diferida. NO_BUFFERING significa que «la lectura y la escritura no pasan por la caché del sistema»: tanto la lectura como la escritura se convierten cada vez en E/S al dispositivo de disco (aunque lo que se evita es solo la caché de Windows, no se salta la caché de escritura interna del propio dispositivo). A cambio, impone restricciones estrictas. El tamaño de la operación y el desplazamiento (offset) dentro del archivo deben ser múltiplos exactos del tamaño de sector del volumen, y la dirección del búfer también debe estar alineada a un límite de sector físico. Además, incluso con NO_BUFFERING, los metadatos del sistema de archivos siguen en caché, así que para escribirlos con garantías también hace falta FlushFileBuffers o combinarlo con WRITE_THROUGH. Es típico de software que gestiona su propio búfer, como los motores de bases de datos; en una aplicación normal, lo razonable es empezar considerando WRITE_THROUGH o FlushFileBuffers.
- ¿La poca memoria libre que muestra el Administrador de tareas se debe a la caché de archivos?
- En muchos casos sí, y además es un comportamiento normal. Windows utiliza de forma activa la memoria física libre como caché de archivos, así que si copia archivos grandes o realiza muchas lecturas y escrituras, la caché crece en consecuencia y el uso de memoria parece aumentar. Sin embargo, la mayoría de las páginas que ocupa la caché son del tipo que se reasigna con relativa rapidez en cuanto una aplicación solicita memoria, y conviene distinguir esto de una situación real de «memoria agotada e insuficiente». Cuando se sospeche de una falta de memoria, en la práctica es más útil fijarse en indicadores como la memoria confirmada (committed) o la frecuencia de fallos de página duros (hard faults), y no solo en la capacidad libre aparente.
- Si toco el mismo archivo con un archivo mapeado en memoria y con ReadFile/WriteFile, ¿pueden desincronizarse los contenidos?
- No, no se desincronizan frente a la E/S normal con la caché habilitada. La propia caché de Windows está implementada como una asignación de archivos (file mapping), y la vista mapeada y la caché de un mismo archivo local comparten los mismos datos, de modo que un cambio hecho por uno es visible desde el otro. Cuando varios procesos crean vistas a partir del mismo objeto de asignación de archivos, los datos también son coherentes entre ellos. Ahora bien, la lectura y escritura de un identificador (handle) abierto con FILE_FLAG_NO_BUFFERING no pasa por la caché, por lo que queda fuera de esta coherencia. Además, para escribir con garantías en disco los cambios de una vista mapeada, no basta con FlushViewOfFile, ya que no escribe los metadatos ni espera a la caché de hardware: hay que llamar a FlushFileBuffers después de FlushViewOfFile.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.