In all the device drivers considered so far in the book, we haven't been overly concerned about the thread context in which our driver subroutines have executed. Much of the time, our subroutines run in an arbitrary thread context, which means we can't block and can't directly access user-mode virtual memory. Some devices are very difficult to program when faced with the first of these constraints.
Some devices are best handled by polling. A device that can't asynchronously interrupt the CPU, for example, needs to be interrogated from time to time to check its state. In other cases, the natural way to program the device might be to perform an operation in steps with waits in between. A floppy disk driver, for example, goes through a series of steps to perform an operation. In general, the driver has to command the drive to spin up to speed, wait for the spin-up to occur, commence the transfer, wait a short while, and then spin the drive back down. You could design a driver that operates as a finite state machine to allow a callback function to properly sequence operations. It would be much easier, though, if you could just insert event and timer waits at the appropriate spots of a straight-line program.
Dealing with situations that require you to periodically interrogate a device is easy with the help of a system thread belonging to the driver. A system thread is a thread that operates within the overall umbrella of a process belonging to the operating system as a whole. I'll be talking exclusively about system threads that execute solely in kernel mode. In the next section, I'll describe the mechanism by which you create and destroy your own system threads. Then I'll give an example of how to use a system thread to manage a polled input device.
To launch a system thread, you call PsCreateSystemThread. One of the arguments to this service function is the address of a thread procedure that acts as the main program for the new thread. When the thread procedure is going to terminate the thread, it calls PsTerminateSystemThread, which does not return. Generally speaking, you need to provide a way for a PnP event to tell the thread to terminate and to wait for the termination to occur. Combining all these factors, you'll end up with code that performs the functions of these three subroutines:
1 2 3 4 5 6 7 8 9 |
typedef struct _DEVICE_EXTENSION { ... KEVENT evKill; PKTHREAD thread; }; NTSTATUS StartThread(PDEVICE_EXTENSION pdx) { NTSTATUS status; HANDLE hthread; KeInitializeEvent(&pdx->evKill, NotificationEvent, FALSE); status = PsCreateSystemThread(&hthread, THREAD_ALL_ACCESS, NULL, NULL, NULL, (PKSTART_ROUTINE) ThreadProc, pdx); if (!NT_SUCCESS(status)) return status; ObReferenceObjectByHandle(hthread, THREAD_ALL_ACCESS, NULL, KernelMode, (PVOID*) &pdx->thread, NULL); ZwClose(hthread); return STATUS_SUCCESS; } VOID StopThread(PDEVICE_EXTENSION pdx) { KeSetEvent(&pdx->evKill, 0, FALSE); KeWaitForSingleObject(pdx->thread, Executive, KernelMode, FALSE, NULL); ObDereferenceObject(pdx->thread); } VOID ThreadProc(PDEVICE_EXTENSION pdx) { ... KeWaitForXxx(<at least pdx->evKill>); ... PsTerminateSystemThread(STATUS_SUCCESS); } |
If you had to write a driver for a device that can't interrupt the CPU to demand service, a system thread devoted to polling the device may be the way to go. I'll show you one way to use a system thread for this purpose. This example is based on a hypothetical device with two input ports. One port acts as a control port; it delivers a 0 byte when no input data is ready and a 1 byte when input data is ready. The other port delivers a single byte of data and resets the control port.
In the sample I'll show you, we spawn the system thread when we process the IRP_MN_START_DEVICE request. We terminate the thread when we receive a Plug and Play request such as IRP_MN_STOP_DEVICE or IRP_MN_REMOVE_DEVICE that requires us to release our I/O resources. The thread spends most of its time blocked. When the StartIo routine begins to process an IRP_MJ_READ request, it sets an event that the polling thread has been waiting for. The polling thread then enters a loop to service the request. In the loop, the polling thread first blocks for a fixed polling interval. After the interval expires, the thread reads the control port. If the control port is 1, the thread reads a data byte. The thread then repeats the loop until the request is satisfied, whereupon it goes back to sleep until StartIo receives another request.
The thread routine in the POLLING sample is as follows:
1 2 3 4 5 6 7 8 9 |
VOID PollingThreadRoutine(PDEVICE_EXTENSION pdx) { NTSTATUS status; KTIMER timer; KeInitializeTimerEx(&timer, SynchronizationTimer); PVOID mainevents[] = { (PVOID) &pdx->evKill, (PVOID) &pdx->evRequest, }; PVOID pollevents[] = { (PVOID) &pdx->evKill, (PVOID) &timer, }; ASSERT(arraysize(mainevents) <= THREAD_WAIT_OBJECTS); ASSERT(arraysize(pollevents) <= THREAD_WAIT_OBJECTS); BOOLEAN kill = FALSE; while (!kill) { // until told to quit status = KeWaitForMultipleObjects(arraysize(mainevents), mainevents, WaitAny, Executive, KernelMode, FALSE, NULL, NULL); if (!NT_SUCCESS(status) || status == STATUS_WAIT_0) break; ULONG numxfer = 0; LARGE_INTEGER duetime = {0}; #define POLLING_INTERVAL 500 KeSetTimerEx(&timer, duetime, POLLING_INTERVAL, NULL); PIRP Irp = GetCurrentIrp(&pdx->dqReadWrite); while (TRUE) { // read next byte if (Irp->Cancel) { status = STATUS_CANCELLED; break; } status = AreRequestsBeingAborted(&pdx->dqReadWrite); if (!status) break; status = KeWaitForMultipleObjects(arraysize(pollevents), pollevents, WaitAny, Executive, KernelMode, FALSE, NULL, NULL); if (!NT_SUCCESS(status)) { kill = TRUE; break; { if (status == STATUS_WAIT_0) { status = STATUS_DELETE_PENDING; kill = TRUE; break; } if (pdx->nbytes) { if (READ_PORT_UCHAR(pdx->portbase) == 1) { *pdx->buffer++ = READ_PORT_UCHAR(pdx->portbase + 1); --pdx->nbytes; ++numxfer; } } if (!pdx->nbytes) break; } // read next byte KeCancelTimer(&timer); StartNextPacket(&pdx->dqReadWrite, pdx->DeviceObject); if (Irp) { IoReleaseRemoveLock(&pdx->RemoveLock, Irp); CompleteRequest(Irp, STATUS_SUCCESS, numxfer); } } // until told to quit PsTerminateSystemThread(STATUS_SUCCESS); } |
The StartIo routine that works with this polling routine first sets the buffer and nbytes fields in the device extension; you saw the polling routine use them to sequence through an input request. Then it sets the evRequest event to wake up the polling thread.
You can organize a polling driver in other ways besides the one I just showed you. For example, you could spawn a new polling thread each time an arriving request finds the device idle. The thread services requests until the device becomes idle, whereupon it terminates. This strategy would be better than the one I illustrated if long periods elapse between spurts of activity on the device, because the polling thread wouldn't be occupying virtual memory during the long intervals of quiescence. If, however, your device is more or less continuously busy, the first strategy might be better because it avoids repeating the overhead of starting and stopping the polling thread.