Although this issue was raised many-many times, I found no good and concise explanation of the basic techniques one has to use to communicate between a user mode application and a kernel mode driver. Most of the first tries are about sharing events, and use the
SetEvent and the
WaitForSingleObject functions to implement notification. While it may serve the purpose in certain cases, this technique is slow and not a good way of doing it.
In this article, I'll describe the way my applications and drivers communicate. This is not the only technique you can use, but I found it convenient and considerably easy to implement.
Please note that the sample application isn't complete, because I cannot simulate a hardware IRQ. Nevertheless, it has all the code you need to understand what's going on. The user mode application is also un-tested, and uses IOCP ports which makes it a bit more complex.
Here are the assumptions I make about the problem:
- You have a hardware that generates interrupts.
- When an IRQ fires, you want to read some data from the hardware.
- You want to pass this data back to an application.
Note that this general description fits to many problems, such as reading a file from the hard-drive, or collecting data from a custom data acquisition hardware.
Implementation of the user mode application
As I said earlier, my assumption is that the user mode application is waiting for the driver to generate some data. Most probably, you'll want to have a thread that has a loop in it, which will read the data from the driver and do something with it. So, the first step obviously is to open the driver.
1. Open the device
I won't go into too much details here. You have to use the
CreateFile function to open the device. Pass in the
OPEN_EXISTING flag for the
dwCreationDisposition, and the
FILE_FLAG_OVERLAPPED for the
dwFlagsAndAttributes. The latter one is not necessary, but since your driver is most probably 100% asynchron-ready (is it?), you'll communicate asynchronously with it, anyway. One more note on opening the device: you must call the
IoCreateSymbolicLink function in your driver when you create the device object in order to be able to open the device with the
2. Read that data!
Once you have the device open, you can use the
ReadFile function to read data from the device driver. Since we opened the device with the overlapped flag, you have to use an
OVERLAPPED structure here. When you call
ReadFile, it will immediately return with
GetLastError will return
ERROR_IO_PENDING. Now, call one of the wait functions (i.e.,
GetQueuedCompletionStatus, etc.) to wait for the data to arrive.
When the wait function returns (with success), the buffer you passed in to
ReadFile contains the data you were so keen on getting!
Implementation of the kernel mode driver
Since we want to read data from the driver, you'll need a dispatch read function. You can specify it in your
DriverEntry routine by setting the
DriverObject->MajorFunction[IRP_MJ_READ] field. This function has the following signature:
NTSTATUS DispatchRead(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp);
The next step is to set up some sort of queue that will store the read requests. For this, I use the
LIST_ENTRY structure and initialize it with the
InitializeListHead function. You can do it in the
DriverEntry routine. Don't forget that this list must be in non-pageable memory! The best thing is to put it in your device extension structure. As you probably know, queues are a real pain when you store IRPs in it and want to be sure that the cancellation of these IRPs are correctly done. God bless Microsoft, for it provided (in newer DDKs) the CSQ routines! They will do most of the hard work. (See DDK for a complete sample.)
So, the next step is to initialize our queue 'cancel-safely'. This is done by calling the
After this, we implement the dispatch read function so that it puts the incoming IRP into our queue (just for clarity: 1
ReadFile in user mode = 1 IRP in the dispatch read function). This is done by calling the
IoCsqInsertIrp function. Now that we have the IRP queued, we simply return
STATUS_PENDING, telling the IO system that the operation is registered and will be served. Note that when we return from the dispatch routine, the
ReadFile function in the user mode code will return also, and - as expected -
Now, when an IRQ arrives, we have to read the data from the device. Do it like this:
- Disable the IRQ in the hardware, so we can safely access the hardware memory or ports.
- Queue the DPC routine by calling
KeInsertQueueDpc. It is of utmost importance that you don't do anything in your IRQ routine except for these two steps! (Of course, you can do whatever you want, but then your Windows will be deadly slow.)
- In your DPC routine, call the
IoCsqRemoveNextIrp function to de-queue an IRP that the dispatch read function queued. If it returns
NULL, the queue is empty.
- Get access to the user buffer by doing something like this (
Irp is the IRP we've just de-queued):
PUCHAR UserBuffer = (PUCHAR)MmGetSystemAddressForMdl(Irp->MdlAddress);
- Read data from your hardware and fill the
- Make sure that the buffers are all flushed:
KeFlushIoBuffers(Irp->MdlAddress, TRUE, FALSE);
- Call the
IoCompleteRequest and pass in
Irp to complete it.
- Re-enable the IRQ in the hardware.
When step 7 completes, the wait function in your user mode application returns and the buffer is full of data!
As you can see, it's not so difficult to implement data exchange once you know the techniques. In a real application, there are additional steps you have to make, such as checking the user buffer's size and accessibility. Also, it is usually unnecessary to complete an IRP each time you have an IRQ. You could as well just fill the buffer until it's full, and complete it when there is no space left in the buffer. The size of the buffer is in the current stack location of the IRP:
PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);
ULONG BufferSize = IrpStack->Parameters.Read.Length;