Chapter 8
OSKit Device Driver (OS Environment) Framework

8.1 Introduction

Note: the framework’s obsolete name as “device driver framework” is historical baggage from its first client component, imported device drivers. Today, a more accurate name would be the “OS Environment” framework. It provides the API and glue used by all “large” encapsulated components (devices, networking, filesystems) imported from other operating systems. We’ll change the name and documentation in the future.

A note on organization and content: this chapter really contains three quite separate parts: a general narrative about execution models, some very sketchy documentation of the “up-side” device interfaces, and the bulk covers the “osenv” interfaces. A later chapter (17) talks sketchily about the default implementation of the interfaces found here.

The OSKit device driver framework is a device driver interface specification designed to allow existing device drivers to be borrowed from well-established operating systems in source form, and used unchanged to provide extensive device support in new operating systems or other programs that need device drivers (e.g., hardware configuration management utilities). With appropriate glue, this framework can also be used in an existing operating system to augment the drivers already supported by the OS. (We believe it’s possible to extend the framework to accomodate drivers in binary form.) This chapter describes the device driver framework itself; other chapters later in this document describe specific libraries provided as part of the OSKit that provide driver and kernel code implementing or supporting this interface.

The primary goals of this device driver framework are, in order from most to least important:

  1. Breadth of hardware coverage. There is a tremendous range of common hardware available these days, each typically supporting its own device programming interface and requiring a special device driver. Device drivers for a given device are generally only available for a few operating systems, depending on how well-established the particular device and OS is. Thus, in order to achieve maximum hardware coverage, the framework must be capable of incorporating device drivers originally written for a variety of different operating systems.
  2. Adaptability to different environments. This device driver framework is intended to be useful not only in traditional Unix-like kernels, but also in operating systems with widely different structures, e.g., kernels written in a “stackless” interrupt model, or kernels that run all device drivers as user mode programs, or kernels that do not support virtual memory.
  3. Ease-of-use. It should be reasonably easy for an OS developer to add support for this framework to a new or existing OS. The set of support functions the OS developer must supply should be kept as small and simple as possible, and there should be few “hidden surprises” lurking in the drivers. In situations where existing device drivers supported by the OSKit have special requirements that the OS must satisfy in order to use them, these requirements are clearly documented in the relevant chapters.
  4. Performance. In spite of the above constraints, device drivers should be able to run under this framework with as little unnecessary overhead as possible. Performance issues are discussed further in Section 8.5.

Since the most important goal of this framework is to achieve wide hardware coverage by making use of existing drivers, and not to define a new model or interface for writing drivers, it is somewhat more demanding and restricting in terms of OS support than would be ideal if we were writing entirely new device drivers from scratch. Other device driver interface standards, such as SVR4’s DDI/DKI and UDI [1], are not designed to allow easy adaptation of existing drivers; instead, they are intended to define and restrict the interfaces and environment used by new drivers specially written for those interfaces, so that these new drivers will be as widely useful as possible. For example, UDI requires all conforming drivers to be implemented in a nonblocking interrupt model; this theoretically allows UDI drivers to run easily in either process-model or interrupt-model kernels, but at the same time it eliminates all possibility of adapting existing traditional process-model drivers to be UDI conformant without extensive changes to the drivers themselves. Hopefully, at some point in the future, one of these more generic device driver standards will become commonplace enough so that conforming device drivers are available for “everything”; however, until then, the OSKit device driver framework takes a compromise approach, being designed to allow easy adaptation of a wide range of existing drivers while keeping the primary interface as simple and flexible as possible.

8.1.1 Full versus partial compliance

Because the range of existing drivers to be adopted under this framework is so diverse in terms of the assumptions and restrictions made by the drivers, it would be impractical to define the requirements of the framework as a whole to be the “union” of all the requirements of all possible drivers. For example, if we had taken that approach, then the framework would only be usable in kernels in which all physical memory is directly mapped into the kernel’s virtual address space at identical addresses, because some drivers will not work unless that is the case. This restriction would make the framework completely unusable in many common OS environments, even though there are plenty of drivers available that don’t make the virtual = physical assumption and should work fine in OS environments that don’t meet that requirement.

For this reason, we have defined the framework itself to be somewhat more generic than is suitable for “all” existing drivers, and to account for the remaining “problematic” drivers, we make a distinction between full and partial compliance. A fully compliant driver is a driver that makes no additional assumptions or requirements beyond those defined as part of the basic driver framework; these drivers should run in any environment that supports the framework. A partially compliant driver is a driver that is compliant with the framework, except that it makes one or more additional restrictions or requirements, such as the virtual = physical requirement mentioned above. For each partially-compliant driver provided with the OSKit, the exact set of additional restrictions made by the driver are clearly documented and provided in both human- and machine-readable form so that a given OS environment can make use of the framework as a whole while avoiding drivers that will not work in the environment it provides.

8.2 Organization

In a typical OS environment in which all device drivers run in the kernel, Figure 8.1 illustrates the basic organization of the device driver framework.


PIC

Figure 8.1: Organization of OSKit Device Driver Framework in a typical kernel

The heavy black horizontal lines represent the actual interfaces comprising the framework, which are described in this chapter. There are two primary interfaces: the device driver interface (or just “driver interface”), which the OS kernel uses to invoke the device drivers; and the driver-kernel interface (or just “kernel interface”), which the device drivers use to invoke kernel support functions. The kernel implements the kernel interface and uses the driver interface; the drivers implement the driver interface and use the kernel interface.

Chapter 17 describes a library supplied as part of the OSKit that provides facilities to help the OS implement the kernel interface and use the driver interface effectively. Default implementations suitable in typical kernel environments are provided for many operations; the OS can use these default implementations or not, as the situation demands.

Several chapters in Part IV describe device driver sets supplied with the OSKit for use in environments supporting the OSKit device driver framework. Since the Flux project is not in the driver writing business, and does not wish to be, these driver sets are derived from existing kernels, either unchanged or with as little code modified as possible so that the versions of the drivers in the OSKit can easily be kept up-to-date with the original source bases from which they are derived.

8.3 Driver Sets

Up to this point we have used the term “device driver set” fairly loosely; however, in the context of the OSKit device driver framework, this term has a very important, specific meaning. A driver set is a set of related device drivers that work together and are fairly tightly integrated together. Different driver sets running in a given environment are independent of each other and oblivious to each other’s presence. Drivers within a set may share code and data structures internally in arbitrary ways; however, code in different driver sets may not directly share data structures. (Different driver sets may share code, but only if that code is “pure” or operates on a disjoint set of data structures: for example, driver sets may share simple functions such as memcpy.)

Of course, the surrounding OS can maintain shared data structures in whatever way it chooses; this is the only way drivers in different sets can interact with each other. For example, if a kernel is using a FreeBSD device driver to drive one network card and a Linux driver to drive another, then the kernel can take IP packets coming in on one card and route them out through the other card, but the network device drivers themselves are completely oblivious to each other’s presence.

Some driver sets may contain only a single driver; this is ideal for modularity purposes, since in this case each such driver is independent of all others. Also, given some effort on the part of the OS, some multi-driver sets can be “split up” into multiple single-driver sets and used independently; Section 8.4.1 describes one way this can be done.

In essence, each driver set represents an “encapsulated environment” with a well-defined interface and a clearly-bounded set of state. The concept of a driver set has important implications throughout the device driver framework, especially in terms of execution environment and synchronization; the following sections describe these aspects of the framework in more detail.

Note that currently all “osenv” code in the same address space is essentially a single driver set. We are planning on changing this to allow drivers to be independant from each other. Currently, the only way to achieve this is to run them in separate address spaces.

8.4 Execution Model

Device drivers running in the OSKit device driver framework use the interruptible, blocking execution model, defined in Section 2.5, and all of the constraints and considerations described in that section generally apply to OSKit device drivers. However, there are a few execution model issues specific to device drivers, which are dealt with here.

8.4.1 Use in out-of-kernel, user-mode device drivers

In some situations, for reasons of elegance, modularity, configuration flexibility, robustness, or even (in some cases) performance, it is desirable to run device drivers in user mode, as “semi-ordinary” application programs. This is done as a matter of course by some microkernels. There is nothing in the OSKit device driver framework that prevents its device drivers from executing in user mode, and in fact the framework was deliberately designed with support for user-mode device drivers in mind.


PIC

Figure 8.2: Using the framework to create user-mode device drivers

Figure 8.2 illustrates an example system in which device drivers are located in user-mode processes. In this case, all of the code within a given driver set is part of the user-level device driver process, and the “surrounding” OS-specific code, which makes calls to the drivers through the driver interface, and provides the functions in the “kernel interface,” is not actually kernel code at all but, rather, “glue” code that handles communication with the kernel and other processes. For example, many of the functions in the driver-kernel interface, such as the calls to allocate interrupt request lines, will be implemented by this glue code as system calls to the “actual” kernel, or as remote procedure calls to servers in other processes.

Device driver code running in user space will typically run in the context of ordinary threads; the execution environment required by the driver framework can be built on top of these threads in different ways. For example, the OS-specific glue code may run on only a single thread and use a simple coroutine mechanism to provide a separate stack for each outstanding process-level device driver operation; alternately, multiple threads may be used, in which case the glue code will have to use locking to provide the nonpreemptive environment required by the framework.

Dispatching interrupt handlers in these user-mode drivers can be handled in various ways, depending on the environment and kernel functionality provided. For example, interrupt handlers may be run as “signal handers” of some kind “on top of” the thread(s) that normally execute process-level driver code; alternatively, a separate thread may be used to run interrupt handlers. In the latter case, the OS-specific glue code must use appropriate locking to ensure that process-level driver code does not continue to execute while interrupt handlers are running.

Shared interrupt request lines

One particularly difficult problem for user-level drivers in general, and especially for user-level drivers built using this framework, is supporting shared interrupt lines. Many platforms, including PCI-based PCs, allow multiple unrelated devices to send interrupts to the processor using a single request line; the processor must then sort out which device(s) actually caused the interrupt by checking each of the possible devices in turn. With user-level drivers, the code necessary to perform this checking is typically part of the user-mode device driver, since it must access device-specific registers. Thus, in a “naive” implementation, when the kernel receives a device interrupt, it must notify all of the drivers hooked to that interrupt, possibly causing many unnecessary context switches for every interrupt.

The typical solution to this problem is to allow device drivers to “download” small pieces of “disambiguation” code into the kernel itself; the kernel then chains together all of the code fragments for a particular interrupt line, and when an interrupt occurs, the resulting code sequence determines exactly which device(s) caused the interrupt, and hence, which drivers need to be notified. This solution works fine for “native” drivers designed specifically for the kernel in question; however, there is no obvious, straightforward way to support such a feature in the driver framework.

For this reason, until a better solution can be found, the following policy applies to using shared interrupts in this framework: for a given shared interrupt line, either the kernel must unconditionally notify all registered drivers running under this framework, and take the resulting performance hit; or else the drivers running under this framework will not support shared interrupts at all. (Native drivers written specifically for the kernel in question can still use the appropriate facilities to support shared interrupt lines efficiently.)

8.5 Performance

Since this framework emphasizes breadth, adaptability, and ease-of-use over raw performance, the performance of device drivers running under this framework is likely to suffer somewhat; how much depends on how well-matched the particular driver is to the driver framework and to the host OS. Various factors can influence driver performance: for example, if the OS’s network code does not match the network drivers in terms of whether scatter/gather message buffers are supported or required, performance is likely to suffer somewhat due to extra copying between the driver and the OS’s network code. The OS developer will have to take these issues into account when selecting which sets of device drivers to use (e.g., FreeBSD versus Linux network drivers). If the device driver sets are chosen carefully and the OS’s driver support code is designed well, in many cases it should be possible to use these drivers with minimal performance loss.

Another consideration is how extensively the OS should rely on this device driver framework. There is nothing preventing the OS from maintaining its own (probably smaller) collection of “native” drivers designed and tuned for the particular OS; this way, the OS can achieve maximum performance for particularly common or performance-critical hardware devices, and use the larger set of device drivers easily available through this framework to provide support for other types of hardware that otherwise wouldn’t be supported at all. This approach of combining native and emulated drivers is likely to be especially important for kernels that are not well matched to the existing drivers this framework was designed around: e.g., “stackless” interrupt model kernels which must run emulated device drivers on special threads or in user space.

For a very rough idea of the performance of drivers and kernels using this framework, see the results in our SOSP’97 paper “The Flux OSKit: A Substrate for OS and Language Research.” Performance results for a related but less formal and less encapsulated framework can be found in the USENIX’96 paper “Linux Device Driver Emulation in Mach.”

8.6 Device Driver Initialization

When the host OS is ready to start using device drivers in this framework, it typically calls a probe function for each driver set it uses; this function initializes the drivers and checks for hardware devices supported by any of the drivers in the set. If any such devices are found, they are registered with the host OS by calling a registration routine specific to the type of bus on which the device resides (e.g., ISA, PCI, SCSI). The host OS can then record this information internally so that it knows which devices are available for later use. The OS can implement device registration any way it chooses; however, the driver support library (libdev) provided by the OSKit provides a default implementation of a registration mechanism which builds a single “hardware tree” representing all known devices; see Section 17.2 for more information.

When a device driver discovers a device, it creates a device node structure representing the device. The device node structure can be of arbitrary size, and most of its contents are private to the device driver. However, the first part of the device node is always a structure of type oskit_device_t, defined in oskit/dev/dev.h, which contains generic information about the device and driver needed by the OS to make use of the device. In addition, depending on the device’s type, there may be additional information available to the host OS, as described in the following section.

8.7 Device Classification

Device nodes have types that follow a C++-like single-inheritance subtyping relationship, where oskit_device_t is the ultimate ancestor or “supertype” of all device types.

In general, the host OS must know what class of device it is talking to in order to make use of it properly. On the other hand, it is not strictly necessary for the host OS to recognize the specific device type, although it may be able to make better use of the device if it does.

The block device class has the following attributes:

The character device class has the following characteristics:

The network device class has the following characteristics:

Note that it would certainly be possible to decompose these device classes into a deeper type hierarchy. For example, in abstract terms it might make sense to arrange character and network devices under a single supertype representing “asynchronous” devices. However, since the structure representing this “abstract supertype” would contain essentially nothing in terms of actual code or data, this additional level was not deemed useful for the driver framework. Of course, the OS is free to use any type hierarchy (or non-hierarchy) it desires for its own data structures representing devices, drivers, etc.

8.8 Buffer Management

XXX overview

8.9 Asynchronous I/O

While asynchronous I/O is not directly suported by the OSKit device interface, it is possible to create an asychronous interface in the OS itself, which calls the blocking fdev functions.

8.10 Other Considerations

XXX some rare, poorly-designed hardware does not work right if long delays occur while programming the devices. (This is supposedly the case for some IDE drives, for example.) For this reason, reliability and hardware compatibility may be increased by implementing osenv_intr_disable as a function that really does disable all interrupts on the processor in question.

XXX Symbol name conflicts among libraries. . . For each existing driver set, provide a list of “reserved” symbols used by the set.

XXX This should be moved somewhere else:

All functions may block, except those specifically designated as nonblocking.

All functions may be called at any time, including during driver initialization. In other words, all of the functionality exposed by this interface must be present and fully operational by the time the device drivers are initialized.

8.11 Common Device Driver Interface

This section describes the OSKit device driver interfaces that are common to all types of drivers and hardware.

8.11.1 dev.h: common device driver framework definitions

SYNOPSIS

#include <oskit/dev/dev.h>

XXX

oskit_dev_init

oskit_X_init_X

oskit_dump_drivers

oskit_dev_probe

oskit_dump_devices

rtc_get and rtc_set interfaces (Real time clock).

8.12 Driver Memory Allocation

The OS must provide routines for drivers to call to allocate memory for the private use of the drivers, as well as for I/O buffers and other purposes. The OSKit device driver framework defines a single set of memory allocation functions which all drivers running under the framework call to allocate and free memory.

Device drivers often need to allocate memory in different ways, or memory of different types, for different purposes. For this reason, the device driver framework defines a set of flags provided to each memory allocation function describing how the allocation is to be done, or what type of memory is required.

As with other aspects of the OSKit device driver framework, the libdev library provides default implementations of the memory allocation functions, but these implementations may be replaced by the OS as desired. The default implementations make a number of assumptions which are often invalid in “real” OS kernels; therefore, these functions will often be overridden by the client OS. Specifically, the default implementation assumes:

Additionally, the default routines which deal with physical memory addresses make these assumptions:

8.12.1 osenv_memflags_t: memory allocation flags

SYNOPSIS

XXX typedef unsigned osenv_memflags_t;

DESCRIPTION

All of the memory allocation functions used by device drivers in the OSKit device framework take a parameter of type osenv_memflags_t, which is a bit field describing various option flags that affect how memory allocation is done. Device drivers often need to allocate memory that satisfies certain constraints, such as being physically contiguous, or page aligned, or accessible to DMA controllers. These flags abstract out these various requirements, so that all memory allocation requests made by device drivers are sent to a single set of routines; this design allows the OS maximum flexibility in mapping device memory allocation requests onto its internal kernel memory allocation mechanisms.

Routing all memory allocations through a single interface this way may have some impact on performance, due to the cost of decoding the flags argument on every allocation or deallocation call. However, this cost is expected to be small compared to the typical cost of actually performing the requested operation.

The specific flags currently defined are as follows:

OSENV_AUTO_SIZE:
The memory allocator must keep track of the size of allocated blocks allocated using this flag; in this case, the value size parameter passed in the corresponding osenv_mem_free call is meaningless. For blocks allocated without this flag set, the caller (device driver) promises to keep track of the size of the allocated block, and pass it back to osenv_mem_free on deallocation.

It is possible for the OS to implement these memory allocation routines so that they ignore the OSENV_AUTO_SIZE flag and simply always keep track of block sizes themselves. However, note that in some situations, doing so may produce extremely inefficient memory usage. For example, if the OS memory allocation mechanism prefixes each block with a word containing the block’s length, then any request by a device driver to allocate a page-aligned page (or some other naturally-aligned, power-of-two-sized block) will consume that page plus the last word of the previous page. If many successive allocations are done in this way, only every other page will be usable, and half of the available memory will be wasted. Therefore, it is generally a good idea for the memory allocation functions to pay attention to the OSENV_AUTO_SIZE flag, at least for allocations with alignment restrictions.

OSENV_NONBLOCKING:
If set, this flag indicates that the memory allocator must not block during the allocation or deallocation operation. More specifically, the flag indicates that the device driver code must not be run in the context of other, concurrent processes while the allocation is taking place. Any calls to the allocation functions from interrupt handlers must specify the OSENV_NONBLOCKING flag.
OSENV_PHYS_WIRED:
Indicates that the must must be non-pageable. Accesses to the returned memory must not fault.
OSENV_PHYS_CONTIG:
Indicates the underlying physical memory must be contiguous.
OSENV_VIRT_EQ_PHYS:
Indicates the virtual address must exactly equal the physical address so the driver may use them interchangeably. The OSENV_PHYS_CONTIG flag must also be set whenever this flag is set.
OSENV_ISADMA_MEM:
This flag applies only to machines with ISA busses or other busses that are software compatible with ISA, such as EISA, MCA, or PCI. It indicates that the memory allocated must be appropriate for DMA access using the system’s built-in DMA controller. In particular, it means that the buffer must be physically contiguous, must be entirely contained in the low 16MB of physical memory, and must not cross a 64KB boundary. (By implication, this means that allocations using this flag are limited to at most 64KB in size.) The OSENV_PHYS_CONTIG flag must also be set if this flag is set.
OSENV_X861MB_MEM:
This flag only applies to x86 machines, in which some device drivers may need to call 16-bit real-mode BIOS routines. Such drivers may need to allocate physical memory in the low 1MB region accessible to real-mode code; this flag allows drivers to request such memory. This is not used by existing driver sets.

8.12.2 osenv_mem_alloc: allocate memory for use by device drivers

SYNOPSIS

void *osenv_mem_alloc(oskit_size_t size, osenv_memflags_t flags, unsigned align);

DIRECTION

Component --> OS, Blocking

DESCRIPTION

This function is called by the drivers to allocate memory. Allocate the requested amount of memory with the restrictions specified by the flags argument as described above.

XXX: While this is defined as blocking, the current glue code cannot yet handle this blocking, as it is not prepared for another request to enter the component. This will be fixed.

PARAMETERS
size:
Amount of memory to allocate.
flags:
Restrictions on memory.
align:
Boundary on which memory should be aligned, which must be a power of two, or 0 which means the same as 1 (no restrictions).
RETURNS

Returns the address of the allocated block in the driver’s virtual address space, or NULL if not enough memory was available.

8.12.3 osenv_mem_free: free memory allocated with osenv_mem_alloc

SYNOPSIS

void osenv_mem_free(void *block, osenv_memflags_t flags, oskit_size_t size);

DIRECTION

Component --> OS, Blocking

DESCRIPTION

Frees a memory block previously allocated by osenv_mem_alloc.

XXX: While this is defined as blocking, the current glue code cannot yet handle this blocking, as it is not prepared for another request to enter the component. This will be fixed.

PARAMETERS
block:
A pointer to the memory block, as returned from osenv_mem_alloc.
flags:
Flags indicating deallocation semantics required. Only OSENV_AUTO_SIZE and OSENV_NONBLOCKING are meaningful in this context. OSENV_AUTO_SIZE must be set if and only if it was set during the allocation, and OSENV_NONBLOCKING indicates that the deallocation operation must not block.
size:
If flags doesn’t include OSENV_AUTO_SIZE, then this parameter must be the size requested when this block was allocated. Otherwise, the value of the size parameter is meaningless.

8.12.4 osenv_mem_get_phys: find the physical address of an allocated block

SYNOPSIS

oskit_addr_t osenv_mem_get_phys(oskit_addr_t va);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

Returns the physical address associated with a given virtual address. Virtual address should refer to a memory block as returned by osenv_mem_alloc. XXX does it have to be the exact same pointer, or just a pointer in the block? In systems which do not support address translation, or for blocks allocated with OSENV_VIRT_EQ_PHYS, this function returns va.

The returned address is only valid for the first page of the indicated block unless it was allocated with OSENV_PHYS_CONTIG. In a system supporting paging, the result of the operation is only guaranteed to be accurate if OSENV_PHYS_WIRED was specified when the block was allocated. XXX other constraints?

PARAMETERS
va:
The virtual address of a memory block, as returned from osenv_mem_alloc.
RETURNS

Returns the PA for the associated (wired) VA. XXX zero (or something else) if VA is not valid?

8.12.5 osenv_mem_get_virt: find the virtual address of an allocated block

SYNOPSIS

oskit_addr_t osenv_mem_get_virt(oskit_addr_t pa);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

Returns the virtual address of an allocated physical memory block. Can only be called with the physical address of blocks that have been allocated with osenv_mem_alloc. XXX or else what?

XXX error codes?

XXX If the Linux glue uses this, and gets and error, should the physical memory be mapped (by the glue) (if it is not in the address space) and re-try?

PARAMETERS
pa:
The physical memory location.
RETURNS

Returns the VA for the mapped PA.

8.12.6 osenv_mem_phys_max: find the largest physical memory address

SYNOPSIS

oskit_addr_t osenv_mem_phys_max(void);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

Returns the top of physical memory, which is noramlly equivelent to the amount of physical RAM in the machine. Note that memory-mapped devices may reside higher in physical memory, but this is the largest address normal RAM could have.

RETURNS

Returns the amount of physical memory.

8.12.7 osenv_mem_map_phys: map physical memory into kernel virtual memory

SYNOPSIS

int osenv_mem_map_phys(oskit_addr_t pa, oskit_size_t length, void **kaddr, int flags);

DIRECTION

Component --> OS, Blocking

DESCRIPTION

Allocate kernel virtual memory and map the caller supplied physical addresses into it. The address and length must be aligned on a page boundary.

This function is intended to provide device drivers access to memory-mapped devices.

An osenv_mem_unmap_phys interface will likely be added in the future.

XXX: While this is defined as blocking, the current glue code cannot yet handle this blocking, as it is not prepared for another request to enter the component. This will be fixed.

Flags:

PHYS_MEM_NOCACHE:
Inhibit cache of data in the specified memory.
PHYS_MEM_WRITETHROUGH:
Data cached from the specified memory must be synchronously written back on writes.
PARAMETERS
pa:
Starting physical address.
length:
Amount of memory to map.
kaddr:
Kernel virtual address allocated and returned by the kernel that maps the specified memory.
flags:
Memory mapping attributes, as described above.
RETURNS

Returns 0 on success, non-zero on error.

8.13 DMA

This section is specific to ISA devices utilizing the Direct Memory Access controller.

If the OS wishes to support devices that utilize DMA, then basic routines must be provided to allow access to the DMA controller.

The Linux drivers directly access the DMA controller themselves, with macros and with embedded assembly. All devices that utilize the DMA controller must be in the same driver set, as there is not way to arbitrate between different driver sets. Because this shortcoming is in the encapsulated drivers, and would take significant effort to correct, we have not provided an interface to access the DMA controller, although we may in the future.

8.13.1 osenv_isadma_alloc: Reserve a DMA channel

SYNOPSIS

int osenv_isadma_alloc(int channel);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

This requests a DMA channel.

If sucessfull, the driver must be able to directly manipulate the ISA DMA controller.

PARAMETERS
channel:
The DMA channel to reserve.
RETURNS

Returns 0 on success, non-zero if already allocated.

8.13.2 osenv_isadma_free: Release a DMA channel

SYNOPSIS

void osenv_isadma_free(int channel);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

This releases a DMA channel. The DMA channel must have already been reserved by the driver.

PARAMETERS
channel:
The DMA channel to release.

8.14 I/O Ports

Many devices have a concept of “I/O space”. In general, multiple devices cannot share the same range of I/O ports. Unfortunately, there are a few exceptions, most notably the keyboard and PS/2 mouse, and the Floppy and IDE controllers.

Many of the device drivers assume they may access port 0x80, for use in timing loops. This is not used in most computers, although POST cards are used to display the last value written to that port.

8.14.1 osenv_io_avail: Check availability of a range of ports

SYNOPSIS

oskit_bool_t osenv_io_avail(oskit_addr_t port, oskit_size_t size);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

Returns true (nonzero) if the range is free; false (zero) if any ports in the range are already allocated.

PARAMETERS
port:
The start of the I/O range.
size:
The number of ports to check.
RETURNS

Returns 0 (false) if any part of the range is unavailable, non-zero otherwise.

8.14.2 osenv_io_alloc: Allocate a range of ports

SYNOPSIS

oskit_error_t osenv_io_alloc(oskit_addr_t port, oskit_size_t size);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

Returns 0 if the range is free, or an error code if any ports in the range are already allocated.

XXX: shared ports?

XXX: Default implementation panics if range is allocated.

Note: this is based on the assumption that I/O space is not mapped through the MMU. On a system where this is not the case (memory mapped I/O), osenv_mem_map_phys should be used instead.

PARAMETERS
port:
The start of the I/O range.
size:
The number of ports to check.

8.14.3 osenv_io_free: Release a range of ports

SYNOPSIS

void osenv_io_free(oskit_addr_t port, oskit_size_t size);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

Releases a range previously allocated. All ports in the range must have been allocated by the device.

PARAMETERS
port:
The start of the I/O range.
size:
The number of ports to check.

8.15 Hardware Interrupts

Shared interrupts are supported, as long as OSENV_IRQ_SHAREABLE is requested by all devices wishing to use the interrupt.

In a given driver environment in this framework, there are only two “interrupt levels”: enabled and disabled. In the default case in which all device drivers of all types are linked together into one large driver environment in an OS kernel, this means that whenever one driver masks interrupts, it masks all device interrupts in the system.1

However, an OS can implement multiple interrupt priority levels, as in BSD or Windows NT, if it so desires, by creating separate “environments” for different device drivers. For example, if each driver is built into a separate, dynamically-loadable module, then the osenv_intr_ calls in different driver modules could be resolved by the dynamic loader to spl-like routines that switch between different interrupt priority levels. For example, the osenv_intr_disable call in network drivers may resolve to splnet, whereas the same call in a disk driver may be mapped to splbio instead.

8.15.1 osenv_intr_disable: prevent interrupts in the driver environment

SYNOPSIS

void osenv_intr_disable(void);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

Disable further entry into the calling driver set through an interrupt handler. This can be implemented either by directly disabling interrupts at the interrupt controller or CPU, or using some software scheme.

XXX Merely needs to prevent intrs from being dispatched to the driver set. Drivers may see spurious interrupts if they briefly cause interrupts while disabled.

XXX Timing-critical sections need interrupts actually disabled.

8.15.2 osenv_intr_enable: allow interrupts in the driver environment

SYNOPSIS

void osenv_intr_enable(void);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

Enable interrupt delivery to the calling driver set. This can be implemented either by directly enabling interrupts at the interrupt controller or CPU, or using some software scheme.

8.15.3 osenv_intr_enabled: determine the current interrupt enable state

SYNOPSIS

int osenv_intr_enabled(void);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

Returns the driver’s view of the current interrupt status.

RETURNS

Returns non-zero if interrupts are currently enabled, zero otherwise.

8.15.4 osenv_intr_save_disable: disable interrupts and return the former state

SYNOPSIS

int osenv_intr_save_disable(void);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

Set the interrupt status to disabled and return the previous status.

This call is equivalent to calling osenv_intr_enabled and then calling osenv_intr_disable if the result was non-zero and is intended to optimize that common case.

RETURNS

Returns non-zero if interrupts are currently enabled, zero otherwise.

8.15.5 osenv_irq_alloc: allocate an interrupt request line

SYNOPSIS

int osenv_irq_alloc(int irqnum, void (*handler)(void *), void *data, int flags);

DIRECTION

Component --> OS, Blocking

DESCRIPTION

Allocate an interrupt request line and attach the specified handler to it. On interrupt, the kernel must pass the data argument to the handler.

XXX: interrupts should be “disabled” when the handler is invoked.

XXX: This has not been verified to function correctly if an incomming request is processed while this is blocked.

Flags:

OSENV_IRQ_SHAREABLE:
If this flag is specified, the interrupt request line can be shared between multiple devices. On interrupt, the OS will call each handler attached to the interrupt line. Without this flag set, the OS is free to return an error if another handler is attached to the interrupt request line. (If shared, ALL handlers must have this set).
PARAMETERS
irqnum:
The interrupt request line to allocate.
handler:
Interrupt handler.
data:
Data passed by the kernel to the interrupt handler.
flags:
Flags indicating special behavior. Only OSENV_IRQ_SHAREABLE is currently used.
RETURNS

Returns 0 on success, non-zero on error.

8.15.6 osenv_irq_free: Unregister the handler for the interrupt

SYNOPSIS

void osenv_irq_free(int irqnum, void (*handler)(void *), void *data);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

Removes the indicated interrupt handler. The handler is only removed if it was registered with osenv_irq_alloc for the indicated interrupt request line and with the indicated data pointer.

PARAMETERS
irq:
The physical interrupt line.
handler:
The function handler’s address. This is necessary if multiple handlers are registered for the same interrupt.
data:
The data value registered with osenv_irq_alloc.

8.15.7 osenv_irq_disable: Disable a single interrupt line

SYNOPSIS

void osenv_irq_disable(int irq);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

Prevents a specific interrupt line from delivering an interrupt. Can be done in software or by disabling at the interrupt controller.

If the interrupt does occur while disabled, it should be delivered as soon as osenv_irq_enable is called (see that section for details).

PARAMETERS
irq:
The physical interrupt line.

8.15.8 osenv_irq_enable: Enable a single interrupt line

SYNOPSIS

void osenv_irq_enable(int irq);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

This allows the specified interrupt to be received, provided interrupts are enabled. (e.g., osenv_intr_enabled also returns true)

PARAMETERS
irq:
The physical interrupt line.

8.15.9 osenv_irq_pending: Determine if an interrupt is pending for a single line

SYNOPSIS

int osenv_irq_pending(int irq);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

Determine if an interrupt is pending for the specified interrupt line.

PARAMETERS
irq:
The physical interrupt line.
RETURNS

Returns 1 if an interrupt is pending for the indicated line, 0 otherwise.

8.16 Sleep/Wakeup

The current driver model only allow one thread or request into the driver set at a time. However, if the driver set is waiting for an external event and can handle another request while it is waiting, then the driver sleeps.

The default implementation of sleep busy-waits on the event, as it is not possible for it to do more without knowledge of the operating sysmte environment it is in.

8.16.1 osenv_sleep_init: prepare to put the current process to sleep

SYNOPSIS

#include <oskit/dev/dev.h>

void osenv_sleep_init(osenv_sleeprec_t *sleeprec);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

This function initializes a “sleep record” structure in preparation for the current process’s going to sleep waiting for some event to occur. The sleep record is used to avoid races between actually going to sleep and the event of interest, and to provide a “handle” on the current activity by which osenv_wakeup can indicate which process to awaken.

PARAMETERS
sleeprec:
A pointer to the process-private sleep record.

8.16.2 osenv_sleep: put the current process to sleep

SYNOPSIS

#include <oskit/dev/dev.h>

int osenv_sleep(osenv_sleeprec_t *sleeprec);

DIRECTION

Component --> OS, Blocking

DESCRIPTION

The driver calls this function at process level to put the current activity (process) to sleep until some event occurs, typically triggered by a hardware interrupt or timer handler. The driver must supply a pointer to a process-private “sleep record” variable (sleeprec), which is typically just allocated on the stack by the driver. The sleeprec must already have been initialized using osenv_sleep_init. If the event of interest occurs after the osenv_sleep_init but before the osenv_sleep, then osenv_sleep will return immediately without blocking.

PARAMETERS
sleeprec:
A pointer to the process-private sleep record, already allocated by the driver and initialized using osenv_sleep_init.
RETURNS

Returns the wakeup status value provided to osenv_wakeup.

8.16.3 osenv_wakeup: wake up a sleeping process

SYNOPSIS

#include <oskit/dev/dev.h>

void osenv_wakeup(osenv_sleeprec_t *sleeprec, int wakeup_status);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

The driver calls this function to wake up a process-level activity that has gone to sleep (or is preparing to go to sleep) waiting on some event. The value of wakeup_status is subsequently returned to the caller of osenv_sleep, making it possible to indicate various wakeup conditions (such as abnormal termination). It is harmless to wake up a process that has already been woken.

PARAMETERS
sleeprec:
A pointer to the sleep record of the process to wake up. Must actually point to a valid sleep record variable that has been properly initialized using osenv_sleep_init.
wakeup_status:
The status to be returned from osenv_sleep. OSENV_SLEEP_WAKEUP indicates normal wakeup, OSENV_SLEEP_CANCELLED indicates an interrupted sleep, while other status values indicate other conditions.

8.17 Driver-Kernel Interface: Timing

The device support code relies on the OS to provide timers to control events. Unfortunately, timers are in a state of flux, and there are currently too many ways to do almost the same thing. We will be cleaning this up.

Meanwhile. . . the interface provided by the host OS is currently at the osenv_timer layer. However, we plan on moving the abstraction layer down to a simple “PIT” interface. (The existing osenv_timer_pit code is similar to the planned interface).

When we move to an osenv_pit interface, the driver glue code will use an intermediate timer ‘device driver’ which will provide the higher-level functionality currently in the osenv_timer interface. The motivation for this is to make the OS-provided interface as simple as possible and to build extra functionality on top.

‘dev/clock.c’ is an example device driver built on the osenv_timer interface. It could be implemented on top of an osenv_pit interface as easily as on the osenv_timer interface.

The current implementation of the default osenv_timer code is based on the osenv_timer_pit interface. osenv_timer_pit is not currently defined as part of the osenv API, but merely exists for implementation convenience. However, over-riding the osenv_timer_pit implementation is probably the easiest way to provide a different implementation of the osenv_timer interface.

The default osenv_timer implementation also provides an osenv_timer_shutdown hook for use by the host operating system. osenv_timer_shutdown disables the osenv_timer.

8.17.1 osenv_timer_init: Initialize the timer support code

SYNOPSIS

void osenv_timer_init(void);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

XXX: Belongs in libdev.a section

Intiializes the timer code.

8.17.2 osenv_timer_register: Request a timer handler be called at the specified frequency

SYNOPSIS

void osenv_timer_register(void (*func)(void), int freq);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

Requests that the function func gets called freq times per second.

XXX: Default implementation currently only works for freq equal to 100.

PARAMETERS
func:
Address of function to be called.
freq:
Times per second to call the handler.

8.17.3 osenv_timer_unregister: Request a timer handler not be called

SYNOPSIS

void osenv_timer_unregister(void (*func)(void), int freq);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

The function pointer and frequency must be identically equal to parameters on a previous osenv_timer_register call.

PARAMETERS
func:
Address of function to be called.
freq:
Times per second the handler was called.

8.17.4 osenv_timer_spin: Wait for a specified amount of time without blocking.

SYNOPSIS

void osenv_timer_spin(long nanosec);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

This allows a driver component to block for a specified amount of time (usually for hardware to catch up) without blocking. Unlike with osenv_sleep, this cannot give up the process-level lock.

PARAMETERS
nanosec:
Time to spin, in nanoseconds.

8.18 Misc

All output goes throught the osenv_vlog interface.

The following log priorities are defined. From highest priority to lowest, they are: OSENV_LOG_EMERG, OSENV_LOG_ALERT, OSENV_LOG_CRIT, OSENV_LOG_ERR, OSENV_LOG_WARNING, OSENV_LOG_NOTICE, OSENV_LOG_INFO, and OSENV_LOG_DEBUG which correspond the the log priorities used by both BSD and Linux.

8.18.1 osenv_vlog: OS environment’s output routine

SYNOPSIS

void osenv_vlog(int priority, const char *fmt, va_list args);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

This is the output interface to the device driver framework. All output must go through this interface, so the OS may decide what to do with it.

Normal printf-type calls should get converted to the OSENV_LOG_INFO priority.

PARAMETERS
priority:
fmt:
printf-style message format
args:
Any parameters required by the output format

8.18.2 osenv_log: OS environment’s output routine

SYNOPSIS

void osenv_log(int priority, const char *fmt, ...);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

Front-end to osenv_vlog

PARAMETERS
priority:
Priority of the message.
fmt:
printf-style message format
...:
Any parameters required by the output format

8.18.3 osenv_vpanic: Abort driver set operation

SYNOPSIS

void osenv_vpanic(const char *fmt, va_list args);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

This function should only be called if the device driver framework can no longer continue and cannot exit gracefully.

The driver’s ‘native’ panic calls will get resolved to this function call.

This should be provided by the OS to provide a graceful way of dealing with a situation that prevents the drivers from continuing.

PARAMETERS
fmt:
printf-style message format
args:
Any parameters required by the output format

8.18.4 osenv_panic: Abort driver set operation

SYNOPSIS

void osenv_panic(const char *fmt, ...);

DIRECTION

Component --> OS, Nonblocking

DESCRIPTION

Front-end to osenv_vpanic

PARAMETERS
fmt:
printf-style message format
...:
Any parameters required by the output format

8.19 Device Registration

Nothing here yet, sorry. See Section 17.2 for a tiny bit more information on our current default implementation of device registration. More information can be gained from the extensively commented header files in the directory <oskit/dev>, starting with file device.h.

8.20 Block Storage Device Interfaces

This section is incomplete. Block device interfaces now provide an open method which returns a per-open blkio object through which block reads and writes are done. See Section 7.3. In the absence of other documentation, the example programs will be helpful.

XXX describe oskit_blkdev, blksize, etc.

8.21 Serial Device Interfaces

XXX: This section is in severe need of an update.

Character device support is provided in the OSKit using device drivers from FreeBSD.

8.22 Driver-Kernel Interface: (X86 PC) ISA device registration

8.22.1 osenv_isabus_addchild: add a device node to an ISA bus

XXX: new device tree management

The address parameter is used to uniquely identify the device on the ISA bus. For example, if there are two identical NE2000 cards plugged into the machine, the address will be be the only way the host OS can distinguish them, because all of the other parameters of the device will be identical. If address is in the range 0-0xffff (0-65535), it is interpreted as a port number in I/O space; otherwise, it is interpreted as a physical memory address. For devices that use any I/O ports for communication with software, the base of the “primary” range of I/O ports used by the device should be used as the address; a physical memory address should be used only for devices that only communicate through memory-mapped I/O.

8.22.2 osenv_isabus_remchild: remove a device node from an ISA bus