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