Skip to content

Drivers

Dietmar Planitzer edited this page Sep 26, 2025 · 1 revision

A driver object manages a device. A device is a piece of hardware while a driver is the software that manages the hardware.

Drivers are organized in a driver hierarchy - also known as a driver tree. The root of the driver hierarchy is the 'platform controller'. This is a special kind of driver that knows how to detect all the various hardware components on a motherboard and how to create drivers for them. Many of the drivers that the platform controller creates are 'bus controllers'.

A bus controller is a kind of driver that manages a physical or a virtual bus. It is responsible for detecting hardware that is connected to the bus and it kicks off the creation of bus client drivers that then in turn manage a piece of hardware that is connected to the bus.

Every driver has a parent driver and it may have children drivers. The parent driver is usually the bus controller that manages the driver. However the parent driver may be an intermediate driver that sits between you and the bus controller. Your driver should use the services of the parent driver to issue commands to the bus.

Driver Lifecycle

Every driver instance goes through a lifecycle. The various lifecycle stages are:

  • Created: driver was just created
  • Active: entered by calling Driver_Start
  • Stopping: entered by calling Driver_Stop with a stop reason
  • Stopped: entered by calling Driver_WaitForStopped
  • Ready for destroy: the driver may be destroyed once Driver_WaitForStopped returns

A driver must be started by calling Driver_Start before any other driver function is called. It is however possible to release a driver reference by calling Object_Release even before Driver_Start is called.

The Driver_Start function transitions the driver lifecycle state to active and it invokes the onStart method. A driver subclass is expected to override onStart to publish the driver to the driver catalog by calling Driver_Publish. Additionally the driver subclass can do device specific initialization work in onStart. A driver will only enter active state if the onStart override returns with EOK.

Once a driver has been started, driver channels may be created by calling Driver_Open and a driver channel should be closed by calling IOChannel_Release on the channel. IOChannel_Release in turn invokes Driver_Close.

A driver may be voluntarily terminated by calling Driver_Stop with the. kDriverStop_Shutdown parameter. This indicates to the driver system that the driver wants to stop in a voluntarily and orderly fashion. If the driver is a bus controller (driver) then this stop will be automatically propagated to all the child drivers and the child drivers will be able to use I/O services of the bus driver to orderly shut down themselves.

For example, a SCSI bus driver that stops will stop all of its bus clients by calling Driver_Stop on them with the same reason that was passed to the SCSI bus driver. If the reason is kDriverStop_Shutdown then bus clients like a SCSI disk driver will be able to use the I/O services of the SCSI bus driver to park the head of the disk.

If however the stop reason is kDriverStop_Abort or kDriverStop_HardwareLoss then the bus client drivers would not be allowed to use the SCSI bus driver I/O services since either the SCSI bus driver is in an undetermined state or it is no longer able to access the SCSI hardware.

The next step after calling Driver_Stop is to invoke Driver_WaitForStopped. A driver may deploy asynchronous services internally and the shutdown of those services is triggered by the Driver_Stop call. However it can take a while for those services to complete their shutdown. This is why it is necessary to wait by calling Driver_WaitForStopped before you release the driver instance by calling Object_Release.

A typical driver lifecycle looks like this:

Driver_Create()
  Driver_Start()
    Driver_Open()
      IOChannel_Read()
      ...
    Driver_Close()
  Driver_Stop()
  Driver_WaitForStopped()
Object_Release()

Note that an important purpose of I/O channels is to enable the driver system to track whether a driver is in active use.

Drivers, Concurrency and Exclusivity

I/O channel provides important preconditions for what is discussed below:

  • All operations on an I/O channel are executed serially
  • At most one operation can be active on an I/O channel at any given time
  • It guarantees that no operation is active when it calls Driver_Close
  • It guarantees that as soon as Driver_Close() starts executing and at any time after it returns, no new I/O operations will be issued to the driver

The driver operations start(), open(), close() and terminate() are exclusive to each other in terms concurrency. This means that only one of those operations will execute at any given time. This ensures for example that start() has completed before open() can begin execution and that open() has completed before close() can begin execution.

The fact that all driver I/O operations (read, write, ioctl) require that the caller passes in an I/O channel ensures that none of these operations can be executed before open() has completed and has returned a valid I/O channel.

The fact that I/O channel guarantees that all active I/O operations on the channel have completed before it invokes close() on the driver ensures that close() can assume that no I/O operations can be active that are related to the channel that is passed to close(). Thus it is not necessary for close() to take the same lock that is used to protect the integrity of the I/O operations.

Note that onStart, onStop, onOpen and onClose are invoked while the driver is holding the driver state management lock. This should not be of much relevance to a driver subclass since a driver subclass has no need to acquire its I/O operations lock from these overrides. See the previous paragraph for an explanation of why this is the case.

A driver subclass is expected to guarantee that its read, write and ioctl operations are properly synchronized with each other and that invoking these methods concurrently will not lead to inconsistent hardware nor software state.

A driver can achieve this by eg protecting these methods with an I/O operation lock (mutex), by using a serial dispatch queue or by implementing its own specialized I/O operations queueing mechanism.

The Driver Lifecycle and I/O Operations Locks

Every driver owns and manages two important locks.

The first one is the lifecycle management lock. This lock is owned and managed by the Driver base class and subclasses do not need to worry about the details of this lock. This lock is used to protect the integrity of the driver state and it is used to ensure atomicity and exclusivity of the start(), stop(), open() and close() driver functions.

The second lock is the I/O operations lock which is owned and managed by the Driver subclass. it is used to protect the integrity of the I/O related hardware and software state of the Driver subclass. It is also used to ensure atomicity and exclusivity of the read(), write() and ioctl() functions.

It is the responsibility of a Driver subclass writer to implement the I/O operations lock and that it is acquired and released at the appropriate times.

The Children of a Driver

A driver may create and manage child drivers. Child drivers are attached to their parent drivers and the parent driver maintains a strong reference to its child driver(s). This strong reference keeps a child driver alive as long as it remains attached to its parent. A child driver in turn receives an unowned reference to its immediate parent driver when it is attached to it. This reference remains valid until a driver is stopped and detached from its parent.

The parent-child driver relationship can be used to properly represent relationships like a bus and the devices on a bus. The bus is represented by the parent driver and each device on the bus is represented by a child driver.

Another use case for the parent-child driver relationship is that of a multi- function expansion board: an expansion board which features a sound chip and a CD-ROM driver can be represented by a parent driver that manages the overall card functionality plus a child driver for the sound chip and another child driver for the CD-ROM drive.

A bus driver must explicitly configure the maximum number of children that it should manage. This number can be seen as the number of slots that are available on a physical bus. You configure this number by calling Driver_SetMaxChildCount after you've created the bus driver instance and before you publish it to the driver catalog.

A child driver may either receive a channel or a direct reference to its parent driver which it can then use to access the services that the parent driver provides. Both models are supported and the parent driver will stay alive as long as at least one channel is open or one child remains attached to it.

A child driver is guaranteed that the reference to its parent driver will remain valid from the moment that its onStart is invoked until the moment it returns from its onWaitForStopped override.

Parent - Child Driver Relationship and Starting/Stopping a Driver

A driver must be attached to a parent driver before it can be started to ensure that the driver will be able to access the services of its parent as soon as its onStart method is called.

A driver must be stopped before it can be detached from its parent driver to ensure that it continues to have access to the services that its parent driver provides until it has returned from its onWaitForStopped override.

The Driver_AttachChild and Driver_DetachChild methods implement and guarantee the semantics described in the previous paragraphs.

What it Means for a Driver to be in Use

A driver is considered to be in use if:

  • at least one channel is open
  • at least one child is attached to it A driver that is in use may be stopped. However the driver isn't destroyed until after the last use of it is gone.

Driver Categories

A driver conforms to a set of I/O categories. For many drivers it is sufficient to declare a single I/O category conformance. However a more complex driver that controls a piece of hardware that implements a number of different features will potentially declare one I/O category per feature. This is the reason why the Driver_Create function takes an array of I/O categories. This array must be terminated with an IOCAT_END declaration. Note that the Driver_Create function does not copy the I/O categories array. This array should be declared as a const static array. Use the IOCATS_DEF() macro to declare the I/O category array for your driver to get the correct declaration.

Use the Driver_GetCategories function to get a pointer to the read-only array of I/O categories the driver conforms to. This array is terminated by an IOCAT_END declaration.

Use the Driver_HasCategory function to check whether the driver conforms to a given I/O category.

These functions can be used to easily and efficiently determine whether a driver controls hardware of a certain kind (eg whether it is a SCSI bus) and whether the hardware supports a certain kind of feature (eg whether a graphic card supports 2D and/or 3d hardware acceleration).

Publishing a Driver and the Driver Catalog

The driver manager maintains a driver catalog. The driver catalog stores an entry for every driver that should be openable. A driver is opened by calling open() with a path that point to a driver's catalog entry.

The driver class provides two convenience APIs to publish a driver:

  • Publish: this should be used by a simple client driver that does not implement a bus.
  • PublishBus: this should be used by a driver that implements a bus

The PublishBus() function creates a directory to represent the bus and to provide a location where the bus clients can publish their respective driver entries. PublishBus() is additionally able to publish a 'self' entry in the bus directory. The 'self' entry represents the bus controller itself. This entry should be published if the bus controller wants to provide useful APIs to user space applications. Eg APIs to probe the bus, reset it or retrieve useful information about it.

A driver should call the Publish() or PublishBus() function from its onStart override. It is not necessary to override onStop to call Unpublish() because the Driver base class takes care of unpublishing the driver automatically. Additionally, a onStart override does not need to call Unpublish() if it encounters an error because Driver automatically unpublishes the driver if necessary if onStart returns with an error.

Unpublish is able to correctly unpublish a client driver and a bus driver.

Clone this wiki locally