AngelOS
Recently I was explaining streams to a coworker. For the sake of simplicity I described them as essentially files, you write data to it and anyone looking at it can see that data as it gets written. And I've been kinda stewing on that for a little while.
So anyway, I had a terrible idea. How about an OS built using streams instead of files? Could it still be considered UNIX-like? How stupid would this really be? I kind wanna do it...
Problem 1: I don't know where to even begin writing a modern usable OS.
Problem 2: it sounds like a lot of work. Which I'm allergic to.
So instead what about an RTOS? I love working with embedded firmware, plus I've been mucking about with FreeRTOS lately so I have a fingernail's grasp of the inner workings. Besides, not knowing how to do something hasn't stopped me from doing the thing yet, and I'm not about to start letting it.
So ok, an RTOS based on streams. You're going to need some way to keep track of streams and tasks and everything else an RTOS needs, but without a filesystem. So a registry then. A registry of streams and their associated data. I think I'm going to call this thing AngelOS, because it'll violently come apart and destroy everything around it if you look at it wrong.
Types? Who needs em
Streams need to be registered through the RTOS equivalent of a syscall, which I'm just going to call syscalls because I want to. I also don't want any restrictions on registering streams, as long as you call the stream registration syscall, it should work. Streams are also not typed. This may seem a little surprising, but I feel like it will make things simpler. After all, a stream is just a, well, stream of bytes. It doesn't really care if those bytes make up something or not, you can put in whatever you want and you can take out whatever you want.
There's a couple ways streams can be used. The most basic method is floating streams. These are registered streams that are explicitly written to or read from. Another method is with listeners. Listeners are registered tasks that are run using a virtual interrupt when new data is written to the stream.
This presents a small problem with stream consistency. If you register two listeners to a stream, how do you ensure the stream reader location is not changed by one of the listener events before others get to run? Short answer: I don't know yet. We'll get to that.
This sounds like an event
Basically, yeah. The reverse of this would be a conventional async "block task until" for streams, but this gets a little messy when dealing with multiple tasks waiting for data. I think you'd either have to have to poll the stream or have the kernel keep track of which tasks are waiting on what, in which case you might as well use listeners. Maybe I'll expand more on why I don't like this in the future, but for now I just kinda like a registered listener system better. Theoretically a similar virtual interrupt driven event registry system could be built for semaphores as well.
Multiple listeners sounds at first like a nightmare, but there are times when it could be useful. Imagine creating a task that monitors a button. As soon as the button is pressed this task trips a semaphore (or writes to a stream) and then handles whatever else it needs to do like software debouncing. A registered listener system lets you hook as many other listeners to that event as you want.
I don't think you don't have to use listeners though. Conventional async access should still exist for floating streams and semaphores. This mechanism will be described once we start discussing ticks and scheduling.
Built in streams
There's a few streams that are registered as part of the kernel. Two of these, which I'm sure you'll see are completely original and in no way stolen from anything else, are the stdin
and stdout
streams.
You might see how the registered listener system shines in the light of this new information. If you write to
stdout
, where does it go? By registering a listener for thestdout
stream you can manage how your standard output prints out to the user. You want to write it to a display? Print it to a serial terminal? Sure!
Consistency across time
AngelOS is preemptive, meaning if a new task is registered with a higher priority than the currently running task the scheduler will switch to the newly added task on the next tick.
FreeRTOS includes the concept of semaphores and mutexes. The FreeRTOS docs do a much better job of explaining these than I could ever do so I'm not even going to try. Regardless, AngelOS needs something like these.
I'm not sure how the implementation of these will look yet. Probably something like registering them in the registry and accessing them from there.
Mutex
Semaphore
Multiple consistency semaphore
These are a bit special. It occurred to me when thinking about stream listener consistency that it would be handy to have a direct way to receive a state signal from multiple sources. A multiple consistency semaphore then is just a number of flags that all must be set. This isn't strictly necessary but I do think it could be convenient, especially for syncing multiple threads.
Inconsistency across time
The tick increment function is going to need to do some processing. It starts by checking for any virtual interrupt flags and ends with task context switching. If an interrupt is found, the task associated with the registered listener will be created. The priority of these tasks is set when registering the listener and can be set to any valid priority. Usually, you will use the default idle priority so that the listener task runs "concurrently" with other idle tasks. However, you are not limited to the idle priority. If you want a short lived listener task to run to completion you can give it a high priority value and it will preempt any current running tasks once the tick function switches task contexts.
Ensuring you do the interrupt checking before any task priority comparisons means there wont be a "lag time" of one tick when firing virtual interrupts. Which is important in a, you know, Real Time OS.
Tasks also have a weight value in addition to their priority. This doesn't have to be changed from the default, but in some cases you might want one task to be scheduled more frequently than another while still allowing other tasks to run concurrently.
I think async syscalls could work by having the syscall kick the task into a "descheduled" state if there is no next value. This state lasts up until the next tick and tells the scheduler to swap the current task context to a different task. At that point the suspended state is cleared and normal task selection can occur. Once the scheduler decides to reschedule tasks, this task is open to being scheduled again. If a next value now exists the task can continue, otherwise the task gets descheduled again. To avoid tasks with the same priority swapping every task we need to be able to override the "minimum tick count" for tasks.
This all means a task object needs some metadata associated with it (aside from the normal task information that the scheduler needs):
- PID (task ID, named "PID" for ease of use. No need to reinvent the wheel all the time, eh?)
- Name (maybe, not sure if it's necessary yet or not)
- Suspended (semaphore)
- Deschedule (semaphore)
- Lock (semaphore)
- Minimum tick count (optional)
You might be wondering what that "Lock" semaphore thing is in the task metadata. That is a way to prevent descheduling of the task. It's a dangerous flag because it can absolutely lock your system, but necessary for tasks that must be able to ensure they run continuously for a short amount of time. An example of this is an IO task, if you're awaiting a peripheral response you don't want the scheduler to suddenly deschedule your task at an inconvenient time.
Signaling the scheduler
Speaking of locking tasks, we need a global way to turn off virtual interrupts. This is a global semaphore that works just like a CPU's interrupt enable register and forces the tick increment function to put off any virtual interrupts. Once the interrupt disable semaphore is cleared, all pending virtual interrupts will be processed.
We also need a way to run the scheduler task next, though this doesn't have to be a semaphore as it's only accessed by the tick increment function.
When I talked earlier about virtual interrupts in the tick increment function I kind of glossed over the creation of the new listener tasks. That can't happen in the tick increment because that function needs to be as fast as possible. Instead, I think it can happen in the scheduler's default idle task. When a virtual interrupt is raised, the tick function notices and clears it immediately in preparation for the next tick. However, it also ignores the usual task scheduling and immediately switches contexts to the scheduler's default idle task where the listener tasks are created.
This default idle task also bears some explanation. I'm also glossing over a little bit on how to signal that listener tasks are ready to be created. I'll get to all that later.
The minimum tick count for tasks is also globally defined, though it can optionally be overridden by tasks.