Книга: Writing Windows WDM Device Drivers

Deferred Procedure Calls

Deferred Procedure Calls

Code that runs at an elevated IRQL needs to run as quickly as possible. An elevated IRQL is any IRQL above DISPATCH_LEVEL (e.g., at Device IRQL in an interrupt service routine). Code that runs at an elevated IRQL cannot make most useful kernel calls.

The Windows kernel helps solve both these problems with Deferred Procedure Call (DPC) routines, that run at DISPATCH_LEVEL When an interrupt service routine has done all the jobs that must be performed, it should request that its DPC routine be run. This DPC routine should continue where the interrupt service routine left off.

A DPC typically either starts another transfer or completes an IRP.

In WdmIo, all the bytes are transferred in the interrupt handler. When the transfer is complete, WdmIo asks that its DPC be run to complete the IRP.

Other drivers may use their DPC routine to do data transfers. While WdmIo could use this technique, it would be slower. Processing the read or write commands in WdmIo is usually fairly quick, so it is simplest to get it over and done with[43].

Direct Memory Access (DMA) is a hardware facility for transferring many bytes of data from place to place without having to be handled by the processor. The DMA controller or the relevant device usually interrupts when the whole transfer has finished. The interrupt service routine usually runs a DPC routine to start its next transfer or complete the IRP. DMA transfers cannot be started at elevated IRQL, so the next transfer must be set up in a DPC.

An indeterminate amount of time elapses between a DPC being requested and when it is run. Do not defer any interrupt servicing to the DPC if data might be lost (e.g., read data being overwritten). If necessary, store any data in the device extension or some other preallocated nonpaged memory.

Using Basic DPCs

The Windows kernel makes it easy to use one DPC routine within a driver. If you want to use more than one DPC routine, check out the next section.

A standard device object contains the necessary KDPC object. However, you need to initialize it using IoInitializeDpcRequest, passing the name of your DPC routine. WdmIo makes this call in Pnp.cpp just after its FDO is created in its AddDevice routine, passing the name of its DPC routine, WdmIoDpcForIsr.

// Initialize our DPC for IRQ completion processing
IoInitializeDpcRequest(fdo, WdmIoDpcForIsr);

Asking for your DPC to be run is very easy — you just call IoRequestDpc, passing an IRP pointer and a context that you want passed to the DPC routine. As shown previously, WdmIo only asks for its DPC routine to be run when it has transferred all bytes or when an error has occurred. In both cases, the current IRP must be completed and the next queued IRP started.

As shown in Listing 17.4, WdmIoDpcForIsr starts by indicating that the transfer has stopped by setting the device extension Timeout field to –1. It then works out how many bytes have been transferred by subtracting the TxLeft field from TxTotal. If the I/O Manager wants the IRP cancelled, store STATUS_CANCELLED in the TxStatus return status field.

WdmIoDpcForIsr then removes the cancel routine, unlocks the device, completes the IRP, and starts the next queued IRP.

Listing 17.4 WdmIo Interrupt Deferred Procedure Call handler

VOID WdmIoDpcForIsr(IN PKDPC Dpc, IN PDEVICE_OBJECT fdo, IN PIRP Irp, IN PWDMIO_DEVICE_EXTENSION dx) {
 dx->Timeout = –1;
 ULONG BytesTxd = dx->TxTotal - dx->TxLeft;
 if (Irp->Cancel) dx->TxStatus = STATUS_CANCELLED;
 DebugPrint("WdmIoDpcForIsr: Status %x Info %d", dx->TxStatus, BytesTxd);
 // Remove cancel routine
 KIRQL OldIrql;
 IoAcqiureCancelSpinLock(&OldIrql);
 IoSetCancelRoutine(Irp, NULL);
 IoReleaseCancelSpinLock(OldIrql);
 // Unlock device and complete
 IRP UnlockDevice(dx);
 CompleteIrp(Irp, dx->TxStatus, BytesTxd);
 IoStartNextPacket(fdo, TRUE);
 // Stop timer calls
 dx->StopTimer = true;
}

DPC Gotchas

Even with this simple DPC for deferred interrupt processing, there are some potential problems that you have to look out for.

The first point to note is that if two or more calls to IoRequestDpc are made before the DPC can be run, the DPC is only run once. Suppose two interrupts occur in quick succession. If each interrupt handler calls IoRequestDpc, you might expect that the DPC is run twice. However, if there is a long time before the DPC is run, then it is only run once. You must cope with this. In this situation, it might be that one interrupt wants a read call completed, while the next wants a write call completed. Use a separate flag in the device extension to indicate each condition. The DPC routine should be prepared to handle both situations. An easier alternative might be to use a different DPC routine for each condition.

For the WdmIo driver, this problem should almost never arise. The only situation I can envision is one in which an IRP is cancelled just after the interrupt handler calls IoRequestDpc.

However, in this case, the late-running DPC routine will find that the IRP has been cancelled, which is correct.

The other potential problems regarding DPCs will only occur in multiprocessor systems.

• A DPC routine is running on one processor as a device interrupt is handled on another.

• When an interrupt handler asks for a DPC to be run, it is run straight away on another processor before the interrupt service routine exits.

• Two or more DPC routines may be running at the same time on different processors.

The main solution to these problems is to use Critical Section routines whenever a DPC routine needs to access fields that an interrupt handler or another DPC use.

Custom DPCs

If you need to use more than one DPC, this is fairly straightforward. These "custom DPCs" are also used for Custom Timers with fine grain time-outs.

Declare a KDPC object in nonpaged memory (e.g., in the device extension). Initialize it using KeInitializeDpc. The DeferredContext parameter is eventually passed to the custom DPC routine.

To ask that your DPC routine be run, call KeInsertQueueDpc from within your interrupt handler. KeInsertQueueDpc returns TRUE if the request was successfully queued. It returns FALSE if the DPC is already in the queue. The SystemArgument1 and SystemArgument2 parameters to KeInsertQueueDpc are eventually passed to your DPC routine. You can use KeRemoveQueueDpc to remove your DPC request from the queue.

Table 17.2 shows the function prototype for your custom DPC routine. There are three context parameters. To be compatible with the basic DPC handler, these should be your FDO, the IRP, and usually the device extension. However, use these as you wish.

Table 17.2 CustomDpc prototype

VOID CustomDpc (IRQL==DISPATCH_LEVEL)
Parameter Description
IN PKDPC Dpc DPC
IN PVOID Context DeferredContext parameter given to KeInitializeDpc
IN PVOID SystemArg1 SystemArgument1 passed to KeInsertQueueDpc (NULL for custom timers)
IN PVOID SystemArg2 SystemArgument2 passed to KeInsertQueueDpc (NULL for custom timers)

Оглавление книги


Генерация: 1.490. Запросов К БД/Cache: 3 / 0
поделиться
Вверх Вниз