Designing an operating system

illustrations illustrations illustrations illustrations illustrations illustrations illustrations
post-thumb

Published on 10 August 2023 by Andrew Owen (6 minutes)

If you read my recent article on implementing DLLs for the Z80 CPU, you’ll be aware that I’m designing an operating system called SE/OS. It’s a component of the firmware (System 1) for the Chloe 280SE FPGA computer. Up until recently I’ve been doing so in something of an ad-hoc manner. But I’ve reached the point where that’s no longer good enough. After asking some of my developer friends for guidance, I was directed to Andrew S. Tanenbaum and Herbert Bos’s “Modern Operating Systems”, now in its fifth edition.

The book focuses mainly on UNIX (including macOS) and Windows, although it does mention related operating systems such as Linux and VMS. Modern operating systems are multi-user, multi-tasking, multi-threaded and, more often than not, run on 64-bit CPUs. System 1 is none of those things. Despite that, I borrowed a copy of the book and read Chapter 12: “Operating System Design”. It turns out that there is no universally agreed upon way to design an operating system. But it does help to have a clear vision of what you are trying to achieve. Tanenbaum and Bos list eight issues that make designing an OS harder than writing an application.

  1. Very large program: System 1 has a memory footprint of 40 kilobytes and uses even less at runtime.
  2. **Concurrency: System 1 is single-user and single-tasking, so it only has to manage multiple I/O devices. It does this using a system of channels and streams.
  3. Hostile users: System 1 is not networked. It can boot into a minimal system without a disk present. Firmware updates are validated before they are installed.
  4. Sharing: System 1 uses the FAT-32 filesystem which makes it easy to move data to and from the system.
  5. Future proofing: System 1’s core is likely to reach a final stable state in a short time frame.
  6. Unknown use cases: System 1 is designed to be extensible without recompiling.
  7. Portability: System 1 is modular and entirely open source.
  8. Backwards compatibility: System 1 provides a BASIC interpreter that can run the majority of classic BASIC programs.

Tanenbaum and Bos also list three guiding principles.

  1. Simplicity: System 1 achieves this in a number of ways, starting with well-organized code.
  2. Completeness: System 1 provides all the features required to make full use of the hardware.
  3. Efficiency: System 1 provides a core set of features and system calls.

System 1 follows a top-down design with the BASIC interpreter as the primary interface. This also provides an integrated interface to the DOS in place of a stand-alone shell. The DOS itself is something of a “black box”, having been reverse engineered from esxDOS 0.8.5. But the system calls abstract the filesystem so that the DOS is only called indirectly. System 1 is more like UNIX than Windows, in that it uses an algorithmic execution paradigm and a unified data paradigm (using channels and streams).

The system call interface uses a standard set of CPU registers and preserves the state of other registers so that the developer doesn’t have to. It’s limited to a core set of functionality, but provides a mechanism to load dynamic libraries. Although the DOS uses connection-oriented file access, the system call interface also provides a connectionless layer for certain file handling operations.

System 1 has a layered system structure. Unlike modern CPUs, the Z80 doesn’t support separate kernel and user modes. Memory protection is achieved by running part of the system in read-only memory (ROM). Functionality running in ROM includes the BIOS, DOS, interrupt handling and vectored system calls (grouped by version and functionality). Parts of the system stored in RAM include:

  • abstracted file handling routines
  • code page (font)
  • device drivers
  • dynamic libraries
  • error message localization
  • keyboard localization
  • system variables
  • user-defined screen mode

In its implementation, System 1 has some similarities with the Apple IIGS system software. Both comprise:

  • DOS: ProDOS, UnoDOS 3
  • interface: Finder, SE Basic IV
  • system calls: GS/OS, SE/OS

UnoDOS 3 provides its own low-level API (inherited from esxDOS), but typically it’s not necessary to call it directly and SE/OS system calls can be used instead. UnoDOS 3 uses the FAT-32 file system but SE/OS provides an abstraction layer. In SE Basic IV, folder and disk names can be 1 to 11 characters while filenames use the 8.3 format. Spaces in filenames are converted to underscores, but underscores are displayed as spaces.

Inspired by BeOS (succeeded by Haiku) and NeXTSTEP (succeeded by macOS), SE/OS provides several API kits such as:

  • audio
  • console
  • storage
  • games
  • graphics

The storage kit includes support for loading dynamic linked libraries (DLLs) and processing data in Resource Interchange File Format (RIFF).

SE/OS applications are stored as a collection of files and folders within a folder in a top-level folder called PROGRAMS. Storing applications in this way instead of as a single executable file is a feature shared with RISC OS and NeXTSTEP (and its successors including macOS).

Application names can be any length and contain any valid FAT-32 filename character. In SE Basic IV, applications are launched with RUN "MY APP NAME" or !MY APP NAME. The application name is truncated to derive the folder and binary filenames and spaces are converted to underscores.

/PROGRAMS
|--------/MY_APP_N.AME
         |------------/PRG
                      |---/MY_APP_N.PRG
                      /RSC
                      |---/RESOURCE.BIN
                      /SRC
                      |---/MY_APP_N.ASM

The RUN command sets the stack pointer to $6000, loads the binary (PRG file) at $6000, changes the path to the resource (RSC) folder and sets the program counter to $6000.

If the binary fails to load it restores the stack and falls back to BASIC. This means the binary can be up to 40 kilobytes in length. The binary is then responsible for loading its own resources. The method for passing parameters to the application is to define a variable in BASIC.

This approach has a number of advantages over the single executable file method. For example, with multi-lingual software, only the selected language resources need to be loaded. It is also easy to customize the application without the need to recompile it.

In conclusion, I learned that I would have saved a lot of time if I’d started with the OS design. And in retrospect, it would have made more sense to start the whole Chloe 280SE project with a clean sheet. But other people have done that. Instead, the project started out in 1999 as an attempt to update an existing 8-bit computer. It’s built on a collection of components including the T/S1000 BASIC, Timex 2068 video modes, ULAplus palettes, esxDOS, and a ZX-Uno FPGA core. As such, I’ve been fortunate in that most of the design decisions I made were dictated by constraints that were beyond my control. And the Chloe is now greater than the sum of its parts, having grown into a unique system in its own right.