The Depths of Windows I/O (Part 1) — Every Read and Write Becomes an IRP: The Big Picture of the I/O System

· Updated: · · Windows, Win32, I/O, Kernel, Device Driver, .NET, C#, Bug Investigation

Revision history (first version, published Jul 29, 2026)
First published
Cite this article(DOI (registered archive): 10.5281/zenodo.22170804)

The DOIs below refer to previously archived versions and may not match the current text. Use this page’s URL to reference the current text.

Go Komura (2026). The Depths of Windows I/O (Part 1) — Every Read and Write Becomes an IRP: The Big Picture of the I/O System. KomuraSoft LLC. https://comcomponent.com/en/blog/windows-io-internals-architecture-irp/

DOI (registered archive)
10.5281/zenodo.22170804
DOI (last registered version)
10.5281/zenodo.22170805

A single line of File.ReadAllText, or one call to ReadFile. What happens inside Windows between the moment you call that function and the moment it returns?

Once you know this flow, you can think about IRP_MJ_READ and FASTIO_READ showing up in Process Monitor, a file that is not released even though you called CloseHandle, and behavior that changes only on a network drive or on a machine with antivirus software installed, all within the same mechanism.

The series “The Depths of Windows I/O” takes in the kernel design that a book such as Windows Internals (published in Japanese as Inside Windows) covers, and explains not only how to use an API but why it behaves the way it does.1 This first article pins down the foundation for the rest — name resolution, the cast of characters, and the flow of a request and its completion — with diagrams. It is not written so that you can write a driver; it is written so that application developers can understand where the API they called actually goes.

Prerequisites: you can read this if you have used FileStream in C#, or called CreateFile / ReadFile in C/C++. No driver development experience is required, and the kernel-side code is presented as a conceptual illustration. As a rough guide, budget about 25 minutes to read it through while looking at the diagrams, or 2–3 minutes if you only want the bottom line of one section.

The structure of the series

Part Topic
Part 1 (this article) The Big Picture of the I/O System — Every Read and Write Becomes an IRP
Part 2 Synchronous and Asynchronous I/O — What OVERLAPPED Really Means
Part 3 I/O Completion Ports (IOCP) and the .NET Thread Pool — The Basement Under async/await
Part 4 The Cache Manager — When Does Your WriteFile Actually Reach Disk?
Part 5 Inside NTFS — Understanding the File System Through the MFT
Part 6 Filter Drivers and Minifilters — Why Procmon and Antivirus Scanners Can Intercept I/O

1. The Bottom Line First

There are three points to grasp before anything else.

  1. Windows I/O decides the destination by name and hands the request over as a packet. What CreateFile opens is not necessarily a file on disk, and C: too is a symbolic link to an NT device name. Most requests sent to a device driver flow down the device stack as an IRP (I/O Request Packet). There is also a shortcut called Fast I/O that builds no IRP (Sections 2 and 4, and Section 5.2).2345
  2. Keep “the thing that handles the request”, “the destination”, and “the state of one open” separate in your head. The driver object holds a table of handler functions, the device object is the destination of a request, and the file object holds the state of one particular open. The HANDLE an application holds is a reference to a file object (Section 3).6789
  3. Issuing and completing a request, closing a handle, and releasing a reference are all separate stages. A driver combines completing an IRP, passing it down, and holding it pending. Synchronous I/O is a guarantee that the call does not return before completion; it is not a different I/O mechanism. Cleanup, which happens when the last handle closes, also has to be distinguished from close, which happens when the references are gone (Sections 4 through 6).310111213

How to read this by purpose

What you want to know Where to read
The big picture of what follows an API call Section 1 → Section 2 (name resolution) → Section 5 (one round trip of ReadFile)
The difference between the driver, device, and file objects and the IRP The mapping table in Section 3 → Section 4
Why handle leaks and “I closed it but it is still in use” happen Section 3.3 → Section 6
How to verify it on your own PC Section 7. Observe the namespace with WinObj and I/O with Process Monitor

In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (40 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle

2. What “Everything Looks Like a File” Really Means

2.1. Where Does the Name You Pass to CreateFile Go?

The entry point for opening a file or a device is a name. A local path such as C:\project\report.csv, a UNC path such as \\server\share\data.csv, and a device designation such as \\.\COM3 can all be passed to the same CreateFile.14

What supports that consistency is the namespace managed by the kernel’s Object Manager. Named devices, events, shared memory sections, and more are all handled in a tree-shaped namespace. Sysinternals’ WinObj lets you inspect that namespace directly.15

\ (root of the namespace)\Device(device objects created by drivers)\GLOBAL??(where the global names visible from Win32 live)\BaseNamedObjects(named mutexes and the like)HarddiskVolume3Serial0Mup (network redirector)C: → \Device\HarddiskVolume3COM1 → \Device\Serial0PhysicalDrive0 → \Device\Harddisk0\DR0

Figure 1: The Object Manager namespace (excerpt). What sits under \GLOBAL?? are symbolic links; the real objects live under \Device

In Figure 1, look at these two sides separately.

Side of the name Example Role
Names Win32 applications use Drive letters, COM port names The entry point from the application
Names the kernel uses NT device names such as \Device\HarddiskVolume3 Names that point at a device object

What connects the two is a symbolic link. The name an application sees and the name of the real object on the kernel side are not the same.

To make a device visible to Win32 applications, a driver uses IoCreateSymbolicLink to create a link from an MS-DOS device name such as \DosDevices\COM1 to an NT device name.4 C: works the same way: it is a link to a volume device such as \Device\HarddiskVolume3.

Name resolution for CreateFile("C:\project\report.csv") proceeds as follows.

The name the application passedC:\project\report.csvThe Win32 layer converts it to NT form\??\C:\project\report.csvThe Object Manager searches the namespaceand finds that \??\C: is a symbolic linkFollow the link and substitute\Device\HarddiskVolume3\project\report.csv\Device\HarddiskVolume3 reachesthe volume's device objectFor the remaining \project\report.csv, the I/O managerissues IRP_MJ_CREATE and leaves resolutionto the file system driver (NTFS)

Figure 2: Name resolution in CreateFile. The first half is the Object Manager’s job; once the device is reached, the second half belongs to the file system

Separate the Name That Leads to the Device from the Name Inside the File System

In the first half of Figure 2, the Win32 path is converted into NT form, and following the link reaches the volume’s device object. Resolving what remains, \project\report.csv, is left to the file system driver (NTFS), to which the I/O manager issues IRP_MJ_CREATE.

Once you know where that boundary is, the following designations read the same way.

  • \\.\ is a designation into the Win32 device namespace. \\.\PhysicalDrive0 and \\.\COM10 specify the device name link directly, without going through a drive letter.514
  • CON and NUL are reserved MS-DOS device names. Even written inside a path they can resolve to the device side, so they cannot be used as ordinary file names.5 The practical implications are covered in “MAX_PATH and the Pitfalls of Windows Paths and File Names”.
  • With a UNC path, the device the name resolves to changes. \\server\share resolves to the network redirector’s device (\Device\Mup), and from there the SMB client carries the request across the network. Why behavior differs between local paths and UNC paths can be reasoned about from this difference in the resolution target (see “Pitfalls of Network Drives and UNC Paths”).

\?? and \GLOBAL?? Are Not the Same Thing

\?? is not merely another name for a directory that exists. It is the entry point of a search order: first look in the local DOS device map for the logon session, and if there is nothing there, look in \GLOBAL??. What Figure 1 draws is only the global side, \GLOBAL??.

Drive letters created with net use or subst go into the local side. That is why the drives you can see differ per logon session even on the same PC, and why a service sometimes cannot see a user’s network drives.

To sum up so far, files and devices can be handled through the same API because the name leads to a device object, and the requests that follow can be handed over in a common format. Next we sort out the objects that represent that destination and the state of an open.

3. The Cast Is Three Objects

First, let us line up what you see in everyday Win32 / .NET code and its counterpart on the kernel side. Whenever the terminology below gets confusing, come back to this table.

What you see on the Win32 / .NET side The counterpart on the kernel side In one phrase
HANDLE / SafeFileHandle An entry in the handle table (a reference to a file object) A ticket number that points at “one open” (3.3)
The open state a FileStream holds The file object The container for the share mode, flags, and current position (3.3)
The drive letter C: or \\.\COM3 The device object (the result of resolving the name) The destination of a request (Sections 2 and 3.2)
“The NTFS driver”, “the disk driver” The driver object A table of handler functions, one per kind of request (3.1)
One call to ReadFile / stream.Read Normally one IRP The request packet carried to the destination (Section 4). A synchronous read or write of a file that is in the cache is handled by a shortcut called Fast I/O, which builds no IRP (5.2). Those are the rows beginning with FASTIO_ in Procmon
FileOptions / CreateFile flags Attributes recorded on the file object They decide read-ahead and asynchronous behavior (the mapping table in Section 7)
The value from GetLastError / a .NET exception NTSTATUS (such as STATUS_PENDING) The completion status. It is translated into a Win32 error code on the way up

This table is there to help you understand the correspondence. It does not mean that exactly one and the same IRP always flows from ReadFile all the way to the disk. The path that builds no IRP is described in Section 5.2, and the boundary where a separate lower IRP is issued in Section 4.2.

3.1. The Driver Object — a Table of Handler Functions

When a driver is loaded, the I/O manager creates a driver object (DRIVER_OBJECT) that represents that driver.6

What application developers want to know about is the MajorFunction array. It is a table mapping “kind of request” to “handler function”, and the kind of request is called a major function code. The representative ones are IRP_MJ_CREATE (open), IRP_MJ_READ (read), IRP_MJ_WRITE (write), IRP_MJ_CLEANUP, and IRP_MJ_CLOSE.16

Expressing only the relationships in C# gives the following. This is not an implementation example but a conceptual illustration for understanding the C structure inside the kernel.

// Conceptual illustration. In reality this is a C structure inside the kernel
class DriverObject
{
    // IRP_MJ_XXX is the index. 28 kinds in total
    public DispatchRoutine[] MajorFunction = new DispatchRoutine[28];
}
// For ntfs.sys, MajorFunction[IRP_MJ_READ] holds "the NTFS read handler"

3.2. The Device Object — the Destination of a Request

A driver creates a device object (DEVICE_OBJECT) for each device it handles. This is the destination of an I/O request.7

It is not necessarily one to one with a physical device, though. A logical entity such as a volume (HarddiskVolume3), and a device a filter driver creates so that it can interpose, are both represented by this object.

A device object points at the driver object that created it. So the relationship is that once the destination is decided, the table of functions that will process the request is decided too.

3.3. The File Object — the State of “One Open”

Every time CreateFile succeeds, the kernel creates one file object. What it represents is not the file on disk itself but the state of one particular open of a file or device.8

Open the same file twice and you get two file objects. The current file pointer of a synchronous handle, and the share mode and flags given at open time, each belong to their own open state. The HANDLE an application receives is a reference to that file object, reached through the per-process handle table.9

Kernel spaceProcess (user mode)File object 1report.csv opened for readingcurrent offset 4096File object 2report.csv opened for appendcurrent offset 65536Device objectequivalent to HarddiskVolume3Driver object NTFSMajorFunction = the table of handler functionsHANDLE 0x1A4HANDLE 0x1B8

Figure 3: The relationship between the three objects. A handle points at a file object through the handle table, the file object leads to the device, and the device leads to the driver

“Opening the Same File Twice” and “Duplicating a Handle” Are Different

Operation File object File pointer
Opening the same file separately One is created per open They are independent
Duplicating a handle with DuplicateHandle They reference the same object It is shared

Even for handles that point at the same file, what state they share depends on whether the file was opened again or the handle was duplicated.

Handle Leaks and Sharing Violations Also Follow from the Open State

When a file handle leaks, the file object and the resources beyond it stay referenced. What you count during an investigation is entries in the handle table. The information Process Explorer and handle.exe show corresponds to that (see “Process Explorer / Handle / VMMap in Practice” and the investigation write-up “Investigating a Long-Run Crash in an Industrial Camera - Handle Leak Edition”).

A sharing violation is decided by matching the share mode of an existing open state against the request of a new CreateFile. The practical side of exclusion control is covered in “The Basics of Exclusion Control for File Integration”.

4. The IRP — an I/O Request Becomes a Parcel

4.1. Why Package It as a Packet?

The I/O manager packs requests such as open, read, and write into an IRP (I/O Request Packet) and hands it to a driver. Most requests to a device driver take this form.3

The reason for using a packet is to make it possible to decouple issuing a request from completing it. Devices such as disks do not run at the same speed as the CPU. If a request can be held as an independent packet, it can be completed at some later moment, after it was issued.2

The inside of an IRP is also divided into information about the request as a whole and instructions for each driver.17

Area What it holds
Header Information about the request as a whole
I/O stack locations The kind of request and the parameters for each driver the request passes through

The I/O stack locations are laid out according to the number of drivers the request is expected to pass through. Each driver reads its own area and works out which operation is being asked of it.

4.2. Going Down the Device Stack

A set of stacked device objects is called a device stack.18 When reading a file on a local disk, the request follows roughly the following route.

The storage side stackThe file system side stackVolume and partition management(volmgr and so on)Disk class driver(disk.sys)Storage port and miniport(storport and so on)File system filters(antivirus, encryption, Procmon, and so on)NTFStranslates an offset within the file into a position on the volumeThe I/O manager builds the IRP(IRP_MJ_READ + stack locations)Disk device

Figure 4: The road a read request travels. The file system side and the storage side are separate device stacks, and NTFS issues a new lower IRP addressed to the storage side to get the work done

Filters Are an Extension Point the OS Provides

Antivirus software can inspect file I/O because it uses a mechanism the OS officially provides for interposing partway through a request.19 Process Monitor records I/O from this same position.

In an investigation into “file access is slow only on that machine”, the filters sitting on this route are the first candidates to check. Part 6 covers this in detail.

A Single IRP Does Not Pass All the Way Through to the Disk

The application asks for “8 KB from offset 4096 of this file”. NTFS maps that onto clusters on the volume, and the storage stack maps it onto sectors on the disk. An upper layer can make its request without knowing the details of the layer below.

What matters here is that the file system side and the storage side are separate stacks. To process an IRP addressed to a file, NTFS creates and issues a new lower IRP addressed to the volume. One and the same IRP is not handed straight through from the application to the disk.

With a fragmented file, a single read can even be split into several lower IRPs. The granularity and lifetime of a request have to be thought about separately for each layer.

4.3. Each Driver Has Three Choices

What a driver does with an IRP it receives can basically be sorted into the following three.310

Action Main behavior Example
Complete it itself Complete it with IoCompleteRequest Satisfy the request from a local cache
Pass it to the driver below Send it to the next device with IoCallDriver A filter forwards it after inspecting it
Hold it pending Return STATUS_PENDING and complete it later Wait for the hardware to respond
the layer below completed it on the spotthe layer below held it pending(pending propagates to the caller)A driver receives an IRPHow should this request be handled(1) Complete it itselfcall IoCompleteRequeste.g. answer at once with data in the cache(2) Pass it to the device belowcall IoCallDrivere.g. a filter inspects it and lets it through(3) Hold it pendingreturn STATUS_PENDING and queue the IRPe.g. waiting for the hardware to respondIoCompleteRequest later,triggered by an interrupt or the likeCompletion travels back up the stack in reverse order(each layer's completion routine is called)

Figure 5: The three choices a driver has when it receives an IRP. The three are not exclusive; the most ordinary road is “passed down and then held pending somewhere below”, and all of them end in completion through IoCompleteRequest

“Forwarding” and “Pending” Overlap in the Same Request

These three are not mutually exclusive options. The typical case is that an upper driver passes the request down and somewhere below it goes pending. In that case STATUS_PENDING travels back through the intervening drivers to the caller.

When a lower layer completes the request later, every layer that registered a completion routine when it forwarded the request is called back in reverse order. Read the act of passing a request down and the completion processing that returns the result upward as two separate things.

This combination is the foundation for the mechanisms covered in the rest of the series. If the cache can complete a request straight away it gets faster (Part 4), and filters can be stacked along the way (Part 6). Because pending and completion can be separated, asynchronous I/O lets a thread do other work while a slow device is being waited on (Parts 2 and 3).

5. Following One Round Trip of ReadFile

Here we follow a ReadFile in the case where it misses the cache and has to go to the disk. In the diagram, look first at the outbound leg that issues the request, and then at the return leg after the data has been read.

Disk deviceStorage stackFilters + NTFSI/O managerApplication threadDisk deviceStorage stackFilters + NTFSI/O managerApplication threadResolve the file object from the handleand build the IRP (IRP_MJ_READ)Synchronous I/O sleeps here waiting for completionAsynchronous I/O returns control and can do other workFrom the interrupt service routine (ISR),completion processing continues in a DPCOnce every requiredlower IRP has completedRun each layer's completion routine in reverse orderand finalize the result with an APC to the requesting threadReadFile → NtReadFile (system call)IoCallDriver (to the top of the stack)Translate the position onto the volume and issuea lower IRP addressed to the storage stackIssue the read commandSTATUS_PENDING (the lower IRP is pending)The original IRP also returns still pending(the outbound leg ends here)Interrupt, "the data has been read"Complete the lower IRP (IoCompleteRequest)the completion routine on the NTFS side receives itComplete the original IRP (IRP_MJ_READ)The status and byte count are final (event signaling and so on)

Figure 6: One round trip of a ReadFile that missed the cache. Completing the lower IRP and completing the original IRP are separate steps, and the outbound and return legs also proceed as separate events

5.1. The Outbound Leg and the Return Leg Are Separate Events

The dividing line in Figure 6 is STATUS_PENDING. After issuing a command to the hardware, the storage driver holds the request pending and returns. The outbound leg ends there, and the completion processing of the return leg proceeds later, triggered by an interrupt or the like.10

Stage What happens
Issue Find the destination from the handle and send the original IRP and any lower IRPs required
Pending While the hardware has not finished, hold the request as not yet completed
Lower completion After the read finishes, the lower IRP on the storage side completes
Completion of the original request Once every required lower IRP has completed, the original IRP completes too, and the status and byte count become final

The Difference Between Synchronous and Asynchronous Is When the Caller Returns

There is no separate mechanism inside the kernel dedicated to synchronous I/O. Synchronous I/O is the guarantee that “the call does not return before completion”. The wait for completion described here occurs when a request goes pending; a request that can be completed on the spot can return its result without waiting for a later completion.11

In Win32, you choose synchronous or asynchronous handling with FILE_FLAG_OVERLAPPED when you open the handle. Note, however, that the OVERLAPPED structure itself is needed once per operation in flight. Keep “the setting on the handle” and “the state of an individual operation” as separate ideas.

Once you see this difference, you can carry “why asynchronous or not is decided when the handle is opened” and “what it means for something that is supposed to be asynchronous to complete on the spot” into Part 2. The reason .NET’s async/await does not have to keep a thread occupied just to wait for I/O also lies in this separation of issue and completion (see “A Practical Decision Table for C# async/await”).

5.2. There Are Exceptions — the Shortcut That Builds No IRP

Not all I/O becomes an IRP. When file data that is in the cache is read or written synchronously, there is a shortcut called Fast I/O that copies directly to and from the cache without building an IRP.

When advanced output is enabled in Procmon, the rows in the Operation column beginning with FASTIO_ correspond to that path. The conditions under which the shortcut cannot be used, and its relationship with the cache manager, are covered in Part 4.

So in an investigation you consider both “the basic path that uses an IRP” and “the shortcut that builds no IRP”. You do not conclude that an IRP was definitely created just because ReadFile was called.

6. Behind CloseHandle — Cleanup and Close Are Different Things

Closing what you opened also has to be split into stages. What CloseHandle does is remove one entry from the process’s handle table.

A file object has a handle count, which is the number of handles, and a reference count, which tracks references inside the kernel. The moment the handles are gone and the moment every reference to the object is gone are not necessarily the same.

File systemI/O managerObject ManagerApplicationFile systemI/O managerObject ManagerApplicationRemove the entry from the handle tableand decrement the handle countCancel outstanding I/O and release locksfor that file objectalt[That was the last handle]But if references inside the kernel remain, such asoutstanding I/O or a section (memory mapping),the file object is still aliveThe final tidying up of the file object is doneonly now is it truly closedalt[The reference count also reached zero]CloseHandle(h)IRP_MJ_CLEANUPIRP_MJ_CLOSE

Figure 7: The two stages of cleanup (the last handle was closed) and close (every reference is gone too)

6.1. Distinguish the Last Handle from the Last Reference

Notification What happened What may still remain
IRP_MJ_CLEANUP The last handle on that file object was closed References from outstanding I/O and the like
IRP_MJ_CLOSE The reference count of the file object reached zero The file object is at the stage of being released

The documentation for IRP_MJ_CLEANUP states explicitly that if outstanding I/O requests remain, the file object may not be released yet.12 IRP_MJ_CLOSE is sent afterwards, but not necessarily immediately after cleanup.13

6.2. When Investigating “In Use”, Look Beyond the Handle

A memory mapping has a lifetime of its own, separate from the file handle. As long as the mapped section holds a reference to the file object, closing the original handle does not release it. Check that the view has been unmapped and the section closed as well (see “Pitfalls of Shared Memory and Practical Best Practices”).

Not finding a handle does not mean the investigation is over. A reference inside the kernel, such as a mapped image, may be holding the file. That is why Process Explorer’s search covers not only handles but also DLLs (mapped files).

In .NET too, being closed eventually and being closed at the moment you need it closed are different things. If you count on SafeFileHandle or a finalizer to close things at some point, a handle you forgot to close delays cleanup and prolongs sharing violations and held locks.

7. See It with Your Own Eyes

The namespace and the flow of I/O can be observed on a Windows PC where you have administrator rights. Start with a safe local file and follow the open, read, write, and close operations.

7.1. Get Ready for Observation

What you need Preparation and cautions
Administrator rights Process Monitor loads a kernel driver, so run it as administrator. With WinObj too, some objects are invisible unless you are an administrator
The Sysinternals tools Use WinObj and Process Monitor, distributed free of charge by Microsoft. You can also get them together in the Sysinternals Suite. You can extract the archive and run the exe; no installer is needed
An operation to observe Saving one file in Notepad, or copying one small file, is enough. Do not use a shared business folder or a production machine; try it on the local disk in front of you

Open the GLOBAL?? directory in WinObj and you can confirm that C: is a symbolic link to \Device\HarddiskVolumeN. Then look under \Device and you will see the real names of the device objects the drivers created. This is the procedure for checking the names and links in Figure 1 against a real PC.15

7.3. Use Procmon to Tell IRPs and Fast I/O Apart

Enable Filter > Enable Advanced Output in Process Monitor and the Operation column changes from displays such as ReadFile to the kernel-side vocabulary of IRP_MJ_READ and FASTIO_READ.

In a file copy, look for the operations described in this article: IRP_MJ_CREATEFASTIO_READ / IRP_MJ_READIRP_MJ_WRITEIRP_MJ_CLEANUPIRP_MJ_CLOSE. Follow them with the read path and the stages from cleanup to close in mind, and the trace stops being a mere list of operation names.

The practical use of Procmon is collected in “A Practical Guide to Process Monitor (ProcMon)”.

7.4. Map .NET Options onto Win32 Flags

On Windows, C#’s FileStream internally opens a handle with CreateFileW and holds it as a SafeFileHandle. The FileOptions given to the constructor map onto Win32 flags as follows.

.NET (FileOptions) Win32 (CreateFile flag) Meaning (related part)
Asynchronous FILE_FLAG_OVERLAPPED Open the handle for asynchronous I/O (Parts 2 and 3)
WriteThrough FILE_FLAG_WRITE_THROUGH Do not let the cache hold up the write (Part 4)
SequentialScan FILE_FLAG_SEQUENTIAL_SCAN A hint for read-ahead (Part 4)
RandomAccess FILE_FLAG_RANDOM_ACCESS A hint to suppress read-ahead (Part 4)
DeleteOnClose FILE_FLAG_DELETE_ON_CLOSE Delete once the last handle is closed (an application of the mechanism in Section 6)

On .NET 6 or later you can also use File.OpenHandle and the RandomAccess class to work in the form of “a handle plus offset-specified I/O”, without a FileStream in between. Understanding the right-hand side of this table lets you choose the options on the left by the behavior you want.

8. Summary

Windows I/O can be sorted out by following it in this order: name resolution → the open state → issuing the request → completion → releasing the reference.

  • Resolve the name and decide the destination. C: is a symbolic link, \\.\ is a designation into the Win32 device namespace, and UNC resolves to the redirector. The foundation that lets files and devices be handled through the same API is this consistency of name resolution.45
  • Keep the three objects separate. The driver is a table of handler functions, the device is the destination, and the file object is the state of one particular open. A HANDLE references that file object.6789
  • An IRP carries the request, and completion can proceed at a different moment. Most requests are sent down the device stack as IRPs, and each driver combines completing, forwarding, and holding pending. The lifetime of a lower IRP and that of the original IRP are separate, and with Fast I/O there is also a path that builds no IRP.231810
  • Think about synchronous and asynchronous I/O in terms of the relationship between issue and completion. Synchronous I/O is the guarantee of not returning before completion. For a request that went pending, the issue and the completion that proceeds later, triggered by an interrupt or the like, are separate events.1110
  • Returning a handle is not the same as releasing the object. Cleanup is the stage where the last handle is gone, and close the stage where the last reference is gone. That distinction helps when investigating “I closed it but it is still in use”.1213

Part 2 continues with “Synchronous and Asynchronous I/O — What OVERLAPPED Really Means”. It covers FILE_FLAG_OVERLAPPED, which is how an application uses this separation of issue and completion, the four ways of receiving completion notifications, cancellation, and the conditions under which “something that should be asynchronous comes back synchronously”.

KomuraSoft LLC works on the design and the investigation of defects around file I/O in Windows business applications, including handle leaks, “the file is in use”, and I/O slowdowns in specific environments.

References

  1. Microsoft Learn, Windows Internals - Sysinternals. The page introducing the book Windows Internals (published in Japanese as Inside Windows). It is the standard work on the internals of Windows, including the kernel architecture and the I/O system, and a starting point for studying the subjects of this series in more depth. 

  2. Microsoft Learn, I/O manager. On how the Windows kernel-mode I/O manager manages communication between applications and the interfaces provided by device drivers, how communication between the OS and drivers is carried out primarily through IRPs (I/O request packets) because devices do not run at speeds that match the OS, and how an IRP is passed from the OS to a driver and from driver to driver in a way that resembles a network packet or a Windows message.  2 3

  3. Microsoft Learn, I/O request packets. On how most requests sent to a device driver are packed into an IRP, how OS components and drivers send an IRP to a driver with IoCallDriver (which takes a pointer to a device object and a pointer to an IRP), how an IRP is normally processed by several drivers stacked as a device stack and is first sent to the device object at the top of the stack, and how each driver can choose either to process and complete the IRP or to forward it to the driver below.  2 3 4 5

  4. Microsoft Learn, Introduction to MS-DOS device names. On how an MS-DOS device name is a symbolic link to an NT-style device name, how user-mode Windows applications access devices by MS-DOS device names (drive letters and COM port names) while drivers and the kernel use NT-style names, and how a driver creates a symbolic link from \DosDevices\name to a device with IoCreateSymbolicLink.  2 3

  5. Microsoft Learn, Naming files, paths, and namespaces. On how CON, PRN, AUX, NUL, COM1 through COM9, and LPT1 through LPT9 are reserved as file names, how the Win32 namespace has a “file namespace” and a “device namespace” with the “\\.\” prefix meaning access to the Win32 device namespace (for example \\.\PhysicalDrive0), and on the rules for resolving these names.  2 3 4

  6. Microsoft Learn, Introduction to driver objects. On how the I/O manager creates a DRIVER_OBJECT structure when a driver is loaded, how the driver object holds the entry points to the driver’s standard routines (including the MajorFunction array, which is the dispatch table), and how the I/O manager uses that table to call the handler function corresponding to a request.  2 3

  7. Microsoft Learn, Introduction to device objects. On how a DEVICE_OBJECT structure represents a logical, virtual, or physical device and becomes the target of an I/O request, how a driver creates a device object with IoCreateDevice, and how a device object is associated with the driver (the driver object) that created it.  2 3

  8. Microsoft Learn, Using files in a driver. On how, in the kernel, a file object represents an instance of an opened file (or device), and how a file object is created each time a file is opened and holds the context of that open, such as the current byte offset.  2 3

  9. Microsoft Learn, File handles. On how the file handle CreateFile returns is specific to the process and is associated with an opened file object, how opening the same file several times gives a separate handle (and open state) each time, and how a handle should be closed with CloseHandle once it is no longer needed.  2 3

  10. Microsoft Learn, Completing IRPs. On how it is the call to IoCompleteRequest that completes an I/O operation, how the IoCompletion routines registered by the drivers higher in the stack are called in turn on completion, and on the flow by which completion of a request happens at a different moment from its issue and the status is finally returned to the requester.  2 3 4 5

  11. Microsoft Learn, Synchronous and asynchronous I/O. On how, with synchronous I/O, the function does not return until the I/O completes and the thread is made to wait, whereas with asynchronous (overlapped) I/O the function that issued the request returns immediately and the thread can go on with other work, how asynchronous I/O requires the handle to be opened with FILE_FLAG_OVERLAPPED, and on the several ways of receiving notification of completion.  2 3

  12. Microsoft Learn, IRP_MJ_CLEANUP. On how receiving this request indicates that the last handle to a file object associated with the target device object has been closed, how the file object may nevertheless not be released yet because of outstanding I/O requests, and how this IRP is sent in the context of the process that closed the handle.  2 3

  13. Microsoft Learn, IRP_MJ_CLOSE. On how receiving this request indicates that the reference count of the file object has reached zero and the file object is about to be released, and how it is sent after the cleanup request but does not necessarily follow it immediately, because it waits for outstanding I/O to complete.  2 3

  14. Microsoft Learn, CreateFileW function. On how CreateFile can open and return a handle not only to files but also to devices such as physical disks, volumes, consoles, communications ports (COM ports), and pipes, how names in the “\\.\” form are used when opening a device, and on the meaning of the various flags, beginning with FILE_FLAG_OVERLAPPED.  2

  15. Microsoft Learn, WinObj - Sysinternals. On how WinObj is a tool that displays the NT Object Manager namespace and lets you browse the objects in that namespace, including device objects and symbolic links.  2

  16. Microsoft Learn, IRP major function codes. A list of the IRP major function codes (IRP_MJ_CREATE, IRP_MJ_READ, IRP_MJ_WRITE, IRP_MJ_CLEANUP, IRP_MJ_CLOSE, IRP_MJ_DEVICE_CONTROL, IRP_MJ_PNP, and so on), what each request means, and which drivers are expected to handle it. 

  17. Microsoft Learn, I/O stack locations. On how the I/O manager provides an I/O stack location inside an IRP for each driver in a chain of layered drivers, how each stack location holds the major and minor function codes and the parameters of that request, and how each driver obtains its own stack location with IoGetCurrentIrpStackLocation to learn what is being requested. 

  18. Microsoft Learn, Device nodes and device stacks. On how device objects stack up to form a device stack, how an IRP is first sent to the device object at the top of the stack and is then processed or forwarded downward at each layer, and how the device objects of filter drivers exist inserted into the stack.  2

  19. Microsoft Learn, Filter Manager Concepts. On how the filter manager is a kernel-mode driver that ships with Windows, how minifilter drivers can interpose on I/O requests to the file system with pre and post callbacks, and how the position at which each minifilter interposes (its altitude) determines its order within the I/O stack. 

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Frequently Asked Questions

Common questions about the topic of this article.

What is an IRP?
An IRP (I/O Request Packet) is the packet the Windows kernel's I/O manager uses to bundle up an application's read, write, and other requests and hand them to a device driver. Microsoft's driver development documentation explains that most requests sent to device drivers are packaged as IRPs. An IRP carries a major function code that indicates the kind of request (create, read, write, cleanup, and so on) and a stack location for each driver it passes through, and it is processed as it is handed down the device stack from top to bottom. Each driver chooses whether to complete the IRP itself, pass it to the driver below, or hold it pending and complete it later. Application developers never touch an IRP directly, but notations such as IRP_MJ_READ that appear in Process Monitor's Operation column are this very mechanism.
Why can Windows open a file, a serial port, and a printer with the same CreateFile call?
Because whatever name you pass to CreateFile follows the same path: it is ultimately resolved to a device object in the Object Manager namespace, and the I/O manager then creates a file object tied to that device and returns a handle. A drive letter such as C: is really a symbolic link to an NT device name such as \Device\HarddiskVolume3, and a designation like \\.\COM1 likewise resolves to the serial port's device object. Whatever device the name resolves to, every subsequent request is packaged into the same IRP format and delivered to a driver, which is why files and devices alike can be opened and read or written through the same API. UNC paths use the same mechanism and simply resolve to the network redirector's device. This design of a namespace plus a packet is the real source of the consistency of Windows I/O.
Why does a file sometimes fail to be released immediately even though CloseHandle was called?
Because what CloseHandle does is return one handle, not close the file. A file object inside the kernel carries two counts: a handle count, which is the number of handles, and a reference count, which is the number of references held by kernel components. IRP_MJ_CLEANUP is sent to the file system once the last handle is closed, but as long as references inside the kernel remain, such as outstanding I/O or a memory-mapped file section, the file object itself stays alive, and IRP_MJ_CLOSE is sent only once the reference count reaches zero. Many phenomena, such as a file that cannot be deleted after being memory-mapped, or a file reported as in use after the application was closed, are explained by this two-stage mechanism.
What use is knowledge of IRPs and device stacks to an application developer?
Even if you never write an IRP yourself, it pays off in both investigation and design. First, Process Monitor's Operation column (when advanced output is enabled) displays exactly the vocabulary of IRPs, such as IRP_MJ_CREATE and IRP_MJ_READ, so knowing the terms at this layer lets you actually read the log. Second, once you know that filter drivers such as antivirus software sit across every path that file I/O takes, you have somewhere to start when investigating a problem like file access being slow only in one particular environment. And once you understand that Windows I/O is built so that issuing and completing a request can be separated, and that synchronous I/O is no more than a guarantee that the call does not return before completion, you can use asynchronous I/O, I/O completion ports, and .NET's async/await with a grasp of how they work.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog