Bill was the principal developer of 2.8 and 2.9BSD and was the chief architect of National Semiconductor's GENIX project, the first virtual memory microprocessor-based UNIX system. Prior to establishing TeleMuse, a market research firm, Lynne was vice president of marketing at Symmetric Computer Systems. They conduct seminars on BSD, ISDN, and TCP/IP. Send e-mail questions or comments to [email protected] (c) 1991 TeleMuse.
Last month we examined the mechanics of processes and context switching. Coupled with a basic understanding of multiprogramming, multiprocessing, and multitasking (see DDJ, September 1991), we have now covered one of the fundamental tenets on which our 386BSD operating systems kernel relies and on which everything else is built. With this, we have conquered the "first pitch" of our mountain.
In essence, we can consider our examination of multiprogramming and multiprocessing and the details of swtch( ) to be analogous to an examination of our map (concepts) and a careful laying of anchors before we climb up and over a treacherous overhang. Why an overhang? Because a cavalier approach to these basic elements could result in a misdesign which causes a great fall later. Witness the difficulty in getting other operating systems to accomplish what UNIX was designed to do from the first.
However, it is time to make tracks and cover new ground. We are now working on many areas of the 386BSD port at once, so we must return to our main( ) procedure (see DDJ, August 1991) and focus on the organization and primitives which impact device drivers. In particular, we need to understand the concepts necessary to the integration of appropriate device drivers. We examine the UNIX concept of "device interface," the layout and terms used in device drivers, and how BSD works the miracle of autoconfiguration. We also examine how our BSD kernel interfaces with its device drivers.
Next month, we will examine actual driver operations. Then, after laying the groundwork for our UNIX device drivers, we will discuss some sample device drivers.
In our previous articles on machine-dependent (DDJ, July 1991) and machine-independent (DDJ, August 1991) initialization, you might have noticed that we completely bypassed a significant area -- I/O device initialization, otherwise known as "automatic configuration" or autoconfiguration. This was done intentionally so that we could present a clear introduction to the basic operating arrangement of our BSD kernel without gorging on UNIX trivia.
By describing the basic framework of kernel services prior to I/O devices, we actually chronicled this port as it happened. We took this approach because by using portions of the kernel services to debug and/or bypass problems we encounter with the device drivers, we make a lot less work for ourselves. When needed, we could build a debugging framework around a targetted problem area, focus on it, try alternatives, and resolve it to conclusion.
In other words, at every point along the climb, we have attempted to belay ourselves against the foundation of work we have built. (The question now becomes "Was the mountain there to climb, or did we build the mountain as we climbed it?" Zen philosophers and systems programmers can debate this question at their leisure.) The further we delve into the system, the greater the possibility of a catastrophic misstep, so our anchors (tools) must be carefully placed to prevent us from minor falls. Our previous work will now form the basis for our current work on drivers.
And while there are many heartbreaks (and other breaks) which result from falling, nothing is quite as sweet as conclusively putting the finger on an obstinate nine-month-old "bug" that has played hide-and-seek through your most relentless attempts. ("That which does not destroy us, makes us strong." --Nietzsche.)
Over the course of integrating drivers into an operating system, a programmer unversed in systems can be intimidated by the device interface problem. The common approach is to try and "glue" an arbitrarily designed driver onto the side of the kernel and attempt to minimize the interface to the kernel, perhaps by doing everything in the driver. This half-hearted approach may result in a (somewhat) working product, but it does not lead to efficient and correct design and operation in all cases. Given the frequency in which this is done, it's no wonder that drivers are frequently considered a black art. They are never truly finished or fully debugged. ("If carpenters built homes the way programmers write programs, then the first woodpecker to come along would destroy civilization.")
Another approach is to actually reverse your perspective and consider the entire problem as a "bag of drivers" with UNIX as the pervasive interface to them (see "Brief Notes: UNIX -- Just a 'Bag of Drivers?'). In other words, hold UNIX as the given constant and mold the driver design to suit. This is somewhat unorthodox, but can be quite instructive.
So, instead of dealing with the driver as an independent entity, we take a broader view of the kernel's interfaces and services provided for the drivers' use. We can then leverage this knowledge of the kernel to illustrate the methodology of how the kernel's rich set of services can be lithely used to integrate device drivers. This approach actually fits in quite nicely with the heritage of UNIX device-driver integration.
Now that we have shifted our perspective of UNIX, we should really sit down and define our terms carefully. In general, the term "device driver" refers to the software that operates a device. Obvious enough, right?
It's when we try to get specific that we run into trouble. For example, if we extend the definition of device to imply control of a "hardware device," we find that we have now excluded many drivers that function entirely in software. These "software devices" are used to mimic a device-driver interface to simulate the effect of the desired "pseudo-device," such as /dev/pty. (Pseudo-ttys, which simulate terminal drivers, are used when logging in over the network with a telnet or rlogin session.) Other device drivers can redirect references to yet another driver elsewhere in the kernel, bypassing the "normal" reference. The /dev/tty device, for example, always refers to the terminal the process is currently associated with, even though this may be different for most processes on the same system.
In systems other than UNIX, device drivers can vary in role, responsibility, and form. Under MS-DOS, we can have drivers implemented in the BIOS as loadable files (for example, ANSI.SYS) or as TSRs (most mouse drivers). Under Mach 3.0, device drivers run outside the kernel in separate processes, as entities completely separate from the operating system.
For our purposes, a driver is a set of functions, compiled into the kernel when it is generated, that connect to the driver interface mechanism. Generally, the functions of a driver are all kept in a single source file, and there is one driver per device. Frequently, the part of the device that the computer directly interacts with is called the "controller," and it may have more than one physical device. If the devices can operate autonomously during operations to a degree, they are called "slaves," because they share responsibility with the controller "master" for the transfer, unlike "dumb" devices that have a trivial role.
Device driver are usually responsible for all aspects of device recognition, initialization, operation, and error recovery. Because the devices may be mounted on a hierarchy of buses and rely on interrupt mechanisms of the processor, they interact with many machine-dependent and bus-dependent support functions. Many times, the characteristics of the support functions are so different between different computers (such as the Mac and a PC or workstation) that drivers for similar controllers look radically different.
The required intimacy with the system and the architecture is one reason that driver code is reinvented all the time (the "have it your way" method gone mad). Even UNIX drivers on the same architecture may require significant rework to port them between different flavors of UNIX (such as SVR3, SVR4, MACH, and BSD). The choice of drivers in 386BSD (as in other UNIX ports), was significantly affected by our ability to leverage other drivers present on the same architecture.
Sometimes, when there is a good match between the needs of a porting project and those of a reliable and well-written "old" driver, it can be leveraged with a minimum of effort. We can then put all our efforts into refining something of demonstrated value rather than reinventing the wheel.
Frequently, however, there is little in common between the two, and trying to glue the old code into the new system becomes more trouble than writing one from scratch. Worse yet, an "old" driver may purport to be more than it is, by claiming to support functionality that has not been tested, although on the surface it may seem to at least pay lip service to needful areas. In fact, we have seen many such half-hearted drivers, and very few that methodically set out to extensively support the equipment. The reason is obvious: The drivers are finished, as far as the programmer is concerned, and never looked at again.
You can assess drivers by looking for the hallmarks: structure, form, history, organization, content, correctness, and clarity. The hardest hallmark to judge, pragmatics of design and appropriate implementation, generally must be borne out through trial of the software. Being a judge of software is as difficult as being a judge of character.
For 386BSD, we assessed two strategies for leveraging past work. The first was to translate driver requests into a series of BIOS commands, then support a mechanism to temporarily enter real mode to allow the BIOS ROMs to satisfy those requests. The value of this approach would be to obtain 100 percent compatibility with any PC-based system (MS-DOS has enforced this from day one). Had this been strictly a commercial effort, this strategy might have been satisfactory. For hard disks and display adapters, the BIOS mechanism has been quite successful in mitigating hardware configuration problems for users.
However, items important to a researcher using 386BSD, such as tape backup, networking, and serial communications, were not anticipated in the initial BIOS plan, because at the time, these things were believed to be far in the future. Also, IBM really only got serious about support for protected-mode BIOS with the PS/2 ABIOS, so even trying to leverage some of the BIOS requires the ticklish matter of switching from protected mode, and maintaining a context for the non-multitasking BIOS to run in while multitasking is going on around it. Clearly, this would require a colossal kludge, as the BIOS was never intended for anything but the vagaries of MS-DOS.
So, although there were tons of MS-DOS driver software available, we ultimately found little usable code without going well out of the scope of the project and markedly altering our specification goals (see DDJ, January 1991). To top it all off, our performance would be shot to hell, because code written for a 16-bit machine with 64-Kbyte segments doesn't leverage a 32-bit machine with a 4-gigabyte flat address space very well. Having already learned more than we ever wanted about ISA and 386, we had no incentive to add BIOS and DOS trivia as well. Thus, we bid a fond farewell to this strategy, fearing that the machine might become obsolete before it was fully mastered!
For the second strategy, we looked at drivers contributed to Berkeley which ran under UNIX on VAX, HP300, NS32000, and 386 PC machines. From this source, we were able to satisfy more than half of our initial driver requirements, and base our system on software that had some history of operating on another platform for a period of time. We could also pick and choose among a number of drivers for some devices. Ironically, the better drivers came from the less well-known machines.
The BSD operating system's kernel broadly interacts with its device drivers, depending on the kind of device and the nature of information it provides. Unit record devices, such as keyboards, terminals, modems, and printers tend to fall into one category of device drivers. Mass storage devices such as tape drives and hard and floppy disks fall into another category. A third category includes packet transfer devices such as network interfaces (Ethernet and token ring controller boards, for example). Bitmap display frame buffers could be considered yet another category.
Often, we would like our system to vary the ways we might configure or interact with these devices, depending on need. For example, the point of disk drives is to store and organize both small and large collections of data or programs, so it is inconvenient to interact with the disk drive on its terms alone (disk sector address and sector data contents). Therefore, we impose an abstraction which allows us to name (or key) collections of data as a file. This file system abstraction is the principle way programs make use of the disk. We still need to have mechanisms to access the disk as a whole, however, if for no other reason than to manage and maintain the file system (for instance, check consistency, backups, file recovery).
We could use a file system to organize a tape drive as well, and it might work, provided we don't mind waiting minutes for a file. However, tapes are more commonly used as archives and thus we impose on top of the tape data record formats, sometimes variable sized and with special hardware-generated records to denote file separators (or file marks) and end of tape indications.
Unit record devices have little in the way of data structure. The application program pushes data bytes through them for the desired effect. For the convenience of the applications programs, the system provides for a variety of mechanisms to facilitate optional input and output processing. Among these mechanisms is a kind of "super" or metadriver, called a "line discipline." The line discipline acts as an intermediary between the device driver and the operating system. The most common of the line disciplines is the "tty driver," which implements the semantics of the UNIX keyboard interface (that is, backspace, line kill, interrupt/suspend a process) for the user.
Network devices are quite different in nature. Incoming and outgoing packets are structured in elaborate and (usually) hierarchical ways. Not only is their content important, but so is the time and means by which they arrive. Also, unlike the other categories cited, a single data record may end up going to one of many different destinations, and this may be dynamically altered as the system software changes routing policies. Thus, the kernel's device interfaces may look quite different from the other categories.
Accomplishing bitmap graphics is reflected in another I/0 interface need. In this case, we must regulate access to the frame buffer's physical memory by arranging to map the memory into an application's (such as an X server) virtual address space.
Each of these categories interfaces to a different portion of the BSD kernel. Disk drives are interfaced into the file system of the kernel and into "device special files" (found in /dev), which allow utility programs to bypass the file system. These files are, in effect, trap doors out of the single UNIX file namespace and into a given device driver. Device-special files also allow devices in general to be operated by applications and utility programs. Network devices are connected to the network protocol processing mechanism and are only visible through the network software interface mechanisms. Thus, network devices don't show up among the device-special files.
Versions of UNIX prior to 3BSD had a rather fixed notion of configuration; systems were conditionally compiled for a given set of hardware or by manually altering the configuration flags in the driver. (Usually this was done to save on the amount of system code taking up space -- this was important if one had as little as 256 Kbytes, where every Kbyte counted.) If the driver was not there, but the hardware was, it could not be used. Worse yet, different systems had to be created for differently configured systems, even if they had minor differences in interrupt vectors, were missing a redundant card, or had conflicting controller port assignments.
Early 4BSD versions introduced a more versatile form of configuration that allowed for runtime configuration shortly after the system's kernel was loaded, but prior to operation of the kernel. The intent of this configuration mechanism was to put off wiring-down device-dependent information until the last moment, then attempt to discover as much of this information from the hardware itself and apply it to the drivers as needed. The prime motivation was to factor out as much of the idiosyncratic configuration differences as possible.
The goal of this work was to minimize the impact of maintaining a diverse number of computer systems and peripherals within a single version of the kernel. The more we can achieve with this the better, because the sheer volume of different kinds of devices that can be configured with systems now is enormous.
Even more elegant mechanisms to automatically configure the drivers for the given devices present have been developed over the course of time. "Autoconfiguration" was an early innovation in Berkeley UNIX, and it remains a hallmark of a Berkeley-derived version of UNIX to the present.
In our BSD kernel, we implement autoconfiguration by incrementally searching for all devices that might be supported by the drivers present in our kernel. This is accomplished by "walking" a table of device information to locate devices on our target system and calling a routine in each associated driver, using this information, to check for the presence of a given device. If this probe( ) routine finds a device, the driver can be wired into the system by applying the configuration information saved in the table. We can inform the driver of this, so that it can adjust its own parameters and "fine-tune" configuration by calling an attach( ) routine in the driver. (In some cases, the attach( ) routine may find a terminal conflict with the attempted device configuration, and may deny the configuration attempt.)
Sometimes we have a master device that manages a number of slave devices (a disk controller with multiple drives, for instance). In such a case, when we find a controller with the probe( ), we iterate through each possible subdevice that might exist on the controller by means of a slave( ) routine in the driver. If any slave devices are found, the attach( ) routine is called for each routine so the drive may be "wired" into the driver.
Depending on the computer, it's possible to do autoconfiguration with varying success. Sometimes, much of the device-dependent information can be obtained by the software cleverly manipulating the device to reveal how it is attached to the system. At other times, it is nearly impossible to detect the presence of a device. Worse yet, a hidden conflict between two mutually exclusive devices could cause them to interfere with each other. (This happens all too easily on the ISA bus.)
As a result of the configuration pass, a manifest of devices and related configuration information is tallied on the console device, so that an operator can observe what the kernel was able to find and make use of. This can be of great use in diagnosing dead equipment, especially if either a device known to be present in the computer fails to respond, or if a device known to be missing mysteriously shows up in its place.
BSD's current autoconfiguration scheme is rigidly top-down, not unlike that of a recursive descent parser. To begin with, all buses directly connected to the computer are probed successively. While examining each bus, all devices on a given bus are summarily probed, and in turn, all slave devices on a given controller device. But this approach has some drawbacks; we may not yet have all the device information at the time we succeed in doing the probe( ) for a device to attach( ) it then.
An alternate solution suggested by Chris Torek (LBL) is to change this arrangement and instead do successive "depth-first" probe( )s on all lower-level objects to discover all information about the device and its hidden requirements before committing to the corresponding attach( ). Thus, a more complete picture of a device's demands and conflicts can be obtained before we commit to attaching the device.
Yet another possibility might be using a two-pass, or "bottom-up" method, in which all devices, resources, and dependencies are found on the first pass in a kind of "survey" expedition. Having gathered a complete picture of system requirements, the second pass assembles the pieces as if they were Lego blocks, incrementally attaching them from peripheral to controller to bus to driver. A device can be said to exist by its driver if a complete, connected path is available.
Note that with a complete description of dependencies by either of these mechanisms, we don't need to tie down the processor's interrupts, special equipment requirements, or other resources -- except when we actually open the device -- so we don't have to configure solely at boot time. Thus, we could change drives with the driver file closed, and when it reopens, the system will discover the change and adapt accordingly.
The current BSD kernel manages to locate and configure devices upon boot-up because it must find (at least) the characteristics of the root file system, paging store, and console device, so that it can begin the most basic operation. Because it has to do all that, the reasoning goes, it might as well find everything else. This is adequate for most purposes, but should you wish to reconfigure a SCSI tape drive, for example, it's a bit of a pain to reboot the system. (Actually, configuration should be done on device "open" as well as during boot-up, but this is a lot of work to do correctly and hence is usually not done.)
Autoconfiguration does not stop with just finding the device. More than half the battle is accumulating all the information possible about the device, in order to properly attach it. As an example, let's try to capture a general list of possible information desired. This should extend beyond the needs of the ISA bus, because we may need to consider other buses.
Devices are usually found on a bus of some kind. In fact, it is not unusual for a computer to have more than one bus, or even buses of more than one type. EISA bus, for example is a kind of bus within a bus, with ISA devices working by one set of rules and full EISA cards working by a completely different set (for example, slot-independent vs. slot-relative). Thankfully, less common is a hierarchical bus arrangement, where bus adapters themselves are devices on buses. (There are DEC VAX machines that use this to a depth of two or three.) In these cases we need to know the description of finding the I/O port or memory-mapped control and status registers of the given device. We may also need to locate the shared buffer memory that display adapters and network interfaces may require. Some bus facilities imply sharing or arbitration among devices; thus, special care must be taken to avoid conflicts between devices.
The processor interrupt mechanisms, which usually differ with each style of bus, must be determined. Many new devices that support shared use among multiprocessors, or that have multiple data streams (such as disk arrays), possess hardware "mailbox" mechanisms to report their progress as they complete lists of operations that the driver may have in progress. As we demand higher aggregate data rates, the complexity of our hardware I/O system may require more elaborate mechanisms to synchronize the hardware with software, and these will necessarily need to be configured and managed by our operating system.
For mass data transport, we may need to find and allocate DMA facilities, which may be in the form of channels or dedicated buses. Some of these may require conflict mitigation and perhaps (in the future) bandwidth reservation. Some facilities also require address translation, as we take a large, logically contiguous transfer and scatter/gather it to a group of data pages (seemly) randomly disposed around the system.
We may have a device with no peripherals, dumb peripherals such as printers or terminals, or those with a master/slave sharing of responsibility. These devices have configuration-dependent parameters that may be set with hard DIP switches or soft configuration mechanisms. (Some manufacturers have caught on to the soft configuration approach. Newer Ethernet cards for the ISA, for example, utilize clever mechanisms to do this.) Disk drive capacity and geometry must also be determined. Modern peripheral standards such as ESDI and SCSI use standard methods to obtain this information. Some devices may have conflicts with others (for example, dual ported access of a single drive), and these must be uncovered. The revision of a given device and its diagnostics/disaster recovery mechanisms is also important information (for instance, does the disk drive use bad sector sparing?).
Within our BSD system, we usually subdivide disk drives into partitions that may contain different kinds of file system abstractions -- all on the same drive. To describe this and the disk geometry in a device-independent fashion, we use a "disklabel" embedded in the data on the drive. The actual location of the disklabel may not be standard across all storage architectures, but the contents and use of the information in the higher layers above that of the given disk driver itself is identical in all cases.
The data structure definition of the current BSD disklabel attempts to support a rather diverse group of mass storage architectures. As a final part of the autoconfiguration process, the disk driver extracts this data structure from the disk drive and adjusts its parameters, including drive partitioning tables, to reflect this information. The kernel uses this information to determine which portions of the disk have been set aside for paging, which have various file system types, and the underlying physical storage parameters implied (such as file system block and fragment size).
Up to this point, we have only outlined the information that the kernel of the operating system may require to configure itself appropriately. Many systems do this low-level configuration well, but few go beyond this after the system boots up and configures itself for use. Other configuration procedures, such as finding and mounting various file systems, attaching to various computer networks, and generally embedding itself into the fabric of the local and regional computer environment, are not usually done.
However, in this modern era of LANs, enterprise networks, and global internetworks, computer systems no longer stand alone. High-level configuration of resources has now become a necessity. As a result, one of the current trends in modern computer systems is resource discovery and management. The cost of systems management is usually calculated on a per-computer basis, and as personal computers and workstations replace dumb terminals, this grows to be a significant factor.
In addition, as the demand for better applications programs increases, more configuration information needs to be maintained per system. At the same time, manufacturers are being forced to grant more autonomy to computer usage groups and move away from the centralized MIS-management mentality that made the trains run on time. Managing what one consortium describes as the "Distributed Computing Environment" is going to be quite a challenge over the next few years.
Now that we have examined how BSD handles configuration, and understand the interface, we must study the other side of the question -- how to work a device on a bus. In the 386BSD porting project, the ISA bus was chosen for the initial port, as it is the most common bus available.
Before we can delve into the code, a review of the ISA bus is necessary. A driver's view of the ISA bus reveals the mechanisms we must create to work a device on the bus.
The ISA has an independent I/O bus, separate from its program and data memory bus, that is primarily used to twiddle the bits for the control and status characteristics of devices. It consists of 1024 discrete, byte-sized "ports," some of which can be accessed in twos as 16-bit-wide operations. Each port may be read or written, and a given device usually decodes (or implements) a block of them (8, 16, or 32). Some devices function exclusively through the I/O ports -- even the most common hard disk controller (which relies on "string" instructions that repetitively sequences data through a single port).
The ISA bus, having mere rudiments of configurability, relies in part on devices being at known port locations, and has no mechanism to discriminate conflicting devices that may have overlapping or mutually exclusive assignments (for example, it does not work). For those devices which do not have standard port addresses, freely assignable zones serve as catch basins in which to place them. Most cards have only a handful of alternative port assignments (each a different handful, of course), so avoiding conflicts with a fully stocked box can sometimes be a tedious puzzle. (This is often made more interesting when a hardware manufacturer cleverly decodes more ports than are documented.) This leads to the "scraped knuckles" effect, where the computer's chassis is laid open, and cards shuffled in numerous attempts to find the "holy grail" -- the correct combination of DIP switches, hardware options, slots, and cables. (All this, while muttering on the 45th attempted power-on, the immortal phrase from Bullwinkle, the patron saint of programmers, "This time for SURE!")
Suffering ISA definitely makes one appreciate EISA or MCA all the more, although ISA systems and I/O cards are still being produced in massive numbers. Hard to believe that so much work is still being done with a bus that was inspired by the Apple II, technological aeons ago!
Devices commonly have one or no interrupts; they rarely have more than one. Again, like I/O ports, there are "standard" assignments for common cards, but the situation is a little more desperate here because we have far fewer interrupts than ports. Depending on whether we have an XT or AT card, we can have as many as 6 or 11 unique choices of interrupts, respectively, out of a net 15 interrupts that the ISA PC fields. This selection is usually constrained even more because few cards allow more than a selection of two or three different interrupts. Also, each interrupt has a discrete priority above higher numbered ones, so choosing a different interrupt can alter the processing order of the interrupt (the lowest numbered ones always getting first billing).
The software has no independent way of ascertaining the association of devices with interrupts, unless it compels a device to interrupt when all other devices are forced mute. (This assumes that the device can be programmed to interrupt without external stimulus.) For electronics reasons, cards cannot reliably share an interrupt. Also, interrupts whose source is too brief to be recorded get unceremoniously deposited onto one of two interrupts, each of which may have a device connected as well. (These interrupts do "double-duty.")
Some devices use a portion of the dedicated region of memory resident on the ISA bus. This region is frequently called the "hole," as it slices the machine's RAM into base and extended memory. Unlike the I/O ports mentioned earlier, this memory is not usually used for device control registers, but for various other purposes. Display adapters use dedicated regions of this memory to hold their frame buffer (or, if in higher resolution mode, a "window" or segment of the frame buffer too big to fit in the "hole"). Network controllers often have shared-memory buffers that can be selected to steal a portion of this memory as well. Finally, the BIOS ROMs, also present in the hole, scan it to find other device ROMs to supplement its functions with. This is how display adapters retain software compatibility -- by extending the number of display modes available through the BIOS and hiding the actual register programming from view. Network and hard disk controller cards use this method to allow for initial loading of MS-DOS off the network or SCSI hard disk. As a characteristic of the ISA, this region of memory is apportioned by ad hoc rules and is the frequent bane of configuration.
Various devices implement the direct memory access mechanism of the ISA. Three 16-bit and four 8-bit wide DMA slave transfers to a single master are available for dedicated use of cards specifically designed to make use of them. An interesting feature of the original PC/AT was that a string instruction to move data for the disk controller was faster than the DMA channel, so the disk controller did not even bother to implement the DMA channel. Unfortunately, the standards for the ISA have been set by its progenitor, so the bandwidth hallmarks of DMA transfer are not present with this bus. Not surprisingly, because of the various restrictions, cards using the DMA facilities are not as common as with other computers. As with the interrupt facilities, the software has no direct method to determine which card is connected to which DMA channel. An even more critical failing for a 386/486 system that uses paging is the lack of a page map to do "scatter/gather" to the 4 Kbyte-sized pages that might be located at random physical addresses, yet consecutive virtual addresses. The DMA facility only works on consecutive physical memory, so the software must improvise a solution.
Having reviewed the key points of our ISA bus, the question becomes "How do we do autoconfiguration for 386BSD?" Luckily, this is not as involved an answer as one might think, because our little 386 ISA bus machine is guaranteed to have just a single bus with a maximum of a few handfuls of hardware devices that need support. (We only have 8 slots.)
First, we create a configuration table that allows us to encode the descriptions of where to find the devices on the bus, as well as wild card values that require us to go out and compel the device to interrupt to locate which interrupt it's configured for.
To find interrupts, we program the interrupt controller to allow us to poll the interrupt lines to check for activity on a given line when we probe for a device, and we wait for a sufficient period before giving up. With some notorious devices, we just wire them into the designated interrupt in the table and go on. For all remaining interrupts not found, an interrupt catcher table will reflect them to an error-logging service of the kernel, so we can note their occurrence.
Next we use a probe( ) entry, locate the device, and "prod" it into optionally generating an interrupt and a DMA request. Sometimes this can be subtle to write, because we need to determine if anything at all is present with the supplied parameters, yet we don't want to inadvertently trigger a device we haven't gotten to in our list of autoconfiguration table entries.
Occasionally, the only way to avoid these conflicts is by ordering autoconfiguration, as in the case of display adapters. Backwards compatibility with earlier software was required, so VGA and EGA display adapters would decode the older CGA/MDA addresses as a part of the auto-sense feature to support software that only knew of the older boards. If we probe for the existence of the boards in an oldest-to-newest order, newer boards will respond as older ones, thus confusing the situation. By checking in order of newest-to-oldest, we can associate the correct driver with the appropriate board, even though there may be some ambiguity.
As we find devices, we logically connect interrupts and DMA request signals to the associated drivers. With interrupts, we point the Interrupt Descriptor Table (IDT) call-gate entry to the assembly language stub routine associated with the driver. We then adjust the interrupt mask to disable interrupts for all devices in the group to which the driver belongs. (In the future, we will learn more about such interrupt groups.)
To complete the attach of the device, the attach( ) routine in each driver is called to configure the device appropriately for operation and to report relevant facts about how the device can be used back to the system. Network drivers manage to extract link layer addresses embedded in the cards and inform the network protocol portions of the kernel of characteristics.
Either at the time of attach, or at the subsequent open, disk labels are extracted off of disk drives, and the system can be made aware of the kinds of file systems used, including paging areas for virtual memory.
We've described the information our BSD kernel might wish to obtain from the hardware to configure devices, and what the ISA has to offer in this regard. The two are far from a perfect match. Much information is missing that we would prefer to have, and the situation regarding configuration conflict detection between devices seems almost hopeless. But this is assuming we have no hints at all about the bus; in fact we do, and we are compelled to use them.
For the more ancient and problematic cases (such as printer parallel ports that won't generate an interrupt unless a printer is attached and ready), we can force the configuration table to assume the interrupt associated with the device. Thus, if the printer is detected during a probe, the software will dutifully wire down the interrupt vector without verifying that it actually is attached. These limits are primarily due to the lack of information available because of the history of the ISA bus.
Much of our current strategy has focused around the ISA bus of our target machine. However, there are a number of machines which utilize other buses, such as the Microchannel (MCA), EISA, VME, or other non-ISA buses. To implement these bus types, this portion of the system would change greatly. While additional buses were outside of the scope of our project, we did not desire 386BSD to be limited solely to ISA, so the ISA bus-related code is a configurable option with a defined interface into the kernel. To add support for other buses, you can add the functionality in along side the current ISA code and use it as an example.
In the case of EISA (which extends the functionality of the ISA bus for new cards designed to this standard), such new code would be interwoven with the existing ISA autoconfiguration mechanism, as both would be needed to support old ISA and new EISA devices. For MCA, which uses a completely different approach to board configuration and is incompatible with ISA, the autoconfiguration and device drivers would be completely separate from the ISA code.
Now that we have reviewed autoconfiguration and its mechanisms, it is time to move on to actual driver operations, such as the enabling operation of the PC hardware devices, splX( ) (interrupt priority-level management), and the interrupt vector code. After this, we will walk through the code of some sample drivers, noting the important points in light of our knowledge about BSD autoconfiguration and interfaces. We will examine in detail some of the code required for the console, disk, and clock interrupt drivers. The basic structure, minimal requirements, and extending the functionality of these drivers through procedures such as disklabels will also be discussed.
Interfaces are a rather crucial part of an operating system, yet we've managed to avoid them up to now. How? Well, it wasn't as hard as you might think. A look at the heritage of operating systems might be instructive.
Many early operating systems were little more than a "bag" of drivers and subroutines to make use of them. The operating system provided the "common unifying" interface between hardware resources and the applications that consumed them. Initially, these early systems used a handful of physical resources (disk blocks, RAM, CPU) packaged as abstractions (files, address space, time slice) which an application would obtain and then relinquish to the system, as needed.
More advanced systems attempted to "multiplex" resources in an effort to manage resources more efficiently among a number of competing applications, in order to get the most use out of expensive hardware (in other words, amortize the costs over widespread use). As operating systems began to contend with networks, data exchange formats and conversion, and standard programming languages, the size and extent of a user's reach extended beyond a single machine. Computers begot more computers, and only then did the issues of resource sharing and interface standardization become worthy of notice.
Because resource sharing/multiplexing was done primarily for cost reasons, and only secondarily for convenience and cooperation with other users, it has gotten second shrift from designers and standards groups, until now. However, so many conflicting approaches exist that it appears hopeless that there will ever be "a standard operating system," let alone "a standard computer architecture."
The modern bane of technology is that as complexity increases, it starts to overwhelm and blind us with its bulk. As this occurs, we are required to deal with the more microscopic elements in ever greater orders of magnitude. Constant improvement in our algorithms, mechanisms, and paradigms are the only way we can ever hope to mitigate this deluge.
And we have not even mentioned the new demands on the frontiers of development, in which video and audio signals, representing hundreds of megabytes per second of bandwidth, need to be channeled, processed, and combined for multimedia purposes. Nor have we mentioned the need for cooperative multiprocessing applications.
Future operating systems challenges will be quite different from those of the past. The economics of computers no longer require us to use whatever means necessary to save a handful of bytes here and there. We can opt for a direction that leads toward increased clarity and scope, instead of recreating a new version of the old. Paradigm shift sometimes allows us to take a step back and recognize that the tree leaves we were previously staring at really are part of a forest.
However, even at this stratospheric level, the operating system still retains its original heritage of being a "bag of drivers." Damn elaborate drivers maybe, but drivers nonetheless. In a way, we have been discussing various aspects of the driver interface all along, because UNIX is the interface.
--B.J. and L.J.