It’s not like we keep track, but one of the most frequent questions we receive, year in and year out, is: “I have two drivers running in the same system. What’s the “best” way for them to communicate?”
Naturally, when you think of two kernel mode drivers communicating, the first thing that comes to mind is sending IRPs from one driver to the other using IoCallDriver(…). This works pretty well (after all, it is the typical method of communication used in the operating system!), especially if the communication is infrequent. When thinking about using IRPs for communicating events or information between two kernel mode drivers, the most important thing to keep in mind is that one driver can always build an IRP and send it to another driver using IoCallDriver(…). There’s no need to use any Zw system services, or for the first driver to “open” the second.
For cases where driver-to-driver communication is more frequent, or a shared context area (containing, perhaps a shared queue or two and even shared locks) is required, there is another simple, yet highly flexible, method. This method relies on the fact that all drivers reside in the same area of the system address space, regardless of the (user) process context. Thus, every (kernel mode) driver in the operating system is within the address space of every other (kernel mode) driver.
If Driver A wants to communicate with Driver B, one simple way to do this is to have the two drivers share a “context block”, the format of which is known to both of these drivers. This context block will be located in non-paged pool (See Figure 1). During initialization, Driver A allocates the context block and fills it in with some data. This data may include initialized queue list heads, initialized queue spin locks, or even pointers to functions that reside within Driver A. Once the block has been allocated and initialized, Driver A builds an IRP with a function code IRP_MJ_INTERNAL_ DEVICE_CONTROL. One easy way to do this is by calling IoBuildDeviceIoControlRequest(…). Driver A puts a pointer to the context block (the kernel virtual address of the context block, to be specific) in Irp->UserBuffer in the IRP. And then Driver A sends the IRP to Driver B using IoCallDriver(…).
(Insert Figure 1 from online content, drawing, entitled: Two Drivers, Sharing a Context Block)
Figure 1 – Two Drivers, Sharing a Context Block
Driver B receives the IRP from Driver A at its DispatchDeviceControl(…) entry point. Driver B saves away the pointer to the context block that it finds in Irp->UserBuffer, and fills in any information it wants to pass to Driver A into the context block. Driver B then completes the IRP, by calling IoCompleteRequest(…). This completion is detected by Driver A any way it pleases (using a completion routine, synchronously waiting for the request to complete, or whatever).
At this point, both Driver A and Driver B have a pointer to this shared context block in non-paged pool. Each driver can access and manipulate the data in the shared context block (perhaps acquiring a shared spin lock and adding things to a shared queue), and can even call functions in the other driver, via pointers that have been pre-filled into the context block.
We’ve used this simple communication method, using a shared context block, with direct functions calls from driver to driver, in several projects and can attest that this is a very effective and efficient mechanism for driver-driver communication.
Of course, this is just one mechanism for communicating between drivers. Need to only communicate the state of an event? How about sharing a named event between two drivers? The possibilities are endless.
Once you realize that both drivers share the system portion of the kernel mode address space, and that this address space doesn’t change with the user process that’s mapped, the possibilities are endless. So, don’t be constrained by just using IRPs. Get creative!