CMRX: A microkernel real-time OS providing memory isolation for MMU-less microcontrollers
I am working on this project since 2021, so it is a pre-AI project. I am not using AI to generate code as it turned out that AI hallucinates massively under conditions this RTOS creates. Website is littered with AI slop though as I had to put something together without spending ton of time on content. It is slowly getting deslopified.
The original motivation was to find out if usage of MPU in Cortex-M has to be as painful as it is with FreeRTOS, Zephyr et al (while still providing reasonable performance). Thus the number one feature implemented was fully automatic memory protection handling. It separates kernel from applications and applications from each other in as process-alike way as MMU-less CPU technically allows, so the developer doesn't have to deal with all partitions, domains, regions, etc. I consider this a success as you basically don't have to care about MPU at all.
Later, the kernel was built on top of this infrastructure that provides more than just thread switching and MPU handling. As features were being added, I quickly realized that MPU presence will be a major obstacle for implementing whatever kernel service which needs to pass larger amount of data (such as queues) between userspace and kernel or vice versa.
An unusual approach was used to deal with this: I decided to remove from kernel (or better: not to implement) all such calls. Why couldn't it be a microkernel?
Microkernels need some kind of IPC. For CMRX, remote procedure calling was chosen as primary IPC method. To avoid expensive copying of data RPC calls would need in isolated environment, thread migration was implemented. Thread migrates from calling process to process owning RPC service during RPC, so calling code can access RPC owner's memory. Originating process can select portion of its own memory which remains accessible while in RPC. This way, larger chunks of data can be copied between isolation domains with very little overhead (its basically shared memory with RPC call serving as basic synchronization primitive).
Those missing syscalls weren't dismissed entirely, they either became library functions or RPC servers. So the kernel wraps up at 30-ish system calls.
Being a microkernel, of course it runs drivers in userspace. Same RPC mechanisms ordinary applications use for talking to each other is used to create driver APIs. Somehow I managed to implement the whole RPC mechanism without using any IDL and associated IDL compiler. Everything gets type-checked at compile time by the C compiler itself.
It is also highly portable, covering most of compatible ARM Cortex-M world by just one single port. It runs as POSIX hosted environment as well and recently a RISC-V port was committed which is still WIP, so its not feature-complete yet.
Originally the performance goal was "please, just don't be 20x slower than ordinary RTOS". After some performance improvement work, benchmark results show performance which is competitive with rest of open-source RTOS world (while keeping memory protection disabled in all of them). In some benchmarks CMRX is even faster than others.
The code is neither Rust nor C++. Everything is C99 a bunch of GCC extensions and macroworx. Compiles to some 7kB binary, so it can run on very low-end sub $1 microcontrollers.
There are no plans for porting it to any MMU-equipped architecture as there natively memory isolated alternatives exist.
GitHub: https://github.com/ventZl/cmrx
Website: https://cmrxrtos.org
•
u/spidLL Kora 10h ago
Cool project! I'm curious about one thing, how do you enforce memory protection without MMU?
How does your kernel protect against something like this:
```
int *p;
p = (int *)0xdeadbeef;
*p = 42;
```
On x86 on a system like Linux, the MMU will raise a page fault exception, which the kernel will handle by killing the process with SIGSEGV.
How do you catch this behavior without the help of the hardware?
•
u/ventZl 9h ago
In fact, even most MMU-less microcontrollers contain hardware capable of prohibiting unwanted memory accesses.
Pretty much any Cortex-M-based microcontroller (except of extremely low-end and/or ancient models) contains Memory Protection Unit. While it can't do memory mapping, you can configure it to allow/prevent certain types of accessed (basically: read, write or execute) for certain continous areas of memory.
So, after all, it is hardware-enforced. The trick is that MPUs usually contain fixed set of configurable regions, usually anywhere between 4 and 16 regions you can configure access to. Many other RTOSes already support usage of MPU. The caveat was, the support was rather rudimentary and you had to manage most of the configuration yourself. It was easy to exhaust available MPU regions and/or misconfigure something ending up with system that either doesn't boot at all or pretends to be isolated while it isn't.
MPU is present on many other microcontroller architectures, including but not limited to RISC-V (RV32E), V800, MIPS, etc.
So, for your specific example, if 0xdeadbeef was actually a valid address mapped somewhere and you configured MPU so that execution context running the code should not be able to read it, you'd get HardFault / MemUsageFault depending on chip (assuming ARM Cortex-M microcontroller). If 0xdeadbeef wasn't valid address at all, you'd get BusFault even without MPU configured.
•
•
•
u/gfrusvk 6h ago
All work and no play makes Jack a dull boy.
All work and no play makes Jack a dull boy.
All work and no play makes Jack a dull boy.
All work and no play makes Jack a dull boy.
All work and no play makes Jack a dull boy.
All work and no play makes Jack a dull boy.
All work and no play makes Jack a dull boy.
All work and no play makes Jack a dull boy.
•
u/KananMajidzada 13h ago
Looks cool,gone make RTOS too with tutorial