Title: Altair OS Part 2
Date: March 27, 2021
Tags: altair programming
========================================

To get started, I had to answer some basic design questions.  There are a couple
ways of handling things in an OS.


# Handling IO #

One of the first things I had to decide when thinking about designing the OS was
how to handle IO.  Altair hardware (or the hardware I have) could operate in two
modes:  polled or with interrupts.

Polling is when you repeatedly ask the hardware if there is data available or,
in some cases, telling the hardware you want data and waiting there until some
arrives.  Alternatively, interrupts allow the hardware to tell the program that
there's data waiting as soon as it arrives.

CP/M used polling and left timing and control mostly up to the program.  The
program would ask CP/M for data and CP/M's routines would go off to the hardware
and wait for it then give it to the program and let the program continue running
once it arrived.  If you wanted more data, you had to keep asking CP/M for as
much as you wanted.  If data never came, the program would never continue.  If
data came but the program wasn't waiting for it, it could be lost.

Timesharing BASIC is an example of a sort of operating system on the Altair that
used interrupts.  This allowed BASIC to manage input from multiple terminals and
differentiate which environment it belonged to.

There's a couple of ways for interrupt driven input to work.  The 8080 processor
has only one interrupt line.  A 2SIO board, for example, which has 2 serial
ports, can be configured to trigger the 8080 interrupt line on data input to
either port.  On an interrupt, the default handler will be called, which resides
at address 070Q.  The handler, if it's expecting more than one serial port to be
in use, would have to check each one to find where the data came in and then
deal with it appropriately.  The handler could deal with multiple boards and
different devices but a problem with this method quickly becomes obvious.  What
if data comes in on a second port and triggers the interrupt again?  If we had
disabled interrupts while running the handler, we'd miss it.  If we didn't, we
might stop the handling of the first byte of data after reading it, but before
doing anything with it and execution would go back to 070Q to grab the next
byte.  Either way, we risk losing data.

The other way to handle interrupts is with another piece of hardware, the Vector
Interrupt board.  The Altair clone I started with emulated one.  I wrote about
the real-time clock part previously[0].  The boards are being re-manufactured so
I built one for my 8800c.  Timesharing BASIC used the VI board to keep track of
each user's terminal input.  The S100 bus has 8 interrupt lines, even though the
8080 CPU couldn't use them (other CPUs like the Z80 could).  The 2SIO and other
boards could have their interrupts tied to one of those lines.  The VI board
would read the interrupt lines, trigger the CPU's interrupt line then put the
appropriate handler address on the bus so it would be called:  000Q, 010Q, 020Q,
etc. through 070Q.

I believe the original UNIX was interrupt based.  FUZIX is.  Modern hardware and
operating systems mostly are.  I decided I would use interrupt-based input so I
could handle multiple users (if I ever get that far) and to prevent loss of
input data which I'll talk more about later.


# Writing a kernel #

What makes an OS "UNIX-like"?  What do I need to implement?  Well, the first
thing I think of when I think of an OS is that it has a kernel.  That kernel
provides the abstractions for talking to the hardware and keeps track of system
data.  UNIX has a file-like abstraction for hardware.  Most everything can be
read from or written to in the same way as a file.  The program doesn't need to
know what type of hardware it actually is.  The data the kernel might track are
the devices and files opened by the program, system up time, multiple processes,
etc.  The kernel provides system calls with a defined method of passing
parameters and returning results as the means of asking the kernel to perform
operations on behalf of the program.

My first pass and proof of concept was more CP/M-like than UNIX-like.  I created
a kernel that ran in kernel-space separate from user-space where the programs
run.  I had simple getchar and putchar syscalls.  I used interrupts to catch
user input from the terminal and the real-time clock to keep system up time.  To
test, I wrote an echo program similar to my first one[1] and a sleep program to
test time keeping.  Sleep started out as a syscall but I then changed it to a
call which gets the current up time and moved sleep into user-space.  I didn't
want to sleep in kernel-space and this is what modern UNIX has.

To start, user programs were still doing most of the work.  They needed to know
which serial port they were interfacing with and they had full access to
everything.  I've been expanding kernel functionality and updating the test
programs over time.  The getchar and putchar calls were replaced with read and
write that takes a file descriptor (which the program had to know).  Then open
and close were written so programs could request access to a serial port and get
the file descriptor back from the kernel which now keeps track of them.  Then I
enabled programs to open devices as files by name.  I created a fake file system
(I still don't have disk drives, yet) with the system's devices hard coded.
/dev/tty0 and /dev/tty1, for the 2SIO board.  The kernel also handles read and
write permissions on devices.

The file-like interface to the serial ports is only part of the equation for
file-like things that aren't actually files.  Devices have additional attributes
called the major and minor number.  These numbers tell the kernel where to go to
handle the data being read or written.  I don't know what this actually looks
like inside a UNIX kernel, I haven't looked at code to find out, but basically
the major number is the type of device, what driver it uses, and the minor is
which port on the device.  I just put the serial ports at device 1 so tty0, the
first port of the 2SIO board, has a major,minor of 1,0.

This abstraction allows me to add additional 2SIO boards without changing much
in the kernel to handle it.  Additional "files" will need to be added to /dev/
and handlers for the hardware addresses of the ports.  The bulk of the read and
write commands don't need to change at all.  They simply index into the list of
handlers for the major number to go to the correct driver which then indexes
into a list of available ports based on the minor number.  However, because 8080
opcodes for IN and OUT use immediate values for the address to read or write,
they can't be held in registers or memory locations so a generic handler can't
be written.  If the addresses change, the kernel has to be reassembled (or the
machine code updated in memory).

The kernel handles all of the interrupts.  As data comes in, the interrupt
handler runs that stores the data in a buffer.  Currently it's only a single
byte but expanding that is one of the next things on the TODO list.  The program
can then read the input when it's ready and pull it out of the buffer.  If the
program wants more bytes than available, currently it waits until it gets all it
wants or until the end of a line.  There are different buffering schemes and
often line buffering is used for user IO.  A disk buffer might be based on
sector or block size.  The number of bytes read is returned to the program so it
can decide if it got enough or if it should ask for more.  User input would be
of unknown length so reading just until enter is pressed is a good idea.
Structured data often includes its size at a known byte so you know when you got
the whole thing or if you need to keep waiting for more.


I'm currently finishing up debugging the open syscall and I need to test close.
After that, I want to add a larger buffer to the input so data won't get lost.

I am running out of things I can do without disk IO.  I need to get my Dual IDE
board working and figure out how filesystems work.


[0] gopher://kagu-tsuchi.com:70/0/blog/articles/8080_RTC_timer.txt
[1] gopher://kagu-tsuchi.com:70/0/blog/articles/8080_IO_echo.txt