Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Exchange data between device drivers and user applications

0.00/5 (No votes)
6 Nov 2004 2  
Describes how to exchange data between a device driver and a user mode application.

Introduction

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.

Sample Projects

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.

Assumptions

Here are the assumptions I make about the problem:

  1. You have a hardware that generates interrupts.
  2. When an IRQ fires, you want to read some data from the hardware.
  3. 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 CreateFile function.

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 0, and GetLastError will return ERROR_IO_PENDING. Now, call one of the wait functions (i.e., WaitForSingleObject, 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 IoCsqInitialize function.

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 - GetLastError returns ERROR_STATUS_PENDING.

Now, when an IRQ arrives, we have to read the data from the device. Do it like this:

  1. Disable the IRQ in the hardware, so we can safely access the hardware memory or ports.
  2. 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.)
  3. 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.
  4. 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);
  5. Read data from your hardware and fill the UserBuffer.
  6. Make sure that the buffers are all flushed:
    KeFlushIoBuffers(Irp->MdlAddress, TRUE, FALSE);
  7. Call the IoCompleteRequest and pass in Irp to complete it.
  8. 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!

Conclusions

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;

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here