RyukOS: An Embedded Operating System
RyukOS is an embedded real-time operating system that implements a novel socket-oriented system call interface. Through the socket API, RyukOS is able to provide a familiar interface, robust inter-process communication, and real-time task scheduling while minimizing expensive context switches.
Documentation and Notes
There are four places where one may find my notes and documentation as I slowly churn out bits and pieces of RyukOS.
- misc.txt- Just a text file in the RyukOS repo where I dump various ideas I have while actually writing the code. Some of the things in misc.txt will be implemented, others are pipe dreams or just plain stupid.
- Flatlanders- Flatlanders is where I will be putting information that is more set-in-stone than misc.txt, but too specific to put in the readme. This includes design specs, planned protocol support, etc. If this project somehow develops its own little developer community, I hope to host it on Flatlanders.
- README.txt- Shouldn't need to explain this one.
The final place to get a foray into the RyukOS design process is my notebook and the half-inch-thick stack of loose pieces of paper coated with notes about ideas, requirements, and whatever else I might have scribbled down at a moment's notice. I can't even read half of it anymore. This is the kind of asinine process that's going to keep me out of the big time for the entire extent of my career, but whatver. It works.
Plan9 and UNIX have files. Redox has URIs. Windows has spaghetti. RyukOS has sockets. Almost all system calls are made through an interface similar to that of the ubiquitous Berkeley sockets.
Because RyukOS talks to a lot of different hardware peripherals, many of which are meant to communicate with other devices, a connection-oriented system is guaranteed to pretty closely match many of the functions the RyukOS kernel will need to perform on a regular basis.
There is nothing implicit in a file-oriented structure that facilitates real-time input capture. In a real filesystem, there is typically one task in control of that file at a time- if the file's content changes, it is implicit in the nature of a file that this change must have come from the writer of the file. However, a digital input pin changes according to external stimulus. If a task needs to quickly respond to changes on a digital input, it must regularly poll the same one-bit file, over and over, until it sees a change- not the best allocation of resources in a real-time system. However, in a socket-oriented system, where system resources are exposed as servers, a low-level digital IO server can take all requests to "listen for changes on pin X," and ensure that those tasks get scheduled when needed. This eliminates needless context switches without sacrificing the responsiveness needed for a real-time operating system.
Files simply don't exist on many embedded systems. Those that do have files may only have a small handful of them. As such, these gotchas of things-that-aren't-files in a file-oriented system overwhelm the advantages that a file-oriented system may otherwise offer to the user. Furthermore, with files comes file paths- long strings with a length that can't be pre-determined. With a socket, I can be highly restrictive about the amount of memory used to namespace things, without it feeling restrictive. It's still just the plain old sockets we've always known, after all.
The socket interface allows dynamic creation of new types of address families, by binding a socket of the AF_AF address family. Thus, low-level machine facilities can be loaded and unloaded. RyukOS is extremely primitive as a whole (give me a break, it's my first time writing an OS and I haven't even gotten halfway through college yet), without any concept of processes (only threads), so the depth to which this idea can be explored is quite shallow. However, it is a proof of concept- its design is brutally simple and it is enough to show that this just might work. This is an idea that the creators of the socket interface brushed shoulders with when they conceived of the AF_UNIX address family, which facilitates connection-oriented inter-process communication. Since RyukOS doesn't have processes, this is sort of a moot point. However, because RyukOS is meant for systems that have extremely limited computational power, yet still have a ton of little bells and whistles, RyukOS is a great place for testing out dynamic creation of device drivers. The Cortex-M4 I am using offers a slew of zany hardware features (the complete reference manual is nearly 3000 pages, about 2700 of that is discussion of hardware peripherals by my reckoning), yet offers scarecely more computational power than a Pentium Pro. This gives me the ability to thoroughly test the idea of dynamic drivers, but if I become complacent and write inefficient code, the platform will be punishing.
Lessons Learned (so far)
- Make it work, make it clean, then make it fast: I used to have a tendency to get these all out of order, until this project beat that habit out of me. The first implementation of preemption in the RyukOS kernel kept the system tick handler on the stack when it yielded back to the kernel, then another thread would run that only contained a yield, then that yield popped the system tick off the stack, then we could actually switch to the next task. That's a nasty hack if I've ever written one. But it worked, and it let me figure out the basics of the scheduler. Now, it's clean- the second-thread-yielding hack is gone. Once it was clean, and I had fixed the context switching hack, I made it fast by completely replacing the scheduler with an EDF scheduler I was designing separately from the RyukOS codebase.
- When in doubt, take stuff apart: I struggle to understand things when they are presented wrapped up in interfaces and object models. When I can see what registers are being written, and I can cross-reference that with a datasheet, the big picture becomes much more clear. Granted, your mileage may vary, but I learned more about ARM by spending a day trying to use it without CMSIS than I did in months of using CMSIS.
- Microkernels don't work for microcontrollers: ...unless you want your OS to be slow, and you want your OS to underutilize hardware features. Different manufacturers implement all kinds of wild features in their peripherals, and that list of features varies from product to product. As such, an effective universal hardware abstraction layer must be able to account for all of those combinations of features, while not being wasteful of memory or clock cycles. Needless to say, that's impossible, unless the interface is massively complicated- at that point, it's not even a microkernel anymore, it's either a crappy monolithic kernel that doesn't implement any valuable services on its own, or it's a crappy hardware abstraction layer that has a bunch of bells and whistles that do literally nothing depending on the target device. Hardware integration of network stacks and other device drivers allows them to maximize the potential of the hardware. When you're lucky to be running at 120 MHz with 200K of RAM, that's a big deal. When you're trying to implement real-time behavior and need things to work in highly deterministic time, it's not just a big deal, it's mission-critical.
Should I consider using RyukOS over FreeRTOS/Contiki/etc?
Short answer: probably not.
Long answer: RyukOS' primary purpose to me is to be a playground. It's small, simple, and aggressively abstracted, which makes it a nice platform for me to put some of my more ridiculous ideas to the test. While RyukOS works, not everything works well. The things I've learned from RyukOS have been applicable in much of my work, but before I can apply these lessons to RyukOS itself, it will need a complete rewrite.