On Writing an Operating System
While pondering what progress could be made in programming languages, I delved into how one might write single process, concurrent code to build systems with many simultaneous clients. It seemed that a stumbling block to feeding such a system was the weird mismatch with Unix and its mostly blocking API.
So, I did the only reasonable thing and started to build my own operating system. It started simply enough with a visit to OSDev.org and wondering if I could just make something say Hello World.
, and then maybe some keyboard input… and a VT console emulator (multiple virtual), well then I needed serial ports to send out diagnostics, and then a TCP/IP over Ethernet. All that meant I needed a filesystem like thing to store my executables and loaders for the executables and some interpreters to run… Lua, then Wren…
Mini-Moral: Don't visit OSDev.org unless you have a few free years.
Stubborn Guiding Principles of My OS
- Force concurrent programming to be the best way. This is to force me to encounter the warts and problems and resolve them or document them and live with them.
- Don't have a full C Runtime Library and POSIX. This will keep me from falling back into the easy, decades old solutions.
- I can make C do concurrent programming, but it really hurts me. For the low level code user mode code I wrote a concurrent programming library for C, but it is really a pain to use it. This is not the answer, but it let me try out some program architectures by implementing the various OS bits and bobs in different styles, if somewhat clumsily.
- I want the user mode programs to be in a safe, high level language. There just isn't much point in programming in a language that finds bugs at runtime when you build cycle is "build new OS image, reboot computer, test". There isn't much point in a language that finds its bugs at run time anyway, but certainly not here.
- Communication between user mode programs must be fast and clean. Ideally there will be zero OS intervention in the communication. (except some scheduling when a process blocks on all of its IO options). I don't want IPC costs to impact the architecture. If lots of heterogeneous processes is the answer, then that's the way I'll go.
Questions You Might Have
What is it called?
For a long time it was named os
. But I recognized that was going to be a ungoogleable. Eventually I spent a day and named it "osy", because that was short, googleable, a nod to various simple OS names over the years, and clearly one better than OS X. Apple changed their OS name and ruined my joke. I'm keeping the name.
What kind of OS is it?
I don't want to call it a micro kernel, but the kernel does processes, memory management, scheduling, some primitive bootstrap facilities that are done better in user mode once we get up, and a couple of devices that kind of have to be done in the kernel (interrupt controllers and timers).
Have you ever heard of L4?
Yes. I wasn't aiming that way, but I did land pretty close to L4.
What is the kernel written in?
The kernel is ~6600 lines of C and 400 lines of assembler. That for ia32 and x86_64 architectures, though I'll throw out the ia32 next time I'm in there.
I use a number of Clang C extensions to great effect. Between cleanup and overloadable I end up with much more readable code, and cleanup in particular eliminates a whole class of errors.
You seem to have left out a lot of stuff, like devices.
Not a question, but yes. That is all done out in user mode processes. Not untrusted, if you are going to give a process enough access to enumerate your PCI address space, then you are trusting it.
The goal is to make use of the large numbers of cores available in modern systems by splitting tasks among them vertically as well as horizontally. IPC is cheap by design, and we'll see where that leads us in the solution space.
For now Ethernet, IP, ARP, UDP, and TCP are each a separate process communicating as needed. Surely that isn't the ultimate answer, but it lets me stress the communication. Oddly, it might be the answer for the general case, but acknowledge that UDP and TCP arrive as if by magic through off chip accelerators on high performance network cards.
There still seems to be a lot missing.
Most of the work ended up being in user mode. There I struggle with different models of concurrent programming and try to decide what I want.
I've been through C, Lua, and Wren for my user mode code. Device drivers are all in C, but are pretty simple in architecture so that works out. Lua was quick to port, but ultimately it isn't a sturdy enough language to make me happy. Wren was trivial to port and I like it, but I keep having odd performance issues I can't pin down and I've decided I need a real compiled language if I'm to make any judgements about performance.
I spent a couple weeks trying to port Swift to the OS, but was ultimately defeated by the thousands and thousands of lines of CMake with endless build variants and strange special case rules for OSes. So I went back to the "language" that started this blog, started over using Swift to write the compiler, and will use that to make my user mode programs. I just need to understand what the language is generating, and be able to alter the language to handle the concurrent communication without descending into a see of language warts.