I’m freshly caffeinated after an evening at Barnes & Noble, which, despite its inclusion of a Starbucks, lacks a hotspot. Who knew. At any rate, I apparently have nothing better to do on a Friday night, so I thought I’d blog. How pathetic. 🙂
To understand why the previous method of finding out “who” is the submitter of a given IRP fails, you really have to understand one of the most fundamental architecture decisions DaveC & company made about the kernel. As we discussed last time, it’s not uncommon at all to post an IRP off to a worker thread. Now, think about the paradigm shift at work here: in all other situations involving communication between functions (and even between libraries), the call stack is typically built using the architecture’s built-in stack mechanisms – esp/ebp on IA-32, for example. In fact, we C programmers have almost forgotten about the fact that there can be other glue to hold together functional programs besides the stack.
There are problems with the traditional call stack, though. Microsoft learned some of these problems in the context of supporting the legacy Windows 9x architecture, which I heard was an influencer of the decision to make the switch to a fundamentally new way of managing function-to-function communication: I/O Request Packets. Before I thought about this shift in software architecture, I was pretty unclear as to why Microsoft named the parameter-holders in IRPs IO_STACK_LOCATIONs.
But think about it: think about the IRP and its associated IO_STACK_LOCATIONS as, primarily, a replacement for the good old fashioned call stack. What characterizes a call stack? Arguments? Yep, they’re part of the IRP – look at the IO_STACK_LOCATION in the DDK. Return addresses? Yep – see completion routines, for example. Local variable storage? Well, sort of – there are a few general-purpose scratch areas in the IRP structure (four PVOIDs in a DriverContext array, although you can’t actually use all of them all of the time, and only for the time that you own an IRP – beware CSQ interactions, for one thing).
Clearly manipulating the goo inside of an IO_STACK_LOCATION is more of a pain than just using the language-intrinsic stack manipulation functions that utilize esp and local stack memory. So, what do you gain for your trouble? Well, two principle things, and lots of ancillary benefits. Firstly, you get a practically unlimited stack size. Remember that the Windows kernel only provides 12K of stack to kernel-mode threads. There are a couple of reasons for this, but we’ll discuss them another time. Suffice it to say that there is a theoretical maximum limit on how deep call stacks can go, and that limit becomes a practical consideration when you start thinking about filter drivers. It’s sometimes very difficult to get your filesystem filter driver to play nice with Norton Antivirus, for example. Microsoft even sponsors “PlugFests” on a regular basis, where all of the filesystem folks get together from all over the world and test compatibility with each others’ drivers.
Using IRPs, on the other hand, allows the creator to specify enough IO_STACK_LOCATIONs to get all the way down the driver stack, guaranteed. This simply wouldn’t be possible without an arbitrarily-definable stack size. The 12K stack is suddenly much less of an issue.
But the more important architectural effect of using IRPs is that it allows for the creation of a fundamentally asynchronous operating system kernel. To get true asynchronicity, you need a well-defined way to get a return value back from the function you called, and you cannot depend on the (traditional) call stack to do it, otherwise you wouldn’t be asynchronous. Think about this for a second if it doesn’t make sense. Now, it’s quite possible to code up workarounds using pass-by-reference variables in user-mode code. However, this doesn’t work as well in an asynchronous kernel, because you have no idea what process context you’re going to be in at any given time (see the previous article in this series for an example of the ramifications of this fact). To make this work at all, you have to have system-global memory from the paged or (more likely) nonpaged pool. Instead of the laissez-faire world of each developer building his own elaborate return value management system (which would have ended up looking a lot like a part of an IRP anyway), Microsoft standardized on a single method for management of return values.
Other considerations of dealing with an asynchronous system also exist. One in particular is the architecture drivers would likely have in the absence of completion routines: you would have an entire chain of waits, which could of course only be done at < DISPATCH_LEVEL. They would be inefficient because they would forcibly break the asynchronicity of the OS, and even more seriously, they would dramatically increase contention for the dispatcher lock, which is one of the hottest locks in the whole OS. Contrast this scenario with the IRP completion scenario as it exists today. I'll have to write more about how completion works later, because this is now officially waaaaay too long of an article.
Okay, so getting back to the point. . . 🙂 Because you have no idea what process context you’re in at any given time, you have to try something else to discern the sender of an IRP. Fortunately, we don’t have to look long (if you know what you’re looking for!) to find a knight in shining armor to rescue us from our plight. I really shouldn’t drink this much caffeine this late at night. Deep inside the IFS Kit lie two functions, IoGetRequestorProcess() and IoGetRequestorProcessId(). They both take a PIRP and return a PEPROCESS and a ULONG, respectively. They look inside the IRP and, as if by magic (not really; stare at the IRP structure for 30 seconds and it becomes obvious how), they return the process associated with an IRP. It’s once again just a hop, skip, and a jump from there to the SID of the “who” that sent the request.
Problem solved! Problem solved? Problem solved. . .
Problem not solved. . .
To be continued next time I am over-caffeinated!