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

· Actualizado el: · · Windows, Win32, I/O, Núcleo, Controladores de dispositivo, .NET, CSharp, Investigación de fallos

Una sola línea de File.ReadAllText, o una sola llamada a ReadFile. ¿Qué ocurre dentro de Windows mientras esa función tarda en devolver el control?

Al abrir Process Monitor durante la investigación de un fallo, aparecen términos desconocidos como IRP_MJ_READ o FASTIO_READ. Al perseguir una fuga de identificadores (handle leak), un archivo que debería haberse cerrado con CloseHandle sigue vivo. «El comportamiento cambia en una unidad de red», «solo es lento en el entorno donde hay un antivirus instalado»: estos fenómenos, que se repiten una y otra vez en el día a día de las aplicaciones empresariales, ocurren todos sobre el mismo terreno: el sistema de E/S de Windows.

Con este artículo comienza la serie «Las profundidades de la E/S de Windows», que excava ese terreno desde su base. Al igual que la célebre obra Windows Internals baja hasta el diseño del núcleo para explicarlo, esta serie tampoco trata sobre «cómo usar la API», sino sobre «por qué funciona así».1 La estructura prevista es la siguiente.

  1. Panorama general del sistema de E/S — toda lectura y escritura se convierte en un IRP (este artículo)
  2. E/S síncrona y asíncrona — el verdadero significado de OVERLAPPED
  3. Los puertos de finalización de E/S (IOCP) y el grupo de subprocesos de .NET — el sótano de async/await
  4. El Administrador de caché — cuándo llega realmente su WriteFile al disco
  5. La estructura interna de NTFS — entender el sistema de archivos a partir de la MFT
  6. Controladores de filtro y minifiltros — por qué Procmon y los antivirus pueden interceptar la E/S

Este primer artículo, base de todas las entregas siguientes, fija con diagramas «los protagonistas y el flujo de las solicitudes». No es un artículo para escribir controladores, sino uno pensado para que el desarrollador de aplicaciones pueda seguir hasta el final el destino de la API que acaba de llamar.

Conocimientos previos: basta con haber usado FileStream en C# o haber llamado a CreateFile/ReadFile en C/C++. No hace falta experiencia en desarrollo de controladores; el código del lado del núcleo solo aparece como diagramas conceptuales. Tiempo estimado de lectura: unos 25 minutos leyendo de corrido y mirando los diagramas, o de 2 a 3 minutos si solo quiere la conclusión del capítulo 1.

Guía de lectura selectiva: como el artículo es largo, aquí tiene puntos de entrada según su objetivo.

  • Solo quiero captar el panorama general → capítulo 1 (conclusión) → capítulo 2 (resolución de nombres) → capítulo 5 (un viaje completo de ReadFile)
  • Quiero ordenar la terminología (objetos driver/device/archivo, IRP) → capítulos 3 y 4
  • Quiero conocer las «formas de romperse» en la práctica (fugas de identificadores, «archivo en uso») → capítulo 6
  • Quiero poner manos a la obra → capítulo 7

1. Ante todo, la conclusión

  • La E/S de Windows está impulsada por paquetes. La mayoría de las solicitudes enviadas a un controlador de dispositivo se empaquetan en un IRP (I/O Request Packet) y fluyen de arriba hacia abajo por la pila de controladores apilados (la pila de dispositivos).23
  • CreateFile no siempre abre un «archivo». La resolución del nombre se realiza en el espacio de nombres del Administrador de objetos, y C: es, en realidad, un vínculo simbólico a un nombre de dispositivo NT como \Device\HarddiskVolume3 (capítulo 2).45
  • Hay tres objetos protagonistas. El «objeto driver» (driver object), que contiene la tabla de funciones de procesamiento; el «objeto device» (device object), que es el destinatario de la solicitud; y el «objeto archivo» (file object), que guarda el estado de una apertura concreta. El HANDLE que usted posee es una referencia al objeto archivo (capítulo 3).6789
  • Cada controlador solo tiene tres opciones. Completar el IRP «por sí mismo», «pasarlo al controlador inferior» o «dejarlo pendiente para completarlo más tarde». La combinación de estas tres opciones explica por completo tanto los controladores de filtro como la caché y la E/S asíncrona (capítulo 4).310
  • En el fondo del núcleo no existe un mecanismo separado llamado «E/S síncrona». La verdadera naturaleza de la E/S síncrona es la garantía de «no retornar antes de completarse»; la espera solo ocurre cuando la solicitud queda pendiente. Este es el punto de entrada de la segunda y la tercera entrega (capítulo 5).11
  • CloseHandle no significa «cerrar», sino «devolver un identificador». Hay dos etapas: cleanup, cuando se cierra el último identificador, y close, cuando desaparecen todas las referencias dentro del núcleo. Esta diferencia es la verdadera causa del «cerrado pero en uso» (capítulo 6).1213
  • Esta capa se puede observar directamente. WinObj permite ver el espacio de nombres y Process Monitor el flujo de los IRP. El vocabulario que muestra Procmon es exactamente la terminología de este artículo (capítulo 7).14

2. La verdadera naturaleza de «todo se ve como un archivo»

2.1. Adónde va el nombre que se pasa a CreateFile

Todas las API de archivos de Win32 empiezan por un «nombre». Una ruta como C:\project\report.csv, una ruta UNC como \\server\share\data.csv, o una designación de dispositivo como \\.\COM3: todas ellas se pueden pasar a la misma función CreateFile.15

Detrás de esta coherencia hay un único espacio de nombres gestionado por el Administrador de objetos del núcleo. Dentro del núcleo, dispositivos, eventos y secciones de memoria compartida se registran todos como «objetos» en este espacio de nombres en forma de árbol. Con WinObj de Sysinternals se puede examinar directamente este espacio de nombres.14

\ (raíz del espacio de nombres)\Device(objetos de dispositivo creados por los controladores)\GLOBAL??(lugar de los nombres globales visibles desde Win32)\BaseNamedObjects(mutexes con nombre, etc.)HarddiskVolume3Serial0Mup (redirector de red)C: → \Device\HarddiskVolume3COM1 → \Device\Serial0PhysicalDrive0 → \Device\Harddisk0\DR0

Figura 1: espacio de nombres del Administrador de objetos (extracto). Lo que hay bajo \GLOBAL?? son vínculos simbólicos; su destino real está bajo \Device

El punto clave es que los nombres que usa una aplicación Win32 (letras de unidad, nombres de puerto COM) y los nombres que usa el núcleo (nombres de dispositivo NT) están en niveles distintos. El vínculo simbólico es lo que conecta ambos.

2.2. La letra de unidad es un vínculo simbólico

Cuando un controlador quiere que su dispositivo sea visible desde una aplicación Win32, crea con IoCreateSymbolicLink un vínculo simbólico desde un nombre de dispositivo MS-DOS, como \DosDevices\COM1, hacia el nombre de dispositivo NT.4 C: funciona igual: en realidad es un vínculo hacia un dispositivo de volumen como \Device\HarddiskVolume3.

Por eso, la resolución del nombre en CreateFile("C:\project\report.csv") avanza así.

Nombre pasado por la aplicaciónC:\project\report.csvLa capa Win32 lo convierte a formato NT\??\C:\project\report.csvEl Administrador de objetos busca en el espacio de nombresy detecta que \??\C: es un vínculo simbólicoSigue el vínculo y lo sustituye\Device\HarddiskVolume3\project\report.csvEn \Device\HarddiskVolume3se llega al objeto de dispositivo del volumenLa resolución del resto, \project\report.csv,queda a cargo del controlador del sistema de archivos (NTFS)cuando el Administrador de E/S emite un IRP_MJ_CREATE

Figura 2: resolución del nombre en CreateFile. La primera mitad es obra del Administrador de objetos; la segunda, una vez alcanzado el dispositivo, es trabajo del sistema de archivos

Este diagrama resuelve varias dudas habituales.

  • El significado del prefijo \\.\. El \\.\ de \\.\PhysicalDrive0 o \\.\COM10 es una notación que apunta directamente al espacio de nombres de dispositivos de Win32 (≈ el directorio \??). Sin pasar por una letra de unidad, designa directamente el lugar donde están los vínculos.515
  • Por qué CON o NUL no se pueden usar como nombre de archivo. Porque están reservados como nombres de dispositivo MS-DOS y pueden resolverse hacia el dispositivo sin importar en qué punto de la ruta aparezcan.5 Las trampas prácticas relacionadas con esto están recopiladas en «MAX_PATH y las trampas de rutas y nombres de archivo en Windows».
  • La ruta UNC tampoco recibe un trato especial. \\server\share se resuelve al dispositivo del redirector de red (\Device\Mup), y a partir de ahí el cliente SMB transporta la solicitud a través de la red. La raíz de los problemas en que el comportamiento cambia entre lo local y lo UNC está en que el dispositivo de destino de la resolución es distinto (véase «Trampas de las unidades de red y las rutas UNC»).

Cabe precisar que \?? no es un alias de un directorio real, sino una entrada virtual que representa un orden de búsqueda: «primero consultar el mapa local de dispositivos DOS de la sesión de inicio de sesión y, si no existe, recurrir a \GLOBAL??». Las letras de unidad creadas con net use o subst entran en este lado local, por lo que en el mismo equipo cada usuario (sesión de inicio de sesión) puede ver unidades distintas, y un servicio puede no ver las unidades de red del usuario. En la figura 1 solo se representó el lado global (\GLOBAL??).

En otras palabras, la forma exacta de decir que «en Windows todo se ve como un archivo» es que «todo nombre acaba resolviéndose en un objeto de dispositivo, y las solicitudes posteriores se unifican en el mismo formato: el IRP». Entonces, ¿qué es ese objeto de dispositivo? Vamos a ordenar los protagonistas.

3. Los protagonistas son tres objetos

La estructura del sistema de E/S de Windows puede describirse como la relación entre tres tipos de objetos del núcleo.

A partir de aquí se suceden términos técnicos. Para no perderse, aquí tiene primero una tabla de correspondencia entre «lo que se ve en el código habitual» y «el nombre que recibe en el lado del núcleo». Los capítulos 3 y 4 están escritos con el vocabulario de la columna derecha.

Lo visible en Win32 / .NET Su equivalente en el núcleo En una frase
HANDLE / SafeFileHandle Entrada de la tabla de identificadores (referencia al objeto archivo) La ficha numerada que apunta a «una apertura» (3.3)
El estado abierto que mantiene FileStream Objeto archivo El contenedor del modo de uso compartido, las marcas y la posición actual (3.3)
La letra de unidad C: o \\.\COM3 Objeto dispositivo (resultado de la resolución del nombre) El destino de la solicitud (capítulo 2 y 3.2)
«el controlador NTFS», «el controlador de disco» Objeto driver La tabla de funciones de procesamiento por tipo de solicitud (3.1)
Una llamada a ReadFile / stream.Read Normalmente, un IRP El paquete de solicitud transportado hasta el destino (capítulo 4). La lectura y escritura síncronas de un archivo que está en caché se resuelven mediante el atajo llamado E/S rápida, sin llegar a crear un IRP (5.2). Son las líneas que en Procmon empiezan por FASTIO_
Las marcas de FileOptions / CreateFile Atributos registrados en el objeto archivo Determinan la lectura anticipada y el comportamiento asíncrono (tabla del capítulo 7)
El valor de GetLastError / las excepciones de .NET NTSTATUS (como STATUS_PENDING) El estado de finalización, que se traduce a un código de error de Win32 al subir

3.1. El objeto driver — la tabla de funciones de procesamiento

Cuando se carga un controlador, el Administrador de E/S crea un objeto driver (DRIVER_OBJECT) que lo representa.6 Desde el punto de vista de un desarrollador de aplicaciones, el miembro más importante es el arreglo MajorFunction. Se trata de una tabla de correspondencia entre «tipo de solicitud → función de procesamiento», donde el tipo de solicitud se expresa mediante códigos de función mayor como IRP_MJ_CREATE (abrir), IRP_MJ_READ (leer), IRP_MJ_WRITE (escribir), IRP_MJ_CLEANUP o IRP_MJ_CLOSE.16

Forzándolo un poco en C#, la idea sería algo así.

// Diagrama conceptual. En realidad es una estructura C dentro del núcleo
class DriverObject
{
    // El índice es IRP_MJ_XXX. Hay 28 tipos en total
    public DispatchRoutine[] MajorFunction = new DispatchRoutine[28];
}
// En ntfs.sys, MajorFunction[IRP_MJ_READ] contiene "el procesamiento de lectura de NTFS"

3.2. El objeto dispositivo — el destino de la solicitud

Un controlador crea un objeto dispositivo (DEVICE_OBJECT) por cada dispositivo del que se hace cargo.7 Este es el destino de la solicitud de E/S. No siempre existe una correspondencia uno a uno con un dispositivo físico: puede tratarse de una entidad lógica como un volumen (HarddiskVolume3), o de un «dispositivo que solo existe para interceptar», creado por un controlador de filtro. Como el objeto dispositivo apunta al objeto driver que lo creó, al determinar el destino queda determinada también la tabla de funciones de procesamiento.

3.3. El objeto archivo — el estado de «una apertura»

Cada vez que CreateFile tiene éxito, el núcleo crea un objeto archivo. No es «el archivo en disco» en sí, sino un objeto que representa una sesión concreta de apertura de ese archivo (o dispositivo).8 Si se abre el mismo archivo dos veces, se crean dos objetos archivo. Aquí se guardan el puntero de archivo actual (en el caso de identificadores síncronos) y el modo de uso compartido y las marcas con que se abrió.

Y el HANDLE que recibe la aplicación es una referencia al objeto archivo, obtenida a través de la tabla de identificadores propia de cada proceso.9

Espacio del núcleoProceso (modo usuario)Objeto archivo 1report.csv abierto para lecturadesplazamiento actual: 4096Objeto archivo 2report.csv abierto para anexardesplazamiento actual: 65536Objeto dispositivoequivalente a HarddiskVolume3Objeto driver NTFSMajorFunction = tabla de funciones de procesamientoHANDLE 0x1A4HANDLE 0x1B8

Figura 3: relación entre los tres objetos. El identificador apunta al objeto archivo a través de la tabla de identificadores; el objeto archivo se conecta al dispositivo, y el dispositivo, al controlador

Con este diagrama en mente, varios conocimientos prácticos dejan de ser «memorización» para volverse «evidentes».

4. El IRP — la solicitud de E/S se convierte en un paquete

4.1. Por qué convertirla en un paquete

Cuando el Administrador de E/S recibe una solicitud de la aplicación (abrir, leer, escribir…), la empaqueta en un paquete llamado IRP (I/O Request Packet) y la entrega al controlador. La mayoría de las solicitudes a un controlador de dispositivo llegan como un IRP.3 Como los dispositivos no van a la misma velocidad que el sistema operativo (un disco es órdenes de magnitud más lento que la CPU), se optó por convertir la solicitud en un «paquete» en lugar de una «llamada», de modo que la emisión y la finalización se puedan separar.2

Además de una cabecera con la información global de la solicitud, el IRP contiene tantas áreas llamadas ubicaciones de pila de E/S como controladores por los que está previsto que pase. Cada controlador lee «las instrucciones dirigidas a él» (el código de función mayor y sus parámetros) desde su propia ubicación de pila.17

4.2. Bajando por la pila de dispositivos

Los objetos dispositivo se apilan y forman la pila de dispositivos.18 Por ejemplo, una lectura de un archivo en un disco local recorre, a grandes rasgos, el siguiente camino.

Pila del lado del almacenamientoPila del lado del sistema de archivosGestión de volumen/partición(volmgr, etc.)Controlador de clase de disco(disk.sys)Puerto de almacenamiento/miniport(storport, etc.)Filtro del sistema de archivos(antivirus, cifrado, Procmon, etc.)NTFSconvierte el desplazamiento dentro del archivo en una posición del volumenEl Administrador de E/S arma el IRP(IRP_MJ_READ + ubicación de pila)Dispositivo de disco

Figura 4: el camino que recorre una solicitud de lectura. El lado del sistema de archivos y el lado del almacenamiento son pilas de dispositivos separadas, y NTFS encarga el trabajo emitiendo un nuevo IRP subordinado dirigido a la pila de almacenamiento

De este diagrama hay dos cosas que conviene recordar.

  1. El filtro es un habitante legítimo. Que un antivirus pueda inspeccionar toda la E/S de archivos no es un truco: este mecanismo de «insertarse en medio» es un punto de extensión oficial del sistema operativo.19 Process Monitor se sitúa en el mismo lugar y registra toda la E/S. Es también el primer sospechoso al investigar por qué «el acceso a archivos solo es lento en ese entorno» (los detalles, en la sexta entrega).
  2. El significado de la solicitud se traduce en cada capa. La aplicación dice «8 KB a partir del desplazamiento 4096 de este archivo»; NTFS lo traduce a «este clúster de este volumen», y la pila de almacenamiento, a «este sector de este disco». Las capas superiores no conocen los asuntos internos de las inferiores. Aquí hay una precisión importante: un mismo IRP no llega de la aplicación al disco tal cual, sin cambios. El lado del sistema de archivos y el del almacenamiento son pilas separadas, y NTFS, como resultado de procesar el IRP dirigido al archivo, crea y emite un nuevo IRP subordinado dirigido al volumen (la pila de almacenamiento). Si el archivo está fragmentado, una sola lectura puede dividirse en varios IRP subordinados, y tanto la granularidad como la vida útil de la solicitud cambian de capa en capa.

4.3. Las tres opciones de cada controlador

Lo que puede hacer un controlador que recibe un IRP se reduce, en esencia, a tres opciones.310

la capa inferior lo completó en el actola capa inferior lo dejó pendiente(lo pendiente se propaga hasta el llamador)El controlador recibe el IRP¿Cómo tratar esta solicitud?(1) Completarlo por sí mismollama a IoCompleteRequestej.: responde de inmediato con datos en caché(2) Pasarlo al dispositivo inferiorllama a IoCallDriverej.: un filtro lo inspecciona y lo deja pasar(3) Dejarlo pendientedevuelve STATUS_PENDING y encola el IRPej.: espera de respuesta del hardwareMás tarde, a raíz de una interrupción u otro evento,se llama a IoCompleteRequestEl procesamiento de finalización sube la pila en orden inverso(se invoca la rutina de finalización de cada capa)

Figura 5: las tres opciones de un controlador que recibe un IRP. No son excluyentes entre sí; el camino más habitual es «pasarlo hacia abajo y que quede pendiente allí», y todas terminan, al final, en una finalización mediante IoCompleteRequest

Cabe aclarar que estas tres opciones no son mutuamente excluyentes. El camino más común es que, al pasarlo hacia abajo con la opción (2), alguna capa más adelante elija la opción (3) y lo deje pendiente; en ese caso, STATUS_PENDING se propaga tal cual tanto a los controladores intermedios como al llamador original. Cuando la capa inferior lo completa más tarde, cada capa que había registrado una rutina de finalización al pasarlo hacia abajo es invocada de nuevo en orden inverso; es decir, «pasar» y «dejar pendiente» ocurren superpuestos sobre un mismo y único IRP.

Esta triple opción es la fuente de la flexibilidad de la E/S de Windows.

  • Si hay un acierto de caché, se completa de inmediato y resulta rápido (el Administrador de caché, en la cuarta entrega).
  • Se pueden insertar tantos filtros como se quiera que dejen pasar la solicitud (los minifiltros, en la sexta entrega).
  • Como se puede dejar pendiente, no hace falta bloquear un subproceso mientras se espera a un dispositivo lento (la E/S asíncrona, en la segunda y la tercera entrega).

En realidad, todo el resto de la serie no es más que una nota al pie de este diagrama.

5. Siguiendo un viaje completo de ReadFile

Con los protagonistas y las herramientas ya presentados, sigamos de principio a fin un único viaje de ReadFile. Es el caso en que se falla la caché y hay que llegar hasta el disco (el caso en que se acierta en la caché se tratará en la cuarta entrega).

Dispositivo de discoPila de almacenamientoFiltro + NTFSAdministrador de E/SSubproceso de la aplicaciónDispositivo de discoPila de almacenamientoFiltro + NTFSAdministrador de E/SSubproceso de la aplicaciónResuelve el objeto archivo a partir del identificadory arma el IRP (IRP_MJ_READ)E/S síncrona: aquí se duerme esperando la finalizaciónE/S asíncrona: el control vuelve y puede hacer otro trabajoDesde la rutina de interrupción (ISR)el procesamiento de finalización continúa en un DPCCuando todos los IRP subordinadosnecesarios se completanEjecuta en orden inverso la rutina de finalización de cada capay confirma el resultado mediante un APC al subproceso solicitanteReadFile → NtReadFile (llamada al sistema)IoCallDriver (hacia la cima de la pila)Traduce la posición al volumeny emite un IRP subordinado dirigido a la pila de almacenamientoEmite el comando de lecturaSTATUS_PENDING (el IRP subordinado queda pendiente)El IRP original también vuelve pendiente(aquí termina la ida)Interrupción — se terminó de leer los datosCompleta el IRP subordinado (IoCompleteRequest)la rutina de finalización del lado de NTFS lo recibeCompleta el IRP original (IRP_MJ_READ)Se confirman el estado y el número de bytes (mediante notificación de evento, etc.)

Figura 6: un viaje completo de ReadFile cuando falla la caché. La finalización del IRP subordinado y la del IRP original son pasos distintos, y la «ida» y la «vuelta» avanzan también como eventos separados

5.1. La ida y la vuelta son sucesos distintos

El punto clave de este diagrama es la línea de STATUS_PENDING. En el momento en que el controlador de almacenamiento emite el comando al hardware, suelta el control, y el procesamiento de «ida» termina ahí. Que los datos ya se han leído se notifica después mediante un evento distinto, una interrupción, y a partir de ahí comienza el procesamiento de finalización de la «vuelta».10

Es decir, dentro del núcleo la emisión y la finalización de la E/S están diseñadas para poder separarse. No existe una tubería aparte llamada «E/S síncrona»: el significado exacto de la E/S síncrona es la garantía de que la llamada no retorna antes de completarse. La espera solo ocurre cuando la solicitud queda pendiente; si el controlador puede completar la solicitud en el acto (el camino de «completar» de la figura 5), incluso con E/S síncrona el subproceso vuelve con el resultado sin dormirse ni una sola vez. Que Win32 alterne entre síncrono y asíncrono según la forma de abrir el identificador (FILE_FLAG_OVERLAPPED) se debe justamente a que lo asíncrono no es una «función adicional especial», sino una diferencia en la forma de esperar.11

Con esta perspectiva, las preguntas que se tratarán en la segunda entrega —por qué lo asíncrono o no se decide al abrir el identificador y no en cada llamada (aunque la propia estructura OVERLAPPED sí es necesaria para cada operación en curso), y qué significa que «algo que debería ser asíncrono se complete de forma síncrona»— se revelan como una consecuencia inevitable del mecanismo. La razón por la que, con async/await de .NET, la espera de E/S no consume un subproceso (tema que se trató desde el lado práctico en «Tabla de decisiones prácticas de async/await en C#») también se fundamenta en este diagrama.

5.2. También hay una excepción — el atajo que no crea un IRP

Para ser honestos, hay que añadir que no toda E/S se convierte en un IRP. Para la lectura y escritura síncronas de un archivo que está en caché, el sistema de archivos ofrece un atajo llamado E/S rápida (fast I/O), que «copia directamente desde la caché sin llegar a armar un IRP». Son las líneas que en la columna Operation de Procmon empiezan por FASTIO_. Las condiciones en las que no se puede usar este atajo y su relación con el Administrador de caché se tratarán en la cuarta entrega.

6. Detrás de CloseHandle — cleanup y close son cosas distintas

Por último, cómo se cierra lo que se abrió. Este punto está directamente relacionado con la investigación de fugas de identificadores y con el problema del «archivo en uso».

Lo que hace CloseHandle es eliminar una entrada de la tabla de identificadores del proceso. El objeto archivo tiene un contador de identificadores (handle count, el número de identificadores) y un contador de referencias (reference count, el número de referencias desde dentro del núcleo), y ambos disminuyen por separado.

Sistema de archivosAdministrador de E/SAdministrador de objetosAplicaciónSistema de archivosAdministrador de E/SAdministrador de objetosAplicaciónElimina la entrada de la tabla de identificadoresy reduce el contador de identificadoresCancela la E/S sin completary libera los bloqueos de ese objeto archivoalt[Era el último identificador]Pero si quedan referencias dentro del núcleo,como E/S sin completar o secciones (mapeo en memoria),el objeto archivo sigue vivoTerminan los últimos trámites del objeto archivosolo aquí se ha 'terminado de cerrar'alt[El contador de referencias también llegó a cero]CloseHandle(h)IRP_MJ_CLEANUPIRP_MJ_CLOSE

Figura 7: las dos etapas — cleanup (se cerró el último identificador) y close (desaparecieron también todas las referencias)

  • IRP_MJ_CLEANUP es la notificación de que «se cerró el último identificador». Sin embargo, la propia documentación aclara que, si quedan operaciones de E/S sin completar, es posible que el objeto archivo todavía no se libere.12
  • IRP_MJ_CLOSE es la notificación de que «el contador de referencias llegó a cero». Entre cleanup y close puede haber un intervalo; no siempre uno sigue inmediatamente al otro.13

Conocer estas dos etapas permite explicar varios misterios del día a día.

  • El archivo no se libera aunque se cierre un archivo mapeado en memoria. Esto ocurre porque la sección mapeada sigue manteniendo una referencia al objeto archivo, y el close no llega hasta que se desasigna la vista y se cierra la sección. La práctica en torno a la memoria compartida se trató en «Trampas de la memoria compartida y buenas prácticas en la práctica».
  • Buscar al culpable de un «archivo en uso» no termina solo con los identificadores. Aunque se hayan cerrado todos los identificadores, puede haber referencias dentro del núcleo (como imágenes mapeadas) reteniendo el archivo. Precisamente por eso, la búsqueda de Process Explorer abarca tanto los identificadores como las DLL (archivos mapeados).
  • Tampoco conviene confiar en que SafeFileHandle o el finalizador de .NET «lo cerrarán eventualmente», porque un identificador que se olvidó cerrar retrasa el cleanup y prolonga las infracciones de uso compartido o la retención de bloqueos.

7. Compruébelo con sus propios ojos

Todo lo visto hasta aquí se puede observar con un único PC con Windows en el que se disponga de privilegios de administrador.

Solo hacen falta tres cosas.

  • Privilegios de administrador. Process Monitor necesita ejecutarse como administrador porque carga un controlador del núcleo. En WinObj también hay objetos que no se ven sin permisos de administrador.
  • Las herramientas de Sysinternals. Microsoft distribuye WinObj y Process Monitor de forma gratuita. Se pueden obtener desde sus páginas de descarga individuales (WinObj, Process Monitor), o bien instalarlas todas juntas con el Sysinternals Suite. No requieren instalador: basta con descomprimir y ejecutar el exe.
  • Alguna operación sencilla para observar. Basta con guardar un archivo desde el Bloc de notas o copiar un archivo pequeño. Pruébelo en su disco local, no en una carpeta compartida de trabajo ni en una máquina de producción.

Ver el espacio de nombres con WinObj. Al iniciar WinObj de Sysinternals y abrir el directorio GLOBAL??, se muestra directamente que C: es un vínculo simbólico hacia \Device\HarddiskVolumeN (figura 1). Bajo \Device se pueden ver los nombres reales de los objetos dispositivo que crean los controladores.14

Leer el vocabulario del IRP en Procmon. Al activar Filter > Enable Advanced Output en el menú de Process Monitor, la columna Operation deja de mostrar ReadFile y pasa a mostrar el vocabulario del lado del núcleo: IRP_MJ_READ, FASTIO_READ, etc. Con solo seguir la copia de un archivo, aparece de forma literal el flujo de este artículo: IRP_MJ_CREATEFASTIO_READ/IRP_MJ_READIRP_MJ_WRITEIRP_MJ_CLEANUPIRP_MJ_CLOSE. El uso práctico de Procmon está recopilado en «Guía práctica de Process Monitor (ProcMon)».

Tener presente el fondo desde .NET. El FileStream de C# llama internamente a CreateFileW y conserva el identificador como SafeFileHandle. El parámetro FileOptions del constructor se traslada de forma casi directa a las marcas de Win32.

.NET (FileOptions) Win32 (marca de CreateFile) Significado (entrega relacionada)
Asynchronous FILE_FLAG_OVERLAPPED Abre el identificador para E/S asíncrona (segunda y tercera entrega)
WriteThrough FILE_FLAG_WRITE_THROUGH La escritura no se detiene en la caché (cuarta entrega)
SequentialScan FILE_FLAG_SEQUENTIAL_SCAN Sugerencia de lectura anticipada (cuarta entrega)
RandomAccess FILE_FLAG_RANDOM_ACCESS Sugerencia para suprimir la lectura anticipada (cuarta entrega)
DeleteOnClose FILE_FLAG_DELETE_ON_CLOSE Elimina el archivo al cerrarse el último identificador (aplicación del mecanismo del capítulo 6)

A partir de .NET 6, con File.OpenHandle y la clase RandomAccess también es posible escribir código sin pasar por la abstracción de FileStream, más cercano a la forma original de Win32 de «identificador + E/S con desplazamiento explícito». Si se entiende el significado de la columna derecha de esta tabla, la elección de la columna izquierda deja de ser memorización.

8. Resumen

  • La E/S de Windows está regida por un único diseño: determinar el destino (el objeto dispositivo) en el espacio de nombres del Administrador de objetos, empaquetar la solicitud en un IRP y hacerla fluir por la pila de dispositivos.2318
  • C: es un vínculo simbólico, \\.\ designa directamente el lugar de los nombres, y UNC se resuelve al redirector. La verdadera naturaleza de «todo se ve como un archivo» es la coherencia de la resolución de nombres.45
  • Los protagonistas son tres: el objeto driver (tabla de funciones de procesamiento), el objeto dispositivo (destino) y el objeto archivo (estado de una apertura). El HANDLE es una referencia al objeto archivo.6789
  • El controlador tiene tres opciones: completar, dejar pasar o dejar pendiente. Tanto la intercepción de un filtro como la respuesta inmediata de la caché o la E/S asíncrona son aplicaciones de esta triple opción.310
  • La emisión y la finalización están diseñadas para poder separarse, y la E/S síncrona es la garantía de que «no se retorna antes de completarse». En una solicitud que queda pendiente, la ida (emisión) y la vuelta (interrupción → finalización) avanzan como eventos distintos.1110
  • CloseHandle solo «devuelve un identificador». Si se conocen las dos etapas, cleanup (el último identificador) y close (la última referencia), el «cerrado pero en uso» deja de ser un misterio.1213

La continuación es la segunda entrega, «E/S síncrona y asíncrona — el verdadero significado de OVERLAPPED». Ahí se profundizará en el mecanismo que permite usar desde el lado de la aplicación la «separación de emisión y finalización» vista en este artículo: FILE_FLAG_OVERLAPPED, los cuatro métodos de notificación de finalización, la cancelación y la trampa de «algo que debería ser asíncrono pero vuelve de forma síncrona».

Artículos relacionados

Áreas de consultoría relacionadas

KomuraSoft LLC se ocupa del diseño y la investigación de fallos en torno a la E/S de archivos de aplicaciones empresariales de Windows (fugas de identificadores, «archivo en uso», lentitud de E/S en entornos específicos, entre otros).

Referencias

  1. Microsoft Learn, Windows Internals - Sysinternals. Página de presentación del libro Windows Internals. Obra de referencia sobre la arquitectura del núcleo de Windows y su estructura interna, incluido el sistema de E/S, y punto de partida para profundizar aún más en el contenido de esta serie. 

  2. Microsoft Learn, I/O manager. Sobre cómo el Administrador de E/S en modo núcleo de Windows gestiona la comunicación entre las aplicaciones y las interfaces que ofrecen los controladores de dispositivo; sobre que, como los dispositivos funcionan a una velocidad distinta de la del sistema operativo, la comunicación entre el sistema operativo y los controladores se realiza principalmente mediante IRP (paquetes de solicitud de E/S); y sobre que los IRP se transmiten del sistema operativo al controlador, y de un controlador a otro, de forma similar a los paquetes de red o los mensajes de Windows.  2 3

  3. Microsoft Learn, I/O request packets. Sobre que la mayoría de las solicitudes enviadas a un controlador de dispositivo se empaquetan en un IRP; sobre que los componentes del sistema operativo y los controladores envían el IRP al controlador mediante IoCallDriver (que toma un puntero al objeto dispositivo y un puntero al IRP); sobre que el IRP normalmente lo procesan varios controladores apilados como una pila de dispositivos, y se envía primero al objeto dispositivo situado en la cima de la pila; y sobre que cada controlador puede elegir entre procesar y completar el IRP o transferirlo al controlador inferior.  2 3 4 5 6

  4. Microsoft Learn, Introduction to MS-DOS device names. Sobre que un nombre de dispositivo MS-DOS es un vínculo simbólico hacia un nombre de dispositivo de estilo NT; sobre que las aplicaciones de Windows en modo usuario acceden a los dispositivos mediante nombres de dispositivo MS-DOS (letras de unidad, nombres de puerto COM), mientras que los controladores y el núcleo usan nombres de estilo NT; y sobre que el controlador crea el vínculo simbólico desde \DosDevices\nombre hacia el dispositivo mediante IoCreateSymbolicLink.  2 3

  5. Microsoft Learn, Naming files, paths, and namespaces. Sobre que CON, PRN, AUX, NUL, COM1–COM9 y LPT1–LPT9 están reservados como nombres de archivo; sobre que el espacio de nombres de Win32 se divide en un «espacio de nombres de archivos» y un «espacio de nombres de dispositivos», y sobre que el prefijo “\." significa acceder al espacio de nombres de dispositivos de Win32 (por ejemplo, \.\PhysicalDrive0); y sobre las reglas de resolución de estos nombres.  2 3 4

  6. Microsoft Learn, Introduction to driver objects. Sobre que el Administrador de E/S crea una estructura DRIVER_OBJECT al cargar el controlador; sobre que el objeto driver mantiene la entrada al conjunto de rutinas estándar del controlador (incluido el arreglo MajorFunction, la tabla de despacho); y sobre que el Administrador de E/S usa esta tabla para invocar la función de procesamiento correspondiente a cada solicitud.  2 3

  7. Microsoft Learn, Introduction to device objects. Sobre que la estructura DEVICE_OBJECT representa un dispositivo lógico, virtual o físico y es el destinatario (target) de una solicitud de E/S; sobre que el controlador crea el objeto dispositivo con IoCreateDevice; y sobre que el objeto dispositivo está vinculado al controlador (objeto driver) que lo creó.  2 3

  8. Microsoft Learn, Using files in a driver. Sobre que, en el núcleo, el objeto archivo representa «una instancia de un archivo (o dispositivo) abierto»; y sobre que se crea un objeto archivo cada vez que se abre un archivo, y este mantiene el contexto de esa apertura (como el desplazamiento de bytes actual).  2 3

  9. Microsoft Learn, File handles. Sobre que el identificador de archivo devuelto por CreateFile es específico del proceso y está vinculado a un objeto archivo abierto; sobre que abrir el mismo archivo varias veces produce cada vez un identificador (y un estado de apertura) distinto; y sobre que un identificador que ya no se necesita debe cerrarse con CloseHandle.  2 3

  10. Microsoft Learn, Completing IRPs. Sobre que es la llamada a IoCompleteRequest la que completa la operación de E/S; sobre que, al completarse, se invocan en orden las rutinas IoCompletion registradas por los controladores de capas superiores; y sobre el flujo por el cual la finalización de la solicitud ocurre en un momento distinto de la emisión, hasta que finalmente se devuelve el estado al solicitante.  2 3 4 5

  11. Microsoft Learn, Synchronous and asynchronous I/O. Sobre que, en la E/S síncrona, la función no retorna hasta que la E/S se completa y el subproceso queda a la espera, mientras que en la E/S asíncrona (E/S superpuesta) la función que emite la solicitud retorna de inmediato y el subproceso puede continuar con otro trabajo; sobre que la E/S asíncrona requiere abrir el identificador especificando FILE_FLAG_OVERLAPPED; y sobre los distintos métodos disponibles para recibir la notificación de finalización.  2 3

  12. Microsoft Learn, IRP_MJ_CLEANUP. Sobre que la recepción de esta solicitud indica que «se cerró el último identificador del objeto archivo asociado al objeto dispositivo de destino»; sobre que, sin embargo, es posible que el objeto archivo aún no se libere debido a solicitudes de E/S sin procesar; y sobre que este IRP se envía en el contexto del proceso que cerró el identificador.  2 3

  13. Microsoft Learn, IRP_MJ_CLOSE. Sobre que la recepción de esta solicitud indica que «el contador de referencias del objeto archivo llegó a cero y el objeto archivo está a punto de liberarse»; y sobre que se envía después de la solicitud de cleanup, aunque no necesariamente de inmediato, ya que se espera a que se completen las operaciones de E/S sin procesar.  2 3

  14. Microsoft Learn, WinObj - Sysinternals. Sobre que WinObj es una herramienta que muestra el espacio de nombres del Administrador de objetos NT, y permite examinar los objetos del espacio de nombres, incluidos los objetos dispositivo y los vínculos simbólicos.  2 3

  15. Microsoft Learn, CreateFileW function. Sobre que CreateFile puede abrir y devolver un identificador no solo para archivos, sino también para dispositivos como discos físicos, volúmenes, la consola, puertos de comunicación (puertos COM) o canalizaciones (pipes); sobre el uso de nombres con el formato “\." al abrir un dispositivo; y sobre el significado de las distintas marcas, incluida FILE_FLAG_OVERLAPPED.  2

  16. Microsoft Learn, IRP major function codes. Sobre la lista de códigos de función mayor del IRP (IRP_MJ_CREATE, IRP_MJ_READ, IRP_MJ_WRITE, IRP_MJ_CLEANUP, IRP_MJ_CLOSE, IRP_MJ_DEVICE_CONTROL, IRP_MJ_PNP, entre otros) y sobre qué significa cada solicitud y qué controlador debe atenderla. 

  17. Microsoft Learn, I/O stack locations. Sobre que el Administrador de E/S prepara, dentro del IRP, una ubicación de pila de E/S por cada controlador de la cadena de controladores en capas; sobre que cada ubicación de pila contiene el código de función mayor/menor y los parámetros de esa solicitud; y sobre que cada controlador usa IoGetCurrentIrpStackLocation para obtener su propia ubicación de pila y conocer el contenido de la solicitud. 

  18. Microsoft Learn, Device nodes and device stacks. Sobre que los objetos dispositivo se apilan para formar la pila de dispositivos; sobre que el IRP se envía primero al objeto dispositivo en la cima de la pila, y en cada capa se procesa o se transfiere a la capa inferior; y sobre que el objeto dispositivo de un controlador de filtro existe insertado dentro de la pila.  2

  19. Microsoft Learn, Filter Manager Concepts. Sobre que el Administrador de filtros es un controlador en modo núcleo incluido con Windows; sobre que los minifiltros pueden interceptar las solicitudes de E/S al sistema de archivos mediante retrollamadas previas (pre) y posteriores (post); y sobre que la posición de intercepción de cada minifiltro (su altitud) determina su orden dentro de la pila de E/S. 

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Qué es un IRP?
El IRP (I/O Request Packet, paquete de solicitud de E/S) es el paquete que el Administrador de E/S del núcleo de Windows utiliza para empaquetar las solicitudes de lectura y escritura de las aplicaciones, entre otras, y entregarlas a los controladores de dispositivo. La documentación de Microsoft para desarrollo de controladores explica que la mayoría de las solicitudes enviadas a un controlador de dispositivo se empaquetan como un IRP. El IRP contiene un código de función mayor que indica el tipo de solicitud (creación, lectura, escritura, limpieza, etc.) y una ubicación de pila por cada controlador por el que pasa, y se procesa mientras se transmite de arriba hacia abajo por la pila de dispositivos. Cada controlador elige entre completar el IRP por sí mismo, pasarlo al controlador inferior o dejarlo pendiente para completarlo más tarde. Los desarrolladores de aplicaciones nunca manipulan un IRP directamente, pero notaciones como IRP_MJ_READ, que aparecen en la columna Operation de Process Monitor, son precisamente este mecanismo.
¿Por qué en Windows se pueden abrir archivos, puertos serie e impresoras con la misma función CreateFile?
Porque todo nombre pasado a CreateFile sigue el mismo camino: se resuelve, en última instancia, a un objeto de dispositivo dentro del espacio de nombres del Administrador de objetos, y el Administrador de E/S crea un objeto de archivo asociado a ese dispositivo y devuelve un identificador (handle). La letra de unidad C:, por ejemplo, es en realidad un vínculo simbólico a un nombre de dispositivo NT como \Device\HarddiskVolume3, y una designación como \\.\COM1 se resuelve del mismo modo al objeto de dispositivo del puerto serie. Sea cual sea el dispositivo al que se resuelve, toda solicitud posterior llega al controlador empaquetada en el mismo formato, el IRP, de modo que tanto archivos como dispositivos pueden abrirse y leerse o escribirse con la misma API. Las rutas UNC siguen el mismo mecanismo: simplemente se resuelven al dispositivo del redirector de red. Este diseño de «espacio de nombres + paquete» es, en realidad, la clave de la coherencia de la E/S de Windows.
¿Por qué a veces un archivo no se libera de inmediato aunque se haya llamado a CloseHandle?
Porque lo que hace CloseHandle es «devolver un identificador», no «cerrar el archivo». El objeto de archivo dentro del núcleo mantiene dos contadores independientes: el número de identificadores abiertos (handle count) y el número de referencias desde componentes del núcleo (reference count). Cuando se cierra el último identificador, el sistema de archivos recibe un IRP_MJ_CLEANUP, pero mientras queden referencias vivas dentro del núcleo —por ejemplo, E/S sin completar o secciones de archivos mapeados en memoria—, el objeto de archivo en sí sigue existiendo, y solo cuando el contador de referencias llega a cero se envía el IRP_MJ_CLOSE. Buena parte de los fenómenos como no poder eliminar un archivo después de haberlo usado como archivo mapeado en memoria, o que la aplicación se cierre y aun así aparezca el mensaje de que «el archivo está en uso», se explican por este mecanismo en dos etapas.
¿De qué le sirve a un desarrollador de aplicaciones conocer el IRP y la pila de dispositivos?
Aunque nunca vaya a escribir un IRP directamente, este conocimiento ayuda tanto en la investigación como en el diseño. En primer lugar, la columna Operation de Process Monitor (con la salida detallada activada) muestra exactamente la terminología de los IRP, como IRP_MJ_CREATE o IRP_MJ_READ, así que conocer el vocabulario de esta capa permite leer sus registros. En segundo lugar, al saber que controladores de filtro como los antivirus se insertan en el camino de toda E/S de archivos, se puede orientar mejor la investigación de un problema como «el acceso a archivos solo es lento en cierto entorno». Además, al entender que la E/S de Windows está diseñada para separar la emisión de la finalización, y que la E/S síncrona no es más que la garantía de «no retornar antes de completarse», resulta natural comprender y usar con criterio la E/S asíncrona, los puertos de finalización de E/S y el comportamiento de async/await en .NET.

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.

Volver al blog