HYDROGEN: Device drivers (by alaric)
As promised, here is the fourth part of my series on HYDROGEN, where I will discuss device drivers.
Introduction
I've mentioned the general approach HYDROGEN takes to interfacing to devices in the section on extensibility. Now let's go into a little more detail.
Two interfaces
For simpler systems, particularly those without any possibility of the hardware changing at run time, we have the static interface. In this, an application that needs or can use a kind of device makes sure it requires the appropriate word set with a require
statement, such as require device:storage
for a persistent storage device (optionally protected by feature? device:storage
and a conditional if it can make do without).
It can then ask how many storage devices are available by calling device:storage:enumerate
, which pushes the number of devices and a pointer to a block of memory that many cells long, where each cell is a 'handle' for a storage device.
Then it can use words such as device:storage:capacity
, device:storage:theoretical-maximum-transfer-rate
, device:storage:average-seek-time
, device:storage:block-size
, and so on to request information about each storage device, given a handle; then it can get down and dirty with device:storage:read
and device:storage:write
.
But for more complex systems, we also have the device-tree
feature. device-tree
has three aspects:
Firstly, it lets the application register an interrupt that is invoked whenever the hardware configuration changes. The interrupt handler is given pointers to and sizes of two arrays of cells, one listing the handles of newly created devices, the other listing the handles of devices that are no more. Subsequent access to handles of removed devices will produce "device has gone away" errors rather than anything undefined for a reasonable time frame (eg, the implementation must avoid reusing a device handle for as long as is reasonably possible), but the application is expected to stop attempting to use the missing device as soon as it can!
Secondly, it guarantees that device handles are distinct. On a system without device-tree
, it could number its device:storage
devices 1
,2
,3
, and its device:port
devices 1
,2
,3
, as there's no need for the handle spaces to be distinct. But systems with device-tree
must identify each device by a unique handle, regardless of type, and each device type must also provide a word such as device:storage?
, which accepts a device handle and returns a boolean indicating if the device is of that type. Note that a device may be of more than one type, which we discuss a little later.
Thirdly, it provides a host of generic operations on devices that are valid regardless of their type. These include query interfaces to ask for a human-readable name for a device as a string and other information such as manufacturer names, model numbers, serial numbers, and the like; a generic interface to query if a device is removable at run time, and if it is, whether it can be locked in software to prevent removal, to register an interrupt handler to be called if the user requests unlocking, and to lock and unlock it; a power management interface, to query a device's level of power management capabilities, and to select between levels of sleep/standby/off/etc modes that a device supports; and, of course, the actual topology of the device tree itself.
All the devices in a system can be organised into a tree structure, starting with a root device which is always of type device:system
. The device tree interfaces supports asking for the handle of a device's parent (device:system
just reports itself), and asking for a list of the handles of a device's children.
This tree is a connection tree, showing how the devices are connected. Conventionally, under device:system
will be found one or more device:chassis
instances, which represent the physical chassis the computer is within, and each chassis is likely to contain some mix of device:psu
s (which may also be instances of device:battery
or device:external-power
and device:motherboard
s; a device:motherboard
will then have device:cpu
s and then busses such as device:pci
, device:agp
, device:isa
, and so on, onto which actual devices may hang, or other busses (where there's a hardware bridge).
Busses such as device:pci
will usually also define a device type that matches any device on such a bus, to support generic operations; all children of a device:pci
, regardless of their primary type, will also be a device:pci-device
, which then has words to query the PCI configuration of the device.
Unrecognised types of device on a bus may then only respond to the bus's general device type. If the general device type supports a generic interface to access that device directly, it becomes possible for higher-level drivers to be written, outside of the HYDROGEN kernel itself.
There's a distinction between storage units, device:storage-unit
, and storage, device:storage
. A hard disk might be a storage unit, which provides an interface for modifying partition tables; it would then have child devices of type device:filesystem
for recognised POSIX-style filesystems, device:storage
for partitions marked as usable by HYDROGEN, and device:boot
for a HYDROGEN boot loader partition; the boot-media
interface discussed in the bootstrapping article provides for installing boot media into whatever bit of the system booted this instance of HYDROGEN, it's possible that there might be multiple boot partitions to be found, as one might have a secondary one on a second disk, or might wish to install a boot loader onto a removable disk to be inserted into another machine. The device:boot
devices support the same interface as boot-media
, but this time parameterised by a device handle. The device:boot
that we actually booted from should identify itself with device:boot:active?
, and the one that the system will normally boot from without human intervention during startup should return true from device:boot:primary?
.
A removable disk drive would have a storage unit device, but whose partitions dynamically appear and disappear.
device:filesystem
devices exist to provide a gateway to POSIX-style hierarchical file systems. They have explicit mount and unmount words.
What about hosted implementations?
An implementation that runs "hosted" on top of a POSIXy operating system would have a rather different device tree structure. You'd still have a device:system
at the root, which would also be a device:posix-system
(providing words to access POSIXy libc functions). Underneath that we'd expect to find a device:filesystem
whose mount and unmount operations do nothing, and which just wraps the underlying OS filesystem. Windows machines might have multiple filesystem devices, one for each drive letter.
Hardware monitoring
The presence of all those chassis and motherboard devices is convenient for locating bits of hardware inside massive computers with more than one chassis and motherboard, but even on normal computers, they serve a purpose. There's a device type called device:monitor
which represents a hardware monitor of some kind. A monitor device will have child devices that are sensors, such as device:temperature-monitor
, etc. Each sensor device monitors another device in the system, and can be asked for the handle of that device. So a temperature sensor attached to a particular CPU would report that; while a chipset temperature sensor would be attached to the motherboard device, and an ambient temperature sensor would be attached to the chassis.
Fan interfaces are likely to be found under monitor devices, too. Each fan device identifies the handle of the device it cools (which may just be the chassis device if the fan is a general circulator), and may offer words to control the fan from software, or to modify its automatic control system.
The Console
A system's console is very important, and often rather overlooked.
The VAX architecture specification defines a rather full-feature console, with the ability to halt the system and to perform various management operations. HYDROGEN leaves such capabilities as an implementation decision; however, it does define a rich console interface for applications to use.
Traditional UNIX consoles are just a teletype (while Windows doesn't even define a console). Although the HYDROGEN console can be implemented via ANSI escape sequences over a teletype interface, it provides a rather higher-level interface to the application.
In effect, the console is composed of three facilities. One is a status facility, where the application registers a number of status items. Each status item has a name (string), value (string), and status (ok/warning/error/unknown). The status item is given a name when it's registered, and the console interface returns a handle; the application may then update the value and status whenever it wishes.
How the console displays these status items is up to it, but one might imagine that the top few lines of a text-mode screen be devote to them, laid out as a table, colour coded by status; if there's too many to fit, they can scroll, with function keys to pause or resume the scroll, to go to the next or previous page, to only show errors and warnings, etc.
Exposing the status information also lets the console take extra alerting steps. If, on a previously happy system, one or more status items go to the error state, the console may sound a beeper until a human presses a button to cancel it. A front-panel LED that's controllable from software might be used to indicate the worst status of an item in the system, as an overall health indicator.
The next aspect of the console is the log. Applications can submit a log message, with a level (debug/info/warning/error/fatal) to a log channel they've created (by giving it a name, and getting back a handle that can be used to submit log messages to that channel with). Again, the console can provide whatever display interface to this log stream it likes, such as using colour to indicate log levels, and providing function keys to show only certain log channels or levels. And the console could be configured to sound a beeper whenever there's a fatal log message.
The final aspect is the command/query interface. If the application registers a command interrupt handler, then the console can present the user with a text entry capability. When the user enters a command and submits it, the command handler is invoked, given a handle which can be used to submit strings for display in response to the user. When the handler finishes, then the command entry ability reappears. But the application may also explicitly ask the user a question, either as part of handling a command or entirely asynchronously; the question is given as a string, and a specified response interrupt handler triggered with the response when it comes.
How the console deals with multiple queries in progress, and queries coming up when the user's in the middle of entering a command, is up to it, of course.
Multiplexing
There might be multiple physical consoles on a computer. Perhaps a server will have a keyboard, mouse, and video interface that can present a text-mode or graphical console, but will also have a serial port that can provide a text-mode console via ANSI sequences, or via an implementation-dependent protocol that directly provides the console over the serial port, so a graphical interface application can run on the other end. An implementation could also provide a low-level network management interface, by using a dedicated network interface or piggybacking on one by accepting and generating its own Ethernet frames in parallel with the use of that interface by the HYDROGEN application. In such cases, the status items and log entries are sent to all the possible consoles; and all of them show if the console is ready to accept a command, or there is a query in progress, and the first one to respond to the query or to submit a command 'wins'.
But we also provide an interface for the application to register a virtual console, which then also takes part in the multiplexing. We can't let a broken or slow virtual console disable the system, so the virtual console is isolated from the primary console multiplexer with an event queue.
Management features
Other management features are implementation-dependent. Things like soft power buttons (that provide an interface to software so that pressing the button requests that the operating system perform an orderly shutdown, rather than directly powering the system off) are handled by a power button device in the device tree, but the implementation may also provide a 'virtual' power button device that's triggered by a function key on a console device. Similarly, any facilities to halt the system and go into HYDROGEN-level debuggers are up to the implementation.
But implementations are encouraged to be creative! Management of large clusters is unnecessarily difficult. Let's fix that.