52964.fb2
This chapter looks at how to write driver dispatch routines that process I/O Request Packets (IRPs). Dispatch routines are used to handle requests from Win32 applications. It is crucial that you understand everything in this chapter clearly. If necessary, refer to Chapter 3 where IRPs are first introduced.
The dispatch routines for the Wdm1 driver are explained in full. These handle open, close, read, write, and IOCTL requests. The Wdm1 driver implements a global memory buffer that is shared by all Wdm1 devices.
This chapter takes a good hard look at I/O Request Packets (IRPs). It is worth reading this chapter carefully, as a good understanding of IRPs will ease your passage through the rest of the book.
Table 7.1 lists the most common Win32 functions that are used to access devices. A CreateFile call to your device ends up as a Create IRP, an I/O Request Packet with a major function code of IRP_MJ_CREATE. The driver routine to handle this IRP can have any name. However, I use a generic name for the Create IRP handler of Create. In your driver, you would usually put a short name or acronym in front of this base name. The Wdm1 device driver's Create IRP handler is called Wdm1Create.
Table 7.1 is not an exhaustive list of Win32 functions and their matching IRPs. For example, ReadFile has several variants, such as ReadFileEx, but they all end up as IRP_MJ_READ requests. IRPs can also be issued by Windows on behalf of the user program. For example, if an application terminates unexpectedly, the operating system will try to tidy any open files by issuing an IRP_MJ_CLOSE IRP to each file.
Table 7.1 Common dispatch routines
Win32 Function | IRP Major Code | Base Driver routine name |
---|---|---|
CreateFile | IRP_MJ_CREATE | Create |
CloseHandle | IRP_MJ_CLOSE | Close |
ReadFile | IRP_MJ_READ | Read |
WriteFile | IRP_MJ_WRITE | Write |
DeviceIoControl | IRP_MJ_DEVICE_CONTROL | DeviceControl |
A driver need not handle all these IRPs, though handling Create and Close IRPs is an obvious minimum. Its DriverEntry routine sets up the entry points that are valid. If an entry point is not set, then the I/O Manager fails the Win32 request and GetLastError returns 1.
The following line in DriverEntry sets the Wdm1Read routine as the handler for Read IRPs.
DriverObject->MajorFunction[IRP_MJ_READ] = Wdm1Read;
All dispatch routines have the same function prototype. The function is passed a pointer to your device object and the IRP. The function must return a suitable NTSTATUS value (e.g., STATUS_SUCCESS).
NTSTATUS Wdm1Read( IN PDEVICE_OBJECT fdo. IN PIRP Irp)
A dispatch routine is usually called at PASSIVE_LEVEL IRQL. (This is a bit unexpected, as you might expect dispatch routines to run at DISPATCH_LEVEL IRQL.) Running at PASSIVE_ LEVEL means that a dispatch routine can very easily be interrupted by other parts of the kernel or even your own driver. However, a routine that runs at PASSIVE_LEVEL can issue most kernel calls, including writing to other files.
A dispatch routine can be called at DISPATCH_LEVEL if a higher level driver calls you at this IRQL level. This might happen if it calls you from a completion routine, as described in Chapter 9. If you think this might be happening, then assume that your driver dispatch routines are running at DISPATCH_LEVEL
Dispatch routines run in an arbitrary thread context. As well as standard user mode threads, the kernel has its own threads that run only in kernel mode. Although your IRP will have originated in one of these threads, you cannot guarantee that you arc running in its context. This means that a valid address in the originating thread may not be valid when your driver sees it. A later section in this chapter shows how to access user buffers correctly.
A dispatch routine must be reentrant. This means that it may be called "simultaneously" to process two separate IRPs. In a multiprocessor Windows 2000 system, one processor could call Wdm1Read with one IRP, and a second process on another CPU could also call Wdm1Read simultaneously with another IRP. However, do not dismiss reentrancy as some arcane requirement that your driver will never encounter. In Windows 98, the single processor could start running Wdm1Read to handle a first IRP. Wdm1Read could issue a kernel request that blocks its operation[14]. Windows 98 might then schedule a different user application that issues another read resulting in another call to Wdm1Read.
The first technique to make your routine reentrant is to use local variables. Each separate call to Wdm1Read definitely has its own separate set of variables on the kernel stack.
Do not use global variables or variables in the device extension unless you protect access to them. However, when these variables are first set up (DriverEntry or AddDevice), you do not need to take special precautions.
The Wdm1 driver uses a spin lock to protect access to the shared memory buffer variables, as described later.
There are two other main techniques to achieve reentrancy in dispatch routines. The first is to use the services of the I/O Manager to create a device queue of IRPs. The IRPs in the device queue are passed one at a time to a StartIo routine in your driver.
The second technique is to use Critical section routines if your device interrupts the computer, as explained in full in Chapter 16. Calling KeSynchronizeExecution runs your Critical section routine safely. KeSynchronizeExecution raises the IRQL to the interrupt IRQL and acquires the interrupt spin lock. This technique ensures that your routine is run to completion without being interrupted by another part of the driver.
There are three main techniques for handling IRPs.
• Handle immediately
• Put in a queue and process one by one
• Pass down to a lower-level driver
Only the simplest drivers, like Wdm1, can handle IRPs straightaway. Even so, you must still take precautions to ensure that your driver dispatch routines are reentrant.
Drivers that access real pieces of hardware usually want to serialize access to the hardware. As mentioned previously, the I/O Manager in the kernel provides a device queue that you can use. Dispatch routines call the kernel to put an IRP into the device queue. The I/O Manager calls your StartIo routine to process one IRP at a time. When your StartIo routine has completed an IRP, it should call the kernel to ensure that it is called again with the next available IRP.
Drivers might want to use more than one device queue. For example, a serial port driver will usually want to have two device queues; one for incoming read data and one for outgoing write data.
A driver can use a different queuing strategy. For example, the DebugPrint driver described in Chapter 14 lets only one read IRP be queued. Any further read IRPs are rejected while the first read IRP is outstanding.
The final main technique for handling IRPs is to pass them down for handling in a lower-level driver. This approach is common in WDM device drivers. Some drivers simply pass the handling of some IRPs to the next lower driver and forget about them. If you take a more active interest, you can inspect the results after all the lower-level drivers have handled the IRP. These techniques are covered later.
A variant on this theme is for a driver to build a new IRP (or IRPs), send it down to lower drivers, and process the results afterwards.
When a driver has finished working on an IRP, it must tell the I/O Manager. This is called IRP completion. As this code snippet shows, you must set a couple of fields in the IRP IoStatus field structure. IoStatus.Status is set to an NTSTATUS status code. The number of bytes transferred is usually stored in IoStatus.Information.
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = BytesTxd;
IoCompleteRequest(Irp,IO_NO_INCREMENT);
Finally, IoCompleteRequest is called (at or below DISPATCH_LEVEL IRQL). As well as the IRP pointer, you must supply a PriorityBoost parameter to give a boost to the scheduling priority of the thread that originated the IRP. For example, keyboard drivers use the constant IO_KEYBOARD_INCREMENT. This is a high value of 6 because foreground threads should respond quickly to user input.
If a dispatch routine does not process an IRP immediately, it must mark the IRP as pending using IoMarkIrpPending and return STATUS_PENDING. When you eventually get round to completing the IRP, do it as described above.
If an IRP is queued and its associated process dies unexpectedly (or calls CancelIo to cancel overlapped IRPs), these pending IRPs must be cancelled. You must set an IRP's cancel routine to handle this circumstance, and to handle the Cleanup IRP. Chapter 16 describes these options in full. Wdm1 does not queue IRPs, so handling these options is not necessary.
Figure 7.1 shows that an I/O Request Packet consists of a header IRP structure followed by a series of stack locations, each an IO_STACK_LOCATION structure. The information in the header and the current IRP stack location tell a driver what to do.
Figure 7.1 IRP overview
Table 7.2 lists some of the fields in the IRP header structure that a driver can access, while Table 7.3 gives the general layout of the IO_STACK_LOCATION structure.
Table 7.2 Some IRP structure fields
Field | Description |
---|---|
IO_STATUS_BLOCK IoStatus | Completion status of IRP |
PVOID AssociatedIrp.SystemBuffer | System space buffer (for Buffered I/O) |
PMDL MdlAddress | Memory Description List (for Direct I/O) |
BOOLEAN Cancel | Set if IRP has been cancelled |
ULONG Flags | IRP Flags |
Table 7.3 Some IO_STACK_LOCATION structure fields
typedef struct _IO_STACK_LOCATION {
UCHAR MajorFunction;
UCHAR MinorFunction;
// …
union {
struct { … } Create;
struct { … } Read;
struct { … } Write;
struct { … } DeviceIoControl;
} Parameters;
// …
} IO_STACK_LOCATION, *PIO_STACK_LOCATION;
The fact that there is an IRP header and several associated I/O stack locations can be a source of confusion. However, the stack locations are a powerful tool for processing IRPs when several layered drivers have to access an IRP in turn.
The I/O stack location contains most of the important information about the IRP. The MajorFunction is the IRP code (e.g., IRP_MJ_READ for a Read IRP). Some IRPs, such as IRP_ MJ_PNP, use the MinorFunction field to specify which particular Plug and Play function is being requested.
Each common IRP type has a struct within the Parameters union in the IO_STACK_LOCATION structure. For example, for Read IRPs, the Parameters.Read.Length field is the number of bytes to transfer. The parameters that are valid for each common IRP are described in the following text.
The key to understanding I/O stack locations is to realize that each driver needs to look at only one, the "current stack location". The information in the IRP header structure and the information in the current stack location are the parameters that a driver uses to process an IRP.
Let's go off track slightly to ask why Microsoft provides a set of I/O stack locations. If you have a stack of drivers that process an IRP, the highest might be a network protocol driver that can accept read requests of any length. This driver might know that the underlying transport driver can only cope with read transfers of up to 1024 bytes. Its job is to break up long transfers into a series of blocks, each with a maximum size of 1024 bytes. When the protocol driver calls the transport driver it, sets up the next I/O stack location with the transfer size set to 1024. When the transport driver processes the IRP, its current I/O stack location has this value, and it should proceed happily to process the IRP. When it has finished, the IRP is passed back to the protocol driver. This checks that the transfer worked and — assuming it did — sets up the next transfer and calls the transport driver again.
In this approach, the protocol driver sends the transport driver IRPs one by one. However, a more sophisticated protocol driver could allocate new IRPs, enough to move all the data. It could then issue them all to the transport driver. The protocol driver would have to check carefully that all the IRPs finished correctly. When all the data has been moved, one way or another, the protocol driver can finally complete its own original IRP.
This example reveals a problem. The field that contains the pointer to the data to be transferred is not in the I/O stack location, but in the IRP header. The transfer length is in the stack. Surely the data pointer ought to be in there as well. The fact that the original stack location is not changed by the call to the lower driver is good, as it makes it easier for a driver to remember how many bytes to transfer.
However, this reveals another common difficulty with IRPs — determining where to store a driver's own information about an IRP. Suppose the protocol driver wanted to remember something simple (e.g., how many bytes it had sent so far)[15].
The ideal place for some storage for drivers would be the I/O stack location. However, there is no space specifically reserved for drivers. Nonetheless, Read IRPs have a ULONG at Parameters.Read.Key in the current I/O stack location that can be used safely, although the DDK does not specifically say so. Write IRPs have a similar ULONG at Parameters.Write.Key.
There is some room in the IRP header that can be used by drivers — a PVOID DriverContext[4] in Tail.Overlay. Use the following code to access the first of these locations.
PVOID p = Irp->Tail.Overlay.DriverContext[0];
However, it is not safe to use these locations for storing context while an IRP is processed by lower drivers, for the simple reason that these other drivers may use this memory too.
The final point to note about I/O stack locations is that you can use different major function code when you call a lower driver. For example, you might implement a read request by sending the lower driver an IOCTL.
This section lists the parameters that are set for the common IRPs. In the following discussion, "the stack" means the current I/O stack location.
The main parameter of interest to the Create IRP handler is the FileObject field in the stack. This is a pointer to a _FILE_OBJECT structure. The FileName field in here is a UNICODE_STRING with any characters after the basic device name. If you appended \file to the symbolic link name found in the GetDeviceViaInterface routine in Wdm1Test, \file would appear in FileName. If no characters are appended, FileName has a length of zero.
Other parameters to the Create IRP are given in the Parameters.Create structure in the stack, such as the FileAttributes and ShareAccess.
If need be, you can double-check that the FileObject in the IRP header matches the one you were sent in the create request.
If you have queued up Read or Write IRPs for this file, the I/O Manager will have cancelled them before the close request is received. It does this by calling an IRP's Cancel routine and issuing a Cleanup IRP, as described in Chapter 16.
The Parameters.Read structure in the IRP stack has Length and ByteOffset fields that say how many bytes are requested and the file pointer. ByteOffset is a 64-bit integer stored in a LARGE_INTEGER structure. The Microsoft compiler can handle this type directly (i.e., Parameters.Read.ByteOffset.Quad Part is an __int64). If you need to specify 64-bit constant values in your code, append i64 to the constant (e.g., 100i64).
The user buffer can be specified in one of two ways, depending on whether your driver uses Buffered I/O or Direct I/O. See the following text for details of these terms. If using Buffered I/O, a pointer to the user buffer is in the IRP header at AssociatedIrp.SystemBuffer. For Direct I/O, a Memory Descriptor List (MDL) is in the IRP header in the MdlAd-dress field.
The Key field in the IRP stack Parameters.Read structure does not seem to be used for anything, and so could be used by a driver for any purpose.
The parameters for Write IRPs are identical to Read IRPs, except that the relevant parameters are in the stack Parameters.Write structure.
The Parameters.DeviceIoControl structure in the IRP stack has IoControlCode, InputBufferLength, and OutputBufferLength parameters.
The user buffer is specified using one or more of the AssociatedIrp.SystemBuffer, MdlAddress, or stack Parameters.DeviceIoControl.Type3InputBuffer fields. See the next section for details.
As a driver can run in the context of any thread, a plain pointer into the user's address space is not guaranteed to access the correct memory.
A driver can use two main methods to access the user's buffer properly, either Buffered I/O or Direct I/O. When you create a device, you must set the DO_BUFFERED_IO bit in the Flags field of the new device object to use Buffered I/O. For Direct I/O, set the DO_DIRECT_IO bit in Flags.
If you use Buffered I/O, the kernel makes the user's buffer available in some nonpaged memory and stores a suitable pointer for you in the AssociatedIrp.SystemBuffer field of the IRP header. Simply read or write this memory in your driver.
This technique is the easiest one for driver writers to use. However, it is slightly slower overall, as the operating system usually will have to copy the user buffer into or out of non-paged memory.
It is faster to use a Memory Descriptor List (MDL). However, this is only available to hardware that can perform Direct Memory Access (DMA). DMA and MDLs are not explained in this book, although Chapter 24 lists the changes in W2000 for those of you who have used DMA in NT 4 and earlier.
The MDL of the user's buffer is put in the MdlAddress field of the IRP header.
A final and uncommon technique for accessing a user's buffer is to use neither Buffered I/O nor Direct I/O. In this case, the user's buffer pointer is simply put in the UserBuffer field of the IRP header. If you are certain that your driver is the first driver to receive a request, the dispatch routine can directly access the buffer, as the driver will be operating in the context of the user's thread. Be very careful if you try to use this technique.
DeviceIoControl requests can use a combination of these user buffer access techniques. Each IOCTL can use a different method, if need be. However, most drivers simply use Buffered I/O, as IOCTL buffers are usually fairly small. I shall show how to define IOCTLs shortly.
The TransferType portion of the actual IOCTL code indicates the buffer access technique. For Buffered I/O, specify METHOD_BUFFERED for TransferType. For Direct I/O, use either METHOD_IN_DIRECT or METHOD_OUT_DIRECT.
If you use METHOD_BUFFERED, AssociatedIrp.SystemBuffer is used for the input and output buffer. The buffer size is the maximum of the user's input and output buffer sizes. As the same memory is used for both input and output, make sure that you use (or copy) the input data before you start writing any output data.
For both METHOD_IN_DIRECT and METHOD_OUT_DIRECT the DeviceIoControl input data appears in buffered memory at Irp->AssociatedIrp.SystemBuffer. In both cases, an MDL for the DeviceIoControl output data is put in Irp->MdlAddress. The size of the read or write buffer is in Parameters.DeviceIoControl.OutputBufferLength.
Finally, for METHOD_NEITHER, the input buffer user space pointer is put in Parameters.DeviceIoControl.Type3InputBuffer in the stack. The user space output buffer pointer is put in the IRP header UserBuffer field.
Wdm1 Dispatch Routines
The dispatch routines for the Wdm1 driver are in the file Dispatch.cpp, which is available on the book CD-ROM. These routines all run at PASSIVE_LEVEL IRQL, so they can be put in paged memory. All the basic dispatch routines complete the IRP straightaway.
The dispatch routines include various DebugPrint trace calls in the code. If you use the checked build version of Wdm1, you can view the trace output using the DebugPrint Monitor application. Listing 7.1 shows the (slightly edited) DebugPrint output on Windows 2000. The Wdm1 driver was started at around 12:00 and the Wdm1Test program was run at 12:10. You can follow the program execution as each test in Wdm1Test is run. The DebugPrint output would be different in Windows 98 because the SetFilePointer function does not work for device files.
Listing 7.1 DebugPrint output in Windows 2000
Monitor 12:03:04 Version 1.02 starting to listen under Windows 2000 (5.0 build 1877)
DebugPrint 11:59:09 Version 1.02 started Wdm1 12:00:42 DebugPrint logging started
Wdm1 12:00:42 RegistryPath is \REGISTRY\Machine\System\ControlSet002\SERVICES\Wdml
Wdm1 12:00:42 DriverEntry completed
Wdm1 12:00:42 AddDevice
Wdm1 12:00:42 FDO is 80AAB020
Wdm1 12:00:42 Symbolic Link Name is \??\Root#UNKNOWN#0003#{c0cf0640…}
Wdm1 12:00:42 PnP IRP_MJ_PNP:IRP_MN_QUERY_CAPABILITIES
Wdm1 12:00:42 PnP IRP_MJ_PNP:IRP_MN_FILTER_RESOURCE_REQUIREMENTS
Wdm1 12:00:43 PnP IRP_MJ_PNP:IRP_MN_START_DEVICE
Wdm1 12:00:43 PnP IRP_MJ_PNP:IRP_MN_QUERY_CAPABILITIES
Wdm1 12:00:43 PnP IRP_MJ_PNP:IRP_MN_QUERY_PNP_DEVICE_STATE
Wdm1 12:00:43 PnP IRP_MJ_PNP:IRP_MN_QUERY_BUS_INFORMATION
Wdm1 12:00:43 PnP IRP_MJ_PNP:IRP_MN_QUERY_DEVICE_RELATIONS
Wdm1 12:10:14 Create File is
Wdm1 12:10:14 Read 4 bytes from file pointer 0
Wdm1 12:10:14 Read: 4 bytes returned
Wdm1 12:10:14 Write 4 bytes from file pointer 0
Wdm1 12:10:14 Write: 4 bytes written
Wdm1 12:10:14 Read 1 bytes from file pointer 3
Wdm1 12:10:14 Read: 1 bytes returned
Wdm1 12:10:14 Write 4 bytes from file pointer 3
Wdm1 12:10:14 Write: 4 bytes written
Wdm1 12:10:14 DeviceIoControl: Control code 0022200C InputLength 0 OutputLength 4
Wdm1 12:10:14 DeviceIoControl: 4 bytes written
Wdm1 12:10:14 DeviceIoControl: Control code 00222010 InputLength 0 OutputLength 7
Wdm1 12:10:14 DeviceIoControl: 7 bytes written
Wdm1 12:10:14 DeviceIoControl: Control code 00222010 InputLength 0 OutputLength 8
Wdm1 12:10:14 DeviceIoControl: 0 bytes written
Wdm1 12:10:14 DeviceIoControl: Control code 00222004 InputLength 0 OutputLength 0
Wdm1 12:10:14 DeviceIoControl: 0 bytes written
Wdm1 12:10:14 DeviceIoControl: Control code 00222010 InputLength 0 OutputLength 7
Wdm1 12:10:14 DeviceIoControl: 7 bytes written
Wdm1 12:10:14 DeviceIoControl: Control code 00222008 InputLength 0 OutputLength 0
Wdm1 12:10:14 DeviceIoControl: 0 bytes written
Wdm1 12:10:14 DeviceIoControl: Control code 0022200C InputLength 0 OutputLength 4
Wdm1 12:10:14 DeviceIoControl: 4 bytes written
Wdm1 12:10:14 DeviceIoControl: Control code 00222014 InputLength 0 OutputLength 0
Wdm1 12:10:14 DeviceIoControl: 0 bytes written
Wdm1 12:10:14 Write 4 bytes from file pointer 0
Wdm1 12:10:14 Write: 4 bytes written
Wdm1 12:10:14 Close
The Wdm1 create and close routines do nothing except complete the IRP successfully. A helper function, CompleteIrp, is used that sets the IRP header IoStatus fields to the given parameters and calls IoCompleteRequest.
The create routine shows how to access the current I/O stack location using IoGetCurrentIrpStackLocation. In the checked build version, it prints out the FileName field in the stack FileObject.
PIO_STACK_LOCATION IrpStack = IoGetCurrentlrpStackLocation(Irp);
DebugPrint("Create File is %T", &(IrpStack –>FileObject->FileName));
Things start to get interesting in the write dispatch routine, Wdm1Write, shown in Listing 7.2. It starts by getting the current stack location pointer and retrieving the current file pointer and the number of bytes to transfer. If the file pointer is less than zero (the kernel should ensure that it never is), it returns STATUS_INVALID_PARAMETER. It is possible to receive a transfer length of zero.
Listing 7.2 Wdm1 write dispatch routine
NTSTATUS Wdm1Write(IN PDEVICE_OBJECT fdo, IN PIRP Irp) {
PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);
NTSTATUS status = STATUS_SUCCESS;
ULONG BytesTxd = 0;
// Get call parameters
LONGLONG FilePointer = IrpStack->Parameters.Write.ByteOffset.QuadPart;
ULONG WriteLen = IrpStack->Parameters.Write.Length;
DebugPrint("Write %d bytes from file pointer %d", (int)WriteLen,(int)FilePointer);
if (FilePointer<0) status = STATUS_INVALID_PARAMETER;
else {
// Get access to the shared buffer
KIRGL irql ;
KeAcquireSpinLock(&BufferLock,&irql);
BytesTxd = WriteLen;
// (Re)allocate buffer if necessary
if ( ((ULONG)FilePointer)+WriteLen>BufferSize) {
ULONG NewBufferSize = ((ULONG)FilePointer)+WriteLen;
PVOID NewBuffer = ExAllocatePool(NonPagedPool.NewBufferSize);
if (NewBuffer==NULL) {
BytesTxd = BufferSize – (ULONG)FilePointer;
if (BytesTxd<0) BytesTxd = 0;
} else {
RtlZeroMemory(NewBuffer,NewBufferSize);
if (Buffer!=NULL) {
RtlCopyMemory(NewBuffer,Buffer,BufferSize);
ExFreePool(Buffer);
}
Buffer = (PUCHAR)NewBuffer;
BufferSize = NewBufferSize;
}
}
// Write to shared memory
if (BytesTxd>0 && Buffer!=NULL) RtlCopyMemory(Buffer+FilePointer, Irp->AssociatedIrp.SystemBuffer, BytesTxd);
// Release shared buffer
KeReleaseSpinLock(&BufferLock,irql);
}
DebugPrint("Write: %d bytes written", (int)BytesTxd);
// Complete IRP
return Completelrp(Irp,status,BytesTxd);
}
The shared memory buffer is implemented using these three global variables.
KSPIN_LOCK BufferLock;
PUCHAR Buffer = NULL;
ULONG BufferSize = 0;
If the buffer size is greater than zero, Buffer points to some nonpaged memory of this size. As mentioned earlier in this chapter, there must be some mechanism to protect access to such global variables in a multiprocessor environment (e.g., to prevent one dispatch routine from changing BufferSize while another, or even the same, routine tries to access or change it simultaneously).
Spin Locks
A kernel spin lock called BufferLock provides this protection. A spin lock can be used where code needs access to a resource of some sort for a short time.
The spin lock is initialized in the Wdm1 DriverEntry routine as follows.
KeInitializeSpinLock(&BufferLock);
Use the KeAcquireSpinLock function to acquire a spin lock and KeReleaseSpinLock to release it. Only one instance of a piece of code can acquire a spin lock at the same time. Other attempts to acquire the spin lock will "spin" until the resource becomes available. "Spinning" means that KeAcquireSpinLock keeps looking continuously. For this reason, make sure that you only hold a spin lock for a short time. The DDK recommends that you never hold a spin lock for more than 25 microseconds.
As shown in the code example, you must provide a pointer to a KIRQL variable in the call to KeAcquireSpinLock. This stores the original IRQL level before it is raised (if necessary) to DISPATCH_LEVEL The call to KeReleaseSpinLock lowers the IRQL if necessary. If you are certain that your code is working at DISPATCH_LEVEL, you can use the KeAcquireSpinLockAtDpcLevel and KeReleaseSpinLockFromDpcLevel routines for better performance.
The Wdm1 driver acquires the BufferLock spin lock for the duration of any accesses to the Buffer and BufferSize variables. Do not access paged code or data while holding a spin lock, as the system will almost certainly crash. Definitely do not exit a main dispatch routine while holding a spin lock.
Write Algorithm
The write dispatch stores the write data in the shared memory buffer, starting from the given file pointer. It extends the buffer, if necessary.
If there is no buffer at all, or the buffer needs to be extended, ExAllocatePool is called to allocate some nonpaged memory. Notice that the algorithm checks for a NULL return value and copes as best as it can.
A new memory buffer is zeroed using RtlZeroMemory. If an old shorter buffer exists, it is copied to the start of the new buffer using RtlCopyMemory. RtlMoveMemory can be used if the source and destination pointers overlap. The old buffer is removed with ExFreePool.
Finally, Wdm1Write copies the data from the user buffer using RtlCopyMemory. As Wdm1 uses Buffered I/O, it can simply copy the data from Irp->AssociatedIrp.SystemBuffer.
This algorithm is fairly crude, because the buffer may have to be reallocated often. A much-enhanced version of Wdm1 could implement a RAM disk.
The driver unload routine frees any shared memory buffer.
if (Buffer!=NULL) ExFreePool(Buffer);
The read dispatch routine for Wdm1, Wdm1Read, is simpler than the write handler. It acquires the spin lock while it accesses the global variables. The required number of bytes are copied to the user's buffer at Irp->AssociatedIrp.SystemBuffer. If the user requests more data than is in the buffer, the request is truncated.
The Wdm1DeviceControl dispatch routine handles the four IOCTLs defined for Wdm1 devices: Zero the buffer, Remove the Buffer, Get the buffer size, and Get the buffer.
All these IOCTLs use Buffered I/O, so any input and output data is found at Irp->Associ-atedIrp.SystemBuffer. As usual, the routine acquires the shared buffer spin lock for the duration of the call. The actual implementation of each IOCTL is straightforward. The Get buffer size and Get buffer handlers check that the output buffer is large enough; if not, they return STATUS INVALID_PARAMETER.
Defining IOCTLs
An IOCTL code is a 32-bit value formed using the CTL_CODE macro shown in Table 7.4. The Wdm1 example defines its IOCTL codes in Ioctl.h, as shown in this example.
//define IOCTL_WDM1_ZERO_BUFFER CTL_CODE( \
FILE_DEVICE_UNKNOWN, \
0x801, \
METHOD_BUFFERED, \
FILE_ANY_ACCESS)
Table 7.4 CTL_CODE macro parameters
Parameter | Description |
---|---|
DeviceType | FILE_DEVICE_XXX value given to IoCreateDevice. |
Control Code | IOCTL Function Code 0x000–0x7FF Reserved for Microsoft 0x800-0xFFF Private codes |
TransferType | METHOD_BUFFERED METHOD_IN_DIRECT METHOD_OUT_DIRECT METHOD_NEITHER |
RequiredAccess | FILE_ANY_ACCESS FILE READ_DATA FILE_WRITE_DATA FILE READ_DATA|FILE_WRITE_DATA |
Ioctl.h is also included in the Wdm1Test project. It includes the standard winioctl.h header file first to get the definition of CTL_CODE.
Dispatch.cpp also contains a handler for the Windows Management Instrumentation IRP, IRP_MJ_SYSTEM_CONTROL. Wdm1SystemControl simply passes the IRP down to the next driver in the stack. A full explanation of this process is given later.
IoSkipCurrentIrpStackLocation(Irp);
PWDM1_DEVICE_EXTENSION dx = (PWDMl_DEVICE_EXTENSION)fdo->DeviceExtension;
return IoCallDriver(dx->NextStackDevice, Irp);
This chapter has looked in detail at I/O Request Packets (IRPs) and how to write dispatch routines to handle common IRPs. In addition, it has covered how to define IOCTL codes, and how to use spin locks. The Wdm1 dispatch routines implement a shared memory buffer, protected by a spin lock.
The next chapter looks at Plug and Play in detail, enhancing Wdm1 to support PnP correctly.
Blocking in dispatch routines is not recommended as it defeats the purpose of overlapped operations.
If the driver processes IRPs one by one, it could store this information in its device extension.