All modern operating systems have a subsystem called the device manager. The device manager is responsible for detecting and managing devices, performing power management, and exposing devices to userspace. Since the device manager is a crucial part of any operating system, it's important to make sure it's well designed.
Device drivers allow user applications to communicate with a system's devices. They provide a high-level abstraction of the hardware to user applications while handling the low-level device-specific I/O and interrupts. Device drivers can be implemented as loadable kernel modules (for a Monolithic Kernel) or user-mode servers (for Microkernels).
The main role of the device manager is detecting devices on the system. Usually, devices are organized in a tree structure, with devices enumerating their children. Device detection should begin with a "root bus driver". On x86 systems, the root bus driver would use ACPI. The root bus driver sits at the root of the device tree. It detects the buses present on the system as well as devices directly connected to the motherboard. Each bus is then recursively enumerated, with its children continuing to enumerate their children until the bottom of the device tree is reached.
Each device that is detected should contain a list of resources for the device to use. Examples of resources are I/O, memory, IRQs, DMA channels, and configuration space. Devices are assigned resources by their parent devices. Devices should just use the resources they're given, which provides support for having the same device driver work on different machines where the resource assignments may be different, but the programming interface is otherwise the same.
Drivers are loaded for each device that's found. When a device is detected, the device manager finds the device's driver. If not loaded already, the device manager loads the driver. It then calls the driver to initialize that device.
How the device manager matches a device to a device driver is an important choice. The way devices are identified is very bus specific. On PCI, a device is identified through a combination of its vendor and device IDs. USB has the same scheme as PCI, using a vendor and product ID. ACPI uses PNP IDs to identify devices in the ACPI namespace. With this information, it's possible to build a database using matching IDs to drivers. This information is best stored in a separate file.
Inter-Process Communication (IPC)
The device manager needs to implement some form of IPC between it and device drivers. IPC will be used by the device manager to send I/O requests to device drivers, and by device drivers to respond to these requests. It is usually implemented with messages that contain data about the request, such as the I/O function code, buffer pointer, device offset, and buffer length. To respond to these I/O requests, every device driver needs dispatch functions used to handle each I/O function code. Each device needs a queue of these IPC messages for it to handle. On Windows NT, this IPC is done with I/O Request Packets.
There are two main types of I/O: synchronous I/O and asynchronous I/O. Synchronous I/O sends an I/O request and then puts the current thread to sleep until the I/O completes. Asynchronous I/O just sends the I/O request and then returns. I/O completion is reported asynchronously using a callback. Asynchronous I/O improves the efficiency of the system by allowing allowing for the program execution to continue while I/O is performed. It also allows for multiple I/O requests to be started and then handled in the order they complete, not the order they execute. However, this comes at the cost of making programming more complex than using synchronous I/O.
Internally, an operating system should use asynchronous I/O for all of its I/O requests. I/O requests are sent to drivers, and then the function that sent them immediately returns. Eventually, the I/O request will be handled. Once it completes, it returns through the driver stack and finally notifies the application of I/O completion. It can do this using callbacks, signals, or completion queues.
Synchronous I/O can simply be implemented as a special case of asychronous I/O. Just like with asynchronous I/O, an I/O request is sent to the driver, but instead of returning, the thread goes to sleep. Once the I/O completion event is queued, the thread will wake up and execute the callback before returning.
The device manager also performs power management. Power management is a feature of hardware that allows for the power consumption of the system and devices to be controlled. Each device managed by the device manager should provide functions to set their power state. For power management support, all systems require a power management driver that controls the system power. On x86, this is done through ACPI. Each device also needs to support power management.
The device manager needs to respond to power management events. Power management events can come from two sources: the user or the system. User-generated power management events are created by user mode applications. They are system-wide events for shutting down, rebooting, hibernating, or putting the system to sleep. When the device manager receives a system-wide power management event, it sets the power state of the system.
System-generated power management events are events that come from the system hardware. Examples of system-generated power management events are plugging/unplugging an AC adapter or closing/opening the lid of a laptop. The device manager takes the appropriate action in response to the event.
Once the kernel interfaces for device drivers are complete, one also needs to figure out how to expose devices to userspace. Most Unix-based operating systems expose devices through the filesystem tree. When devices are placed in the filesystem tree, there is a directory (usually /dev) containing special files that represent devices. The advantage of placing devices in the filesystem tree is that devices can be treated as files, meaning they can be read from or written to. Windows NT does not expose devices through the filesystem tree. Instead, there is an internal namespace of objects, through which devices can be found and accessed similarly to files.
No matter how devices are exposed, the functions that are provided for devices must be decided on as well. Both Unix-based operating systems and Windows NT treat devices like files, meaning their functions are open(), close(), read(), and write(). However, it was soon realized that this API would not be adequate for device functions that don't fit into these functions, like setting the graphics mode of a video card. For this purpose, a new syscall called ioctl() was developed, that allows a device to have special functions. However, this is by no means the only way to call device functions.
Existing Driver Interfaces
An operating system doesn't need to implement its own driver interface. A few driver interfaces have already been programmed with the intent of being integrated into operating systems. These driver interfaces can be implemented instead of a native driver interface, on top of a native driver interface, or along with a native driver interface.
Uniform Driver Interface
- Main article: Uniform Driver Interface
Project UDI is a driver interface intended to be binary portable or source portable when running on different CPU architectures. It is not very widespread (however, neither are EDI or CDI); for example, due to philosophical concerns, Linux did not embrace UDI. However, several members of the community are striving to popularize UDI again since it would be of a huge benefit to hobby operating systems. You are strongly encouraged to participate by implenting a UDI environment and writing drivers.
Extensible Driver Interface
- Main article: Extensible Driver Interface
EDI is a driver interface intended to be source code portable and fairly simple in implementation (also, limited in functionality), so that hobby small hobby OSes may share driver code base.