dunniu
驱动老牛
驱动老牛
  • 注册日期2005-05-26
  • 最后登录2016-01-09
  • 粉丝0
  • 关注0
  • 积分555分
  • 威望0点
  • 贡献值0点
  • 好评度997点
  • 原创分0分
  • 专家分0分
阅读:1293回复:0

DriverWorks Object Model 1

楼主#
更多 发布于:2005-07-01 09:43
  This description discusses the architecture of DriverWorks version 1.20.
An updated paper that describes Plug and Play support and other features added in version 2.00 is available in a separate white paper.
The DriverWorks object model reflects the architecture of Windows NT, and extends the object orientation of that system by providing additional classes built on system services. The goal of DriverWorks is to provide objects that closely model the underlying operating system, and at the same time provide powerful methods to simplify and clarify the task of developing kernel mode drivers.
At its core, NT is an object-oriented operating system. It maintains a directory of various kinds of objects, and uses common methods to manipulate those objects. This enables the system to implement security and maintain reference counts isomorphically across a broad range of object types. The effect is to improve robustness and reduce the size of the operating system.
The problem is that the DDK obscures much of the system's inherent object orientation. The C interface does not express the object nature of the architecture. Interfaces that manipulate a single type of object are not grouped together in a way that presents objects as coherent entities. As a result, the developer is forced to work in a procedural mode, and cannot easily leverage the object nature of the system.
The DriverWorks Object Model reinforces the object architecture of Windows NT. It presents a model that illuminates system objects, rather than flattening them into a procedural interface. The result is a greater clarity of design, greater programmer productivity, and a more powerful development framework.
It is much easier to learn to write a driver with DriverWorks than with the raw DDK. However, even developers experienced in developing device drivers with the DDK will find that the DriverWorks model simplifies many tasks, making code much easier to develop, read, and maintain.
The Driver Object
Device Objects
Objects for Controlling Hardware
Memory Objects
Dispatcher Objects
Containers
Configuration Query Objects
Controllers
Driver Managed Queues
Event Log Entries
Heaps
Image Sections
Interrupt Request Level (IRQL)
I/O Request Objects
Lower Devices
Queues
Resource Requests and Assignments
Spin Locks

1.       The Driver Object
The Driver Object is implemented by class KDriver.
The I/O Manager uses driver objects to create a structure for managing devices. Each driver object is responsible for managing one or more device objects that it creates. An I/O request made by a user subsystem causes a request to be sent to a particular device object. The I/O Manager, a kernel component, determines which driver object is responsible for the targeted device, and passes the request object to a method exported by that driver. Driver objects have additional methods for initialization and shutdown.
Initialization
The driver writer derives a class from KDriver to form the basic framework of a driver. The system creates a driver object for each driver that is loaded. The driver subclass must supply an initialization entry point that the system calls. This entry point is always named DriverEntry.
The initialization phase may have several steps. In a typical case, the driver first queries for the presence of devices that it must control. In order to support those devices, the driver may require system resources such as interrupts, DMA channels, or address ranges. Depending on the driver's design, these resources may be claimed by the driver object, or each device object created by the driver may be responsible for claiming the resources that it needs.
After probing the system for configuration and resource information, a driver creates device objects that correspond to actual devices under its control. Device objects are typically created during initialization, although this is not required.
Reinitialization
Besides DriverEntry, a driver object may export an additional initialization method to be called at a later stage of the system's initialization process. Drivers do this in order to reduce sensitivity to load order. A driver may depend on the presence of other drivers, and may require that those drivers initialize first. To some degree, a driver can control this by means of installation parameters that are stored in the registry, but this is not always sufficient.
The driver object exports a method that DriverEntry calls to set up a callback to a reinitialization method. After the system makes a first pass of initialization for all drivers, it calls any reinitialization methods that drivers have registered. The reinitialization method may request additional callbacks.
Dispatching Requests
Given an I/O request object intended for a particular device object, the I/O Manager delivers that request to the driver object that created the targeted device object. The default action of the driver object is to route the request to the targeted device as quickly as possible. A driver subclass can override this default behavior. For example, the driver object may need to compile statistics of how many requests have been processed. To do so, it overrides a dispatch filter method that can view all I/O requests coming into the driver. The driver object's dispatch filter can examine or process the request before choosing to pass it on the targeted device object. This mechanism is not to be confused with the concept of a filter driver which can filter requests that are targeted to devices under the control of other drivers.
Unloading
A well behaved driver indicates to the system that it can be unloaded when it is no longer needed. To do so, it simply exports an unload method. Certain kinds of drivers that are critical to the operation of the computer, such as the keyboard or display, do not export unload methods, because they are always needed.
A driver object is responsible for destroying any objects that it has created when the system calls its unload entry point, including all device objects. Invoking the device destructors should, in turn, cause device objects to destroy any objects they have created. If a driver claimed system resources with a resource request during initialization, its unload method should release them.

2.       Device Objects
Device objects are implemented by class KDevice.
Because they provide a representation of the I/O capabilities that are available on the computer, device objects are central to the architecture of the I/O subsystem. Device objects comprise the set of logical I/O targets with which applications may exchange information. The system supplies APIs (such as CreateFile, ReadFile, and WriteFile) that applications use to send requests to device objects indirectly via the I/O Manager. Although the I/O Manager delivers the request first to the driver responsible for the device, it is the device object that does the bulk of the processing. Device objects are collectively responsible for providing a set of abstract services (e.g. read, write, and control functions) that provide access to the available hardware.
For simplicity, throughout this discussion. references to "device" mean "device object" except when the term "device" might be construed to mean "physical device."
Most devices are named, although anonymous devices are allowed. A device can communicate with a second device if the name of the second device is known.
In order for an application to communicate with a device, the device must be referenced by at least one symbolic link. A symbolic link is essentially a string that identifies the device to user mode code. For a give device, there may be multiple symbolic links that point to it, or there by be none at all. Each link is either protected or unprotected. If a link is unprotected, user mode code can change the device object that the link references.
Every kernel mode driver implements one or more subclasses of the system device object. For each subclass, the driver writer determines the behavior of the class by overriding virtual methods, including those methods that process the various types of IRPs.
In order for a device object to fulfill its IRP processing responsibility, it must have the ability to control the hardware that it represents to the system. In order to maintain compatibility across multiple hardware architectures, all low-level access to hardware is performed through a set of hardware objects which are in turn defined in terms of the system Hardware Abstraction Layer (HAL). These hardware objects, such as I/O port objects, interrupt objects, and DMA objects, are described later on.
The device object is primarily an IRP target. The I/O Manager delivers requests to the device driver object, and the driver object automatically dispatches it to the device. The system does not attempt to abstract any particular capabilities of physical devices. That is the job of subclasses of the system device object that are implemented in kernel mode drivers.
Another way to think of it is to consider a device as having an upper edge and a lower edge. Class KDevice and subclasses thereof define the upper edge of the device. In general, this is an IRP based interface, although that is not always the case. A subclass of KDevice may implement processing for a set of IRPs specific to a class of devices, or it may implement an upper edge that has a entirely different interface.
The lower edge of a device is either a direct interface to the hardware or an interface to another device. Direct control of hardware is done with the aid of the HAL. Interfacing to lower devices employs a "lower device" object, which is discussed below. A device object may have one or more embedded lower devices.
The important architectural consideration here is to notice that the upper edge and lower edge are intentionally decoupled. Class KDevice provides the basis for defining the upper edge of a device, but has no bearing on the lower edge. The lower edge is determined by the objects which the driver writer embeds in the subclass of KDevice. The decoupling of the upper and lower edges enables the object model to flexibly support the layered nature of the system.

3.       Objects for Controlling Hardware
The object model provides objects that assist in the control of hardware devices. A kernel mode driver creates these objects to represent the physical attributes of the devices it supports. These objects include ports and memory ranges, interrupts, and objects that support DMA.
Peripheral Address Objects
Peripheral address objects are implemented by the following classes: KPeripheralAddress, KIoRange, KMemoryRange, KIoRegister, and KMemoryRegister.
In order to understand the abstraction of peripheral bus addresses, it is necessary to understand how the system views physical addresses. In the simplest machine model, there is a single address space which is common to both the processor and programs. That is, the address that a program accesses in software is the same as the address that is output on the processor's address lines. In a more sophisticated model, there are two address spaces, namely physical and virtual. In this model, the address that a program uses is different than the address represented on the processor's address lines, as a result of a translation performed to make discontiguous physical memory appear as contiguous to software.
NT extends this concept further. In modern PC designs, there are multiple buses in the system. Typically, the CPU, external cache, and main memory reside on a high speed bus whose architecture is specific to the processor. The CPU bus is bridged to a standardized local bus, on which reside high speed devices such as the display. The local bus is bridged to secondary local buses and to external buses that host storage devices, network adapters, and communications ports. It is the multiple bus architecture of modern PCs that gives rise to a more complex address space model.
In order to access devices residing on buses other than the CPU bus, the system hardware must create a mapping between the CPU bus address and the corresponding address on the bus where the target device resides. Therefore, the address that the processor puts on its address lines and the address the target device is configured to decode may be different.
Some peripheral buses supply separate address spaces for I/O and memory operations. Similarly, some processors (such as Intel x86) recognize this distinction. The mapping that the system makes from a peripheral bus to the processor bus must take this into consideration. An address in the memory space of a peripheral bus always maps to a memory address on the CPU bus. However, an I/O address on the peripheral bus does not always map to a I/O address on the CPU bus. This is obviously the case if the CPU does not recognize an I/O space.
The system does not require the device driver writer to know the details of how each bus in the system is mapped to the processor's address space, but it does require taking into account that such a mapping may occur. For example, suppose it is learned from the registry that device D is on bus B of bus type I, and responds to I/O address A on that bus. In order to access that device, the driver must first request the system to map that device address information to a physical address on the CPU bus. The result could be in either the memory space or the I/O space on the CPU bus, depending on how the peripheral bus is hosted. If it is in memory space, a driver that performs programmed I/O to that device must make an additional request to the system in order to set up page tables to access that physical address.
In the DriverWorks object model, peripheral address objects hide the operations that configure the system to support a particular range of ports or memory registers of a physical device. To create a bus address object for a particular range of addresses, the following are required:
the bus type and instance number of that type
the peripheral address space to be accessed, either memory or I/O
the start address as decoded by a device on that bus (this may be a 64-bit value)
the size of the address range
Subclasses of the address object export methods for reading and writing the locations specified during construction. These methods use system services appropriate for the address space of the peripheral bus mapped by the base class. These platform dependent services ensure that caches are properly flushed.
A word about nomenclature. This documentation uses register to refer to a location on the device. A register may be in either the I/O space or the memory space of the peripheral bus. Sometimes register is qualified as being a memory register, or an I/O register. In both cases, it is the address space of the peripheral bus on which the device resides that determines what kind of a register it is. A register of a device in the I/O space of its peripheral bus is still an I/O register even if it is mapped to the memory space of the CPU bus. Sometimes port is used to mean register. The more neutral register is used to avoid confusion.
Interrupt Objects and Deferred Procedure Calls
Interrupt objects are implemented by class KInterrupt. Deferred procedure call objects are implemented by class KDeferredCall.
The system abstracts interrupts from I/O devices in a manner that can be implemented in a platform independent fashion. The attributes of an interrupt object include the type and instance of the bus on which the interrupt is asserted, the priority level on that bus, the bus vector number, and the mode, either latched or level sensitive. On some buses, such as ISA, bus priority level and bus vector are mapped to the same value. It is possible for more than one interrupt object to have the same attributes, as long as the drivers that create those objects specify that the interrupt is sharable. The system associates a specific IRQL with each interrupt object, depending on the bus and bus level of the interrupt. A driver that creates multiple interrupt objects can arrange for all of them to be associated with the same IRQL.
The interrupt object exports methods to connect and disconnect an interrupt service routine, or ISR. Once an interrupt object is connected, the system invokes the ISR whenever the interrupt occurs. The object model allows the ISR to be a standalone function or a method of any class defined or derived by the driver developer.
The ISR cannot run until a processor is available at an IRQL less than the IRQL associated with the corresponding interrupt object. Similarly, the ISR cannot be interrupted unless an interrupt of higher priority occurs.
Because it runs at device IRQL, an ISR must execute as quickly as possible. Running at DIRQL for too long may interfere with the operation of other devices by blocking out interrupts. If an interrupting device requires significant processing time to service the interrupt, then as much as possible of that processing should be executed below device IRQL. A second reason for deferring to a lower IRQL is that the processing of the interrupt may require system services that are not callable at DIRQL.
The system provides Deferred Procedure Call objects (DPCs) to enable drivers to continue the processing of an interrupt at a lower IRQL, namely DISPATCH_LEVEL. The construction of a DPC requires the address of a callback function to be invoked by the system. If an ISR requires the use of a DPC, the driver must construct the DPC prior to connecting the interrupt object.
The operation of a typical ISR follows a simple format. It first queries its hardware to verify that the device is actually requesting service. It then executes code to acknowledge the interrupt and put the interrupting device in a known state. Next, it queues a previously initialized DPC object, and returns. The kernel services the DPC queue whenever the IRQL of some processor is below DISPATCH_LEVEL. This means that on a system with a single CPU the DPC cannot execute until sometime after the ISR returns. On a multiprocessor system, it may execute in parallel.
If the callback function of a DPC needs to access variables that are modified by the ISR, then the callback function must take measures to ensure that its access to those variables is atomic. If the callback function failed to do this, then it would risk using data that was temporally inconsistent. As a simple example, suppose the DPC callback function reads the value of some variable, and increments it, and stores it back to memory. If the ISR modifies the variable after it has been read but before it is written, then the variable has been corrupted. This is an issue not only for DPC callback functions, but for any code in the driver that executes at a lower IRQL than that associated with the interrupt object.
To deal with the issue of atomic data access, the interrupt object exports a method for synchronization. This method takes the address of a function as a parameter, and synchronously calls that function in a way that prevents the associated ISR from executing (on any processor) while the function is executing. The function called by the synchronization runs at the IRQL associated with the interrupt object.

Objects for DMA
Direct Memory Access (DMA) is a transfer of data between system memory and a device, distinguished by the fact that the bus cycles that effect the transfer are not generated by a CPU. Instead, the bus cycles are generated either by the device itself, or by a DMA controller on the system board.
Devices that themselves generate the bus cycles for a transfer are referred to as bus masters, while devices that rely on the system DMA controller are referred to as slaved devices. Transfer to and from slaved devices are also referred to as system DMA transfers.
The memory region that acts as the source or target of a DMA transfer is normally physically contiguous. This is necessary because the system DMA controller and some bus masters can only be programmed with a single physical address. Some bus masters can be programmed with multiple physical addresses, and this capability is known as supporting scatter/gather. A bus master that supports scatter/gather does not require the memory region for a DMA transfer to be physically contiguous.

Common Buffer Objects
Common buffer objects are implemented by class KCommonDmaBuffer.
A common buffer is simply a range of physically contiguous virtual memory that is shared between a device and its driver. The storage is allocated from system memory, and is guaranteed to be accessible by the device and the driver at all times. The attributes of a common buffer include its size, virtual address, and logical address. Driver software uses the virtual address to access the buffer. The logical address is the physical address that a device on a peripheral bus must assert in order to access the buffer.
Construction of a common buffer requires a DMA adapter object. In order to provide a buffer accessible by a device, the system requires the characteristics of the device and the bus on which it resides. Like any mapping of a peripheral bus to contiguous virtual memory, a common buffer may consume scare system resources.
A driver creates a common buffer object to deal with devices that can initiate DMA without direction from the driver. This applies to many bus master devices, and to slaved devices that exploit the auto-initialize capability of the system DMA controller. In a typical scheme, a bus master writes data into the common buffer, and asserts an interrupt to signal the driver of its presence. The driver and the device can both access the region without re-mapping the entire DMA transfer for each exchange of information.

DMA Adapter Objects
DMA adapter objects are implemented by class KDmaAdapter.
A DMA adapter object represents the physical resources that a device requires for DMA transfers. The object model uses the same object for both bus master and slaved DMA devices, and this distinction is an attribute of the object specified to its constructor. For both cases, the object is created for a device on a particular bus type and bus number. For bus master devices, support for scatter/gather is indicated by a variable in the adapter object. For slaved devices, the system DMA channel on which the device transfers data is an adapter attribute.
DMA adapter objects resemble controller objects in that they export a method for serialization of access. When a device object needs to transfer data, it calls this method, supplying the address of a function to invoke when the object is no longer owned. If the adapter is not owned at the time of the call, it invokes the supplied function immediately. If the adapter is busy, the function address is placed on a callback queue embedded in the adapter object. When another device object releases the adapter object, the adapter checks its queue to see if there are other devices that have requested access. If so, it dequeues the next function address and calls it, thereby granting ownership of the adapter to the called device.
If the adapter object was created for a device that does not support scatter/gather, then the adapter object provides access to resources that enable mapping of a limited range of bus space to contiguous virtual memory. Recall from the earlier discussion of multiple bus architectures that this mapping depends on how many buses are implemented on a particular computer, and the relationship between them. Some computers have mapping registers that map a contiguous range of a peripheral bus to an arbitrary set of memory pages on the physical CPU bus. For computers that lack such registers, the mapping is effected by using a range of contiguous physical memory as an intermediate buffer for the transfer. In either case, the mapping is transparent to kernel mode drivers. A mapping from a device on a peripheral bus to contiguous virtual memory consumes scare resources regardless of the physical implementation of the mapping.

DMA Transfer Objects
DMA transfer objects are implemented by class KDmaTransfer.
DMA transfer objects manage the transfer of data between memory and a device. A DMA adapter object for the device must be available. The adapter object informs the transfer object of the type of transfer to be made, i.e. whether it is slaved or bus master, scatter/gather or not, which DMA channel is to be used, etc.
The purpose of the transfer object is to oversee the transfer, calling out to external functions as needed to manipulate the physical device. The transfer object acquires control of the associated adapter object, breaks up the transfer into segments that conform to the limits of the hardware and available buffers, and flushes caches and buffers as necessary. The client of a transfer object supplies code to pass physical memory addresses to bus masters, to tell slaved devices to start requesting DMA cycles, and to detect when a segment of a transfer has completed. The executive transfer object does most of the work.
The transfer object exports a method to initiate a transfer. This method takes as parameters the direction of the transfer, a memory object that describes the buffer to or from which the transfer is to take place, and the address of a function to be called each time the transfer object has set up the next segment of the transfer. When the transfer object calls this function, the client causes the device to start transferring the current segment. When the client later detects completion of the segment, usually by an interrupt, it calls a method of the transfer object that continues with the next segment. This is repeated until the client callback function detects that the transfer is complete, and calls the transfer's termination method.

4.       Memory Objects
Memory objects are implemented by the class KMemory.
A memory object is a description of a region of virtual memory. Its attributes include the virtual address of the page where the region starts, the offset on that page of the start of the region, the size of the region, the process in which the virtual address is valid, a system virtual address of the region is one has been mapped, and the physical page numbers for each page of the region when it is locked into memory. Memory objects are also referred to as Memory Descriptor Lists, or MDLs.
Memory objects are referred to by IRPs for devices that do direct I/O. When such a device performs a read or write, the I/O Manager locks down the user buffer and creates a memory object to describe it. It passes the address of the memory object to the device in an IRP.
Memory objects are also required by DMA transfer objects to initiate a transfer.

5.       Dispatcher Objects
Dispatcher objects are implemented by the class KDispatcherObject.
The system supports a class of objects for synchronization and scheduling, called dispatcher objects. Despite the nomenclature, these have nothing to do with DISPATCH_LEVEL nor dispatching of IRPs. Rather, they are objects that interact with scheduler.
Applications can create dispatcher objects to which kernel mode drivers can gain access. Therefore, it is possible to share dispatcher objects between drivers and applications, which is useful for signaling and synchronizing user mode code with kernel mode code.
All dispatcher objects have the property of being in one of two states: signaled or not signaled. Furthermore, all dispatcher objects export a method which causes its callers to block execution while the object is in the not signaled state. This method is named Wait.
When a thread calls method Wait of a dispatcher object that is not signaled, the thread suspends. When the object becomes signaled, the thread may return from method Wait and continue execution. Each of the various kinds of dispatcher objects behave somewhat differently. The dispatcher objects are: events, semaphores, mutexes, timers, and system threads.
Events
Event objects are implemented by class KEvent.
The simplest dispatcher object is the event. An event can be set, cleared, queried, and waited on.
Mutexes
Mutex objects are implemented by class KMutex.
Drivers use mutex objects to protect sections of code or data objects from simultaneous access by multiple threads. Once a thread gains ownership of a mutex, any other thread that attempts to gain ownership of that mutex is blocked until the owner releases it.
A driver that uses multiple threads often needs a mechanism to prevent multiple threads from accessing sensitive resources simultaneously. Operations that leave data objects in a temporary transient state must be performed atomically in order to prevent corruption. For example, insertion of an object into a queue may require manipulation of several related pointers. Since the states of the pointers are related, accessing them in a transient state would corrupt the queue. By forcing threads to acquire a mutex before accessing the queue, a driver ensures the integrity of the queue.
Mutexes have the attribute that they can be recursively acquired. That is, a thread that owns a mutex can enter it again. However, the thread must release the mutex exactly once for each time it acquires it.
Semaphores
Semaphore objects are implemented by class KSemaphore.
The attribute that distinguishes semaphores from the other dispatcher objects is a count. The state information for a semaphore includes a count which increments when the semaphore is signaled and decrements when a wait is satisfied. When the count is greater than zero, the semaphore is in the signaled state.
A semaphore's count enables it to perform useful synchronization operations. Suppose a driver maintains a pool of n buffers, and it has a dedicated set of threads that require exclusive access to one of the buffers in order to run. The driver can then initialize a semaphore with a count of n, and each thread can wait on the semaphore to acquire a buffer. When no buffer is available, the threads block.
Here is another example. Suppose a driver queues I/O requests (IRPs) to a dedicated thread. The driver creates a semaphore whose count corresponds to the number of IRPs in the queue. Initially, the count is zero and the thread is blocked. As IRPs arrive, the driver queues them and signals the semaphore, thereby unblocking the thread. The thread dequeues the request, processes it, and waits on the semaphore again. By ensuring that the semaphore count is equal to the number of threads in the queue, the driver ensures that the thread runs only when there is work to be done.
Timers
Timer objects are implemented by class KTimer.
Timer objects enable kernel mode drivers and system threads to arrange for notification when a specified time interval elapses. Like all dispatcher objects, a timer is either in the signaled or non-signaled state. When first set, it is non-signaled; when the timer expires, it becomes signaled. In addition to methods inherited by all dispatcher objects, the class has a method to set the timer, to cancel it, and to query its state.
Timed Callbacks
Timed callback objects are implemented by class KTimedCallback.
Timed callback objects are derived from timer objects. The constructor takes as a parameter the address of a function to call when the timer expires. The object model allows this function to be a member of any class defined or derived by the developer of the driver. A driver may cancel a timed callback while it is pending execution. The object uses a DPC object internally to invoke the callback function.
System Threads
System thread objects are implemented by class KSystemThread.
Kernel mode drivers can create system thread objects. A thread object is an execution context under the control of the system scheduler. Threads can be used to implement continuous processing at the system level, rather than depending on external events or calls from the user subsystem to instigate driver actions.
Threads are appropriate for drivers that have long term tasks to carry out, such as polling a slow device that cannot interrupt. By utilizing a thread, the system avoids borrowing time quanta from an arbitrary thread. Instead, execution of the system thread can be balanced by the scheduler according to the load on the system.
When the thread is active, it is not signaled. When it terminates, it becomes signaled. This allows a waiter to take action when the thread terminates.
A system thread exploits the fact that its IRQL is PASSIVE_LEVEL. This removes restrictions on which system services it may call, and enables it to access paged code and data. It also enables use of synchronization objects such as events, semaphores, mutexes, and timers.
游客

返回顶部