Ownership is Theft: Experiences Building an Embedded OS in Rust (2015) [pdf]
sing.stanford.edu
sing.stanford.edu
Here's one in particular:
In principal, this would allow us to statically allocate
a buffer sized for each particular closure.
Unfortunately, there is no size_of keyword in Rust,
and static initialization cannot invoke functions—
in this case the function size_of<T>() -> usize provided
by the Rust core library.
This can be solved by using the lazy_static crate:https://github.com/rust-lang-nursery/lazy-static.rs
From the description in the README:
Using this macro, it is possible to have statics that
require code to be executed at runtime in order to be
initialized.
Hey look, it's exactly what they need! It's also "no_std" compatible and can work just fine in a kernel-like context.Take this paper with a grain of salt: it's a bit dated at this point, and there are solutions for a number of the problems posed.
Also, lazy-static has been around for a long time. Releases first occurred in 2014! I'd be surprised if it had not been considered.
That said, Rust supports custom allocators now:
https://doc.rust-lang.org/book/custom-allocators.html
Write your own allocator and use Box.
I wouldn't be surprised if it hadn't been considered. The paper was released in 2015, so development may have begun in 2014. Furthermore, I don't believe the authors were engaged with the broader Rust community before this paper was released (they are today), so it's possible that there was no expert there to advise them about this approach. Also, discoverability of libraries was more of an issue back then, so it's plausible that they wouldn't have found it independently.
Edit: I apologize for the snark, but still, monospace for quoting doesn't add anything, and I don't see why people do it once they see it forces a side scroll on small screens. Please just use >
> because HN formats > text blocks > like this
> It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of Darkness, it was the spring of hope, it was the winter of despair, we had everything before us, we had nothing before us, we were all going direct to Heaven, we were all going direct the other way – in short, the period was so far like the present period, that some of its noisiest authorities insisted on its being received, for good or for evil, in the superlative degree of comparison only.
> like
> this
Make sure to check out "japaric" on GitHub; they have built a lot of awesome tooling for Rust embedded stuff.
For instance in the 'Callback through closures' example, there is code like:
setTimeout(|| {
activityLed.toggle();
}, 2000);
First, in an embedded system, you cannot have multiple sources of truth. So its not just because of Rust ownership reasoning that `activityLed` cannot be shared between callbacks - if multiple events is to be considered for a given output they should be syncronized explicitly.Instead I think the code should be more like the following, which models calculation of new state as a pure function. Side-effects are separated out by storing the desired effect as data, dedicated function for realizing. Time is just data, time-based logic without depending on hidden state.
This increases testability significantly. And it can be fully reasoned about by Rust, no conflicts with the borrow checker.
# for each tick
# calculate new state
if (now >= state.toggleLedTime) {
state.activityLed = !state.activityLed;
state.toggleLedTime = now + 2000;
}
# and realize it
updatePin(config.ledPin, state.activityLed);Sleeping is not affected? Can trigger that whenever you want. Only difference is that there is no 'implicit' sleeping from registering a callback.
But if using an exisiting RTOS, there might be serious impedance mismatch causing this approach to not worth it.
1) Testability. With it separated, we can write automated tests for our state calculation logic that are hardware independent, like 'normal' code. We just need to create `currentState` data, pass it into our `calculate()` function, and verify the new `state` returned. This can run both on our host-system, and on-target. We can even build simulations if we want, http://www.jonnor.com/2017/03/host-based-simulation-for-embe... (shameless plug).
2) Deterministic execution time. Many embedded systems have soft or hard real-time constraints, action must always be performed within X microseconds. The more branches we have the harder it is to reason about whether we can keep this guarantee. By eliminating branches we are executing our 'worst case' always, giving it much more testing.
In this case the realization of the state should still be in a separate function for testability. But it can take both `newState, currentState` as arguments, and then do:
if (newState.thingon != currentState.thingon) {
nonIdempotentUpdate(newState.thingon);
}
If the device is latency critical, then the functions should be tested in all its permutations. You can use generative techniques like fuzzing to generate the `state` inputs.http://manishearth.github.io/blog/2015/05/17/the-problem-wit... details why it's necessary.
The article does sort of mention this (and I have talked with the authors about this before) but IMO it underrepresents the importance of it.
One thing I did discuss with one of the authors at one point was swapping around the guarantees a bit -- allowing multiple &mut for cases not involving any form of runtime typing (enums and vectors are both cases of runtime typing -- in a vector the number of elements is runtime dependent). This would create a significantly different language and be incompatible with the vast majority of the ecosystem; however, it would still be safe, and has the potential to be useful. I even started hacking on a fork of the compiler that does this, but never got the chance to finish it. The idea in essence is not too hard to implement.
Octopress does this stupid thing where it generates post URIs based on the local timezone, and I published my last post whilst in Taipei so all the URLs broke. I need to figure out how to patch the code to fix this :|
"...embedded platforms are highly event-based, and Rust’s memory safety mechanisms largely presume threads. In our experience developing an operating system for embedded systems in Rust, we have found that Rust’s ownership model prevents otherwise safe resource sharing common in the embedded domain, conflicts with the reality of hardware resources, and hinders using closures for programming asynchronously..."
I believe this part also warrants mention. Not only did they share their findings as experts in the field, they also did the extra work of detailing improvements.
My take away is that its painful to make embedded systems in Rust now but its promising enough to make it worth the effort for the embedded community to help guide Rust's development.
Please note that this paper is old and some of it is due to the author's lack of experience with Rust at the time. It has gotten way way easier since then.
Fun story: Niko, a Rust core team member, was keynoting the conference where this paper was presented, and ended up sitting down with the authors and working out some stuff. So their hard work was upstreamed even faster than usual! (It ended up being more of a redesign than a language extension, though.)
> the embedded commmunity to help guide Rust's development.
Yes please!
I find stories like how Niko met the authors fascinating, so thank you for that as well.
Amit, one of the authors, has left a comment on Reddit with some more context about how these problems were solved: https://www.reddit.com/r/rust/comments/655816/ownership_is_t...
http://www.adacore.com/uploads_gems/Ada_Safe_and_Secure_Book...
Also, SPARK Ada can straight-up prove common errors don't exist in that code with an automated prover. Most engineers would spend quite a bit of time doing that with pencil-and-paper examination or human-driven provers:
http://www.adacore.com/customers
Note that the authors are just doing research on seeing how well Rust handles the problem domain. There's nothing wrong with them avoiding Ada to explore another tool or problem area. They should consider each carefully if they were trying to pick best tool for real work.
Ada would be a good choice, but it's way more awkward to program in. It definitely has some nice features of its own that Rust should steal asap though.
The problems they mention and run into, have already been examined and dealt with in embedded Schemes, such as BIT, PICBIT and PICOBIT.
PICBIT http://www.iro.umontreal.ca/~feeley/papers/FeeleyDubeSW03.pd... (see the GC section)
PICOBIT http://users.eecs.northwestern.edu/~stamourv/papers/picobit.... (not as relevant in that hard decisions/tradeoffs examined in predecessor PICBIT)
BIT (1995) https://github.com/melvinzhang/bit-scheme
QNX, though usually sends a message to a thread in a process when an interrupt comes in. The interrupt is processed slightly later. QNX has no interrupt lockouts longer than a few microseconds, because it's not doing much from interrupt code except sending messages. QNX also has a strict real-time priority system, so this doesn't hurt interrupt processing latency. So everything running in a thread uses the regular locking mechanisms, rather than depending on interrupt lockout.
Something like that might be a better fit to Rust's locking model. Either that, or the language has to know about interrupts, as in Modula I.
One thing about those interrupt messages ('signals') in QNX parlance, they get processed before any other messages.
(A QNX rep once told me they'd had problems with x86 boards that did things in "system management mode", stealing CPU cycles from the real work and messing up the response time. They had a quiet blacklist of unacceptable embedded boards.)
Linux has a long history of long interrupt lockouts. It's gotten better.[1] The phrase "95% real time" is used. Kernel drivers can lock out interrupts, so you have to watch for drivers that are not real-time qualified.
That's the trouble with doing real work in an interrupt handler. Usually you have to activate a thread after an interrupt anyway, so there's no overall performance gain in doing work at interrupt level.
[1] http://events.linuxfoundation.org/sites/events/files/slides/...
Ah, so that was the source of QNX informally suggesting we stick to certain brands of motherboards. I always wondered about that, since it seemed to work ok. I guess they didn't want to alienate those vendors outright so they chose to informally promote the ones they were sure were good.
95% real time is funny. It's real time or don't bother. I worked with and for the QNX dealership in NL long ago.
The authors have been meaning to write a follow-up paper describing how they overcame the problems here, but haven't just quite yet. That slightly different design is what became Tock.
Largely unrelated to Rust, but I want to point out that that is a powerful insight. It took me a while to internalize that. Once you see it, you'll understand why asynchronous, callback-based programming is usually inferior to other models (green threads, channel, lightweight processes and yes, threads). There are some exceptions for course, say when callback chains are short, like a web proxy, in the an embedded world, or a short demo code someone pasted on SO.
Another way to think about it is that "an sequence of callbacks forms an ad-hoc execution context, like a thread almost, except it is spread all through various functions, not very well specified, not in one single place".
Now perhaps in this style you can avoid CPU locks, but if callbacks are used for concurrent IO, you are back to the the need to protect global data from concurrent modifications. For example, if a callback chain starts, say cb1, cb2, cb3 but stops at cb2 while it was modifying some global data. Then another user request (or other such event) comes and it starts running cb1, cb2, cb3. It will possibly start modifying the same global data before it was in a consistent state. Next thing you know is you need something like a lock or semaphore. And sure enough such a thing exists, here is an example in Twisted: http://twistedmatrix.com/documents/8.1.0/api/twisted.interne...
No no no. Badly designed GC, for sure. But for any modern, thought-through GC this is not true.
And in any case, the Linux kernel uses ref-counting all over the place, which is the worst form of GC - slow, hard to debug, full of potential places to trip you up and cause a leak or a security bug, ...
I get the point about reference counting, but I should point out it depends on the data structure you are dealing with. If you've got a tree-like or list-like data structure with reference counted pointers everywhere, then yes that can cause nondeterministic delays. But if you're reference counting a flat structure containing no reference-counted pointers, or contains some reference counted pointers, but doesn't allow for arbitrarily deep data structures, reference counting delays are completely deterministic. In many cases that can be faster than GC, and definitely uses less memory. Point still stands about the safety issue, there are quite a few CVEs having to do with not properly decrementing the reference count, though C++ shared_pointer and Rusts Rc<> make that safer.
Of course, in some trivial cases it is obvious. But in those cases, you could use manual memory management. For instance, trivial case: making an object and then dropping it in the same scope.
You can (at the cost of some additional complication) make this more deterministic by adding newly-dead objects to a "finalize this soon" queue and chewing through that when you have time (or on a different thread).
Ref-counting in C++ with smart pointers is much more robust.
It can also be partially optimized. You can use const references to pass smart pointers into helper functions, which means that the refcounts don't have to be bumped. You can do that sort of thing manually too, but then you hav e to be careful about tracking which functions are sharing ownership with the caller (and so must not drop a reference, nor allow the reference to "escape").
http://www.businesswire.com/news/home/20050307005203/en/Aoni...
The first was in 2003 according to IBM work trying to improve on it in 2007:
http://researcher.watson.ibm.com/researcher/files/us-bacon/F...
I haven't checked on multicore research in a while. A quick Google implies that they're either there or part of the way there for one project on JamaicaVM:
https://www.researchgate.net/publication/221032857_Concurren...
So, yeah, you can do deterministic, hard-real-time GC's. They've been done since 2003 with products shipping since about 2005 for use in safety-critical applications. The DO-178B stuff in particular requires high rigour in how software is designed, coded, analyzed, and tested. That an implementation passed that indicates it can be built with high confidence. Research continues on them. Use in FOSS or popular embedded? Almost nothing.
Often the cost of the extra complexity + specialized hardware outweighs the benefits of using a gc:ed language.
There's no reason to doubt hard, real-time GC's exist or can work at this point. You should've moved to another goalpost like what tradeoffs were required as others did. You're either trolling us or dodging all evidence due to beliefs only rooted in faith. That is, ignoring all facts contrary to your belief that hard, real-time GC's don't exist or can't work in embedded. So, I'm done arguing with you since nobody can argue against religion that twists or dismisses real-world things to maintain its faith-based ones.
void *malloc(size_t _) { return NULL; }
void free(void *ptr) {}
More seriously, I bet there are useful allocators that have bounded runtimes; for instance, if you use a multimap to track free blocks as {size: base address} you can find the best-fit block in O(log n) time for malloc (but this doesn't solve the problem of coalescing adjacent blocks in free, you'll need a different, hopefully log-time structure, for this; and you'll need to keep the two in synch; etc).In the kind of embedded I typically think of, there's no need to worry that malloc() has to call sbrk(), which might do things like need to access disk to swap out pages from another process. You're right, if you have sbrk() that takes unbounded time, malloc() will have to take unbounded time if it sometimes calls sbrk().
Unless you impose a bound on n, both n^n and log(n) -> inf as n-> inf. In other words, no upper bound on the pause time.
Now it may be that plugging in "300" into your pause time formula where log(n) appears leads to unacceptably long pauses. That's a separate discussion and if you're at that point you care not only about O(log n) vs O(1) but also about constants and non-asymptotic behavior and whatnot. An algorithm that gives you a running time of 1ms for all values of n except n == 1, when the running time is 1000ms, is O(1) and even has "1" as the relevant asymptotic constant, but in practice that 1000ms thing might be a problem.
The same argument really doesn't work for anything that is linear (or really most sorts of polynomial), simply because the range of possible values becomes so much larger. But if you're ok with your worst-case pause being two orders of magnitude longer than your best-case pause, then a log(n) algorithm works just fine.
You can claim that a log(n) algorithm runs quickly or almost always fast enough and I'll agree with you. But that is not the same as it being bounded.
In practice, log(n) is no guarantee for an algorithm being quick. A tree search is log(n) but for a big enough n it will begin to cause thousands of really slow page faults.
In other words it doesn't matter if you pick theory or practice, for strict enough real time guarantees and for large enough values of n, log(n) is too slow. Which is exactly the reason why many real time systems only use static memory.
Yes, but in _practice_ the difference in the bounds is gigantic. log(n) is not just bounded for all practical purposes, it's bounded by a constant that's just not that big. That is simply not true for n^n, or even just n.
> In practice, log(n) is no guarantee for an algorithm being quick.
Sure, the constant matters.
> cause thousands of really slow page faults.
Which means the constant is huge. I agree that this is a problem, sure.
> for strict enough real time guarantees and for large enough values of n, log(n) is too slow.
It really depends on what the operations are. log(n) randomly-distributed memory accesses are a problem, I agree. If you have to touch that much memory and have a paged memory system, you lose no matter whether your memory is dynamically or statically allocated. You're right that dynamic allocation can make it more necessary to touch that much memory.
By the way, I fully agree that if you want to do hard realtime and have any hope of actually shipping, sticking to no dynamic allocation is probably simpler than doing the analysis to prove that your dynamic allocations are OK.
proving you never exhaust the heap is probably harder than proving your allocator never takes more than X ns, once you've decided to do runtime allocation on your embedded RTOS.
And, as another person replied, it might not even matter at all or with a different choice of hardware.
[1] Despite us sharing the same name, that's not me.
So suitable for soft realtime like games, but not actually non-deterministic.
https://news.ycombinator.com/item?id=14108543
It's the GC rather than the language you have to focus on unless it's a dynamic language supporting features like LISP. One like Java or Rust can use the most predictable subset of the language combined with hard, real-time GC. Many of those exist & at least one had plenty of commercial deployment with a Java subset.
Trust me, I know more about it than I'd like to. I work on embedded software with hard realtime constraints and we had to remove all malloc/free calls from the critical paths.
What I don't like is people claiming garbage collection is free. It annoys me to no end. Garbage collection comes at a clear cost and you need to make a cost/benefit analysis. For your average chat application, this is okay. For video games, it might be, but it's probably already noticeable. For anything remotely realtime, forget garbage collection.
Or just use real-time, garbage collection if its costs are acceptable:
https://news.ycombinator.com/item?id=14108543
At least one is deployed in DO-178B-certified systems in industry that require highest confidence and testing. That's as opposite of your implication as possible.
Yes, but perhaps it doesn't have to be an unpredictable amount of overhead.
[1]: https://tokio.rs/
EDIT: In particular: the people behind this recently began offering an IoT development board, "Hail", that runs Tock (the OS from the OP): https://www.tockos.org/blog/2017/introducing-hail/ (discussion on /r/rust, with authors present: https://www.reddit.com/r/rust/comments/61257c/introducing_ha... )
EDIT 2: I see that the title has been updated, thanks. :)
One choice, for example, would be to rely on the bootloader's handling of .bss sections to allocate a temporary 'heap'. Haven't tried it, but it probably gets the job done well enough to bootstrap a real dynamic allocator. The point is, though, you can't just say "throw lazy-static and a custom allocator at the problem". Note, for example, my proposed solution requires writing the custom allocator entirely outside of Rust, since it can't access any Rust statics. It also requires two implementations of the custom allocator - one for bootstrapping, and the other after bootstrapping. These will have to be dynamically selected between at runtime since (AFAIK) Rust is hardly going to support static switching between them. Then you have to consider interoperability between them.
Finally, not all kernel designs are prepared to do any kind of dynamic allocation at all: microkernels which hand all available physical memory to userspace for management there cannot dynamically allocate (except, perhaps, for O(1) of allocation in a small, static heap - but at that point, what's the point?).
We detached this subthread from https://news.ycombinator.com/item?id=14107906 and marked it off-topic.
Clearly YMMV as to whether or not you can support a kmalloc()-style API. If you can, allocators will be great for you, and if not they are worthless.
I didn't mean to imply Rust has one-size-fits-all solutions to these problems. Rust provides a lot of solutions to various problems, and it's up to you as the Rust developer to figure out which ones are applicable to your problem at hand.
Later I pointed out allocators are a great solution if you're able to use them. Again, if you can't, I'm afraid you're out of luck and I apologize this solution doesn't fit your needs.
You can, of course, still make statics, you just won't be able to use Box to conveniently allocate buffers in an automatic and type-safe way.
Also check out UnsafeCell if you want to assert as the developer on behalf of your program that you're accessing data in a memory safe way.