"...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..."
"... In addition, we draw from our experience to propose a new language extension to Rust that would enable it to provide better memory safety tools for event-driven platforms."
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.
Not my field but I don't understand the rationale behind choosing Rust. Given the performance and security concerns wouldn't Ada be the natural choice here? It may not be the most fashionable language but it seems I read about security problems with embedded and IoT devices on a daily basis. Why not choose a language designed for this problem?
Ada isn't actually particularly safe in any reasonable sense - or rather, it provides enough escape hatches with little obvious marking that it's costly (in terms of time spent in code review) to ensure safety. Safety of existing Ada programs is primarily a result of the engineering practices that went into them, more than the language itself.
Nonsense. The designer looked at almost every spot where coding mistakes were leading to crashes or corruption. Then, made those safe-by-default wherever possible. They're described in modern form in the book below by Barnes. The effect was confirmed when defects dropped to around half what Mitre et al had been observing on C projects. There was only one case study I saw where Ada made no difference whose control group was C++. An anomaly that might have been same system reimplemented by same people, easily-correct system, or just both super-talented. So, it is a safe language with only exception being temporal safety w/out a GC. Rust delivers on that.
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:
It would be. Especially with mature tooling available from AdaCore. Seem my reply to vertex-four for links containing proof. Here's some examples in industry & CompSci just for kicks:
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.
> Given the performance and security concerns wouldn't Ada be the natural choice here?
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.
> My take away is that its painful to make embedded systems in Rust now
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.
I've been curious to see how something like tokio[1] and futures[2] would work on a microcontroller. These both seem like good fits for event-based work.
"Garbage collection, for example, introduces nondeterministic
delays. Automatic memory allocators complicate common kernel
optimizations such as slab allocation"
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, ...
No no no. Garbage collection has some mandatory overhead. Sure you can do an amortised analysis. Just like pushing into the back of a C++ vector is O(1) amortised... except that every once in a while, the entire vector has to be copied over into a larger block of memory, costing O(n) for that particular operation. You can mislead people, but you cannot trick the computer and hard realtime constraints.
Do you think your malloc/free (or kmalloc) implementation has no non-deterministic delays? I hate to break it to you, but malloc/free is not magical and O(1). It's complex, even reaching into the cache and operating system, and non-deterministic.
Yep, hard real time code tends not to malloc and free for that reason. Most hard real time systems only allow dynamic memory management at system initialization, not at normal runtime.
>I hate to break it to you, but malloc/free is not magical and O(1).
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.
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.
Everything is predictable, given enough information. Except for Laplace's Demon, that's not possible. Aah, I love my daily fix of pedantry, which I reliably find on hackernews.
Knocking down strawman arguments isn't pedantry, and there's no need to be rude. You were replying to a comment that said GC could have deterministic overhead; and you replied that "no, GC has mandatory overhead" -- which is a different concern. Wtetzner just pointed out, rightly so, that mandatory doesn't mean non-deterministic.
Exactly. That commenter and some others in threads like these are going out of their way to ignore all evidence that deterministic GC's exist with costs that are acceptable for various types of applications. Their myth needs to disappear so real-time GC's get more attention by those who might use or improve on them.
I meant it had a mandatory nondeterministic overhead. I even made an example with the nondeterministic overhead of C++ vectors. Come on, now you're really putting effort into misunderstanding me.
I can reference Richard Jones's[1] Garbage Collection Handbook (http://gchandbook.org/contents.html) which has a whole chapter on Real Time Garbage Collectors.
[1] Despite us sharing the same name, that's not me.
According to the docs "These procs provide a 'best effort' realtime guarantee...Tests show that a 2ms max pause time will be met in almost all cases on modern CPUs"
So suitable for soft realtime like games, but not actually non-deterministic.
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.
This is simply not true! There exist deterministic garbage collectors suitable for realtime applications. For hard real time you probably don't want to be using dynamic allocation at all -- after all, malloc/free (or kmalloc) is non-deterministic too (unless you're using something like TLSF).
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().
Notice how your suggested algorithm's complexity is a function of the amount of memory available. In other words, it is not any more deterministic than the absolutely worst O(n^n) mark&sweep collector you can find.
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.
While that's true in theory, in practice n, if it's your memory size, is bounded above by things like "number of atoms in the universe". In other words, log(n) < 300 (doing log base 2 here).
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.
Yup, a hidden assumption that I didn't state is that 'n' is modest and of course 'log n' is very modest. For example, n = 256 might be the case on a microcontroller doing realtime activities.
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.
If n is bounded, then log(n) is bounded, yes. But so is n^n.
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.
> If n is bounded, then log(n) is bounded, yes. But so is n^n.
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.
I don't have enough experience to speak with authority here. Just an observation from reading on lots of different implementations of stuff. There might be a general rule that gaining more determinism often trades away throughput or vice versa. I remember specific choices of algorithm where this was true. Your claim about hard, real-time GC's having performance degradation was also true for early ones. I know lots of people were focusing on improving performance, though. It's worth surveying the field again to see what the current losses are.
And, as another person replied, it might not even matter at all or with a different choice of hardware.
All of you arguing with rwmj seem to have missed over a decade of research and deployed software in real-time embedded. Here's one product from 2005 whose company is well known in those circles:
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:
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.
I just have to smile. Parallel realtime garbage collector. What do you do, start threads? On an embedded system? What if all the other cores are already occupied with doing something? Do you preempt those threads? There, you lost a couple dozen microseconds until your garbage collector even started doing something (also see: Translation Lookaside Buffer flushes and its costs). I am sorry, I do not believe it. I need to hit timings within that time, accurately. I don't have any time for garbage, I need to get stuff done on the CPU, then and there!
I knew you'd focus on the one I Googled as evidence they're doing something in multicore that's not embedded. It's part of why I left it there. You predictably ignored ones for embedded systems that are hard, real-time along with those I cited deployed in industry for hard, real-time. You continue not believing in hard, real-time GC's despite them in production in real systems that aren't crashing or falling out of the sky. You continue to avoid any exploration of the significant amount of published designs you get just Googling "hard real-time GC" to focus on any peripheral, barely-related point you can.
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.
These collectors have fixed pause time given a fixed heap size. All collectors have that. My toy mark&sweep collector have a fixed worst case pause time of 100 ms on a 128kb heap. The trick is offering a worst case guarantee on an arbitrarily sized heap.
Do hard, real-time, embedded systems usually have arbitrary sized or changing heaps? I thought most avoided dynamic or non-deterministic resource use as much as possible. Restricting the heap to a known size ahead of time then reaping benefits of HRT GC & higher-level language still seems worthwhile in many use-cases.
They don't, so there is certainly many use cases for real time gc:s. But keep in mind that the more parameters are known, such as heap size, memory allocation profile, software to run, hardware and so on the less useful gc becomes.
Often the cost of the extra complexity + specialized hardware outweighs the benefits of using a gc:ed language.
Those points are true. You have to balance these tradeoffs. The main point of my side of this discussion is (a) they exist and (b) they're useful sometimes so worth considering. That's all I'm saying.
Depends on your requirements. A hard real-time system discourages any form of dynamic memory management, let alone GC. There are soft real-time GC implementations, but they are fairly special beasts. I wouldn't consider any non-soft-real-time GC implementation "badly designed".
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.
Even in the flat structure case, doesn't reference counting turn referencing an object into a synchronization point? For example if I try to use several threads to make a million objects reference a single object, I'll have to lock every time I add a reference, reducing parallelism.
Reference counting is a form of GC. It has nondeterministic delays. You don't know which dropref() operation will just decrement and which will finalize. Moreover, when refcounted objects finalize, they recursively drop the refs they hold.
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.
> Moreover, when refcounted objects finalize, they recursively drop the refs they hold.
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).
Well yes and no. In the multi-threaded case you'd use ARC (atomic reference count) pointers. These use atomic instructions to increment and decrement references, and are thus lock-free. There's still contention, but only on the data bus and the cache coherency protocol for the atomic instructions. Under contention, you're looking at latency for access less than 80-100ns. Of course, if all of these threads are accessing an object at once you're going to have contentions no matter what, unless all of the threads are read-only.
Do hard real time systems discourage dynamic memory because historically there were no systems that could provide real time guarantees or because of some theoretical barriers? I think it's the former.
There's other issues. Like the fact that you could run out of memory and allocation fails. Then what do you do? For hard real time systems crashing isn't acceptable, so you have to continue on, but assuming you actually needed that data you tried to allocate that's not going to be easy. So you need to prove your system won't ever allocate too much memory. That's pretty hard to do, and in most cases if you can prove that you can also write your system to use only static allocations.
Funnily enough I'm thinking about this because I'm currently writing a garbage collected ML-/LISP-ish mini-operating system for the BBC Micro:bit. It has only 16K of RAM, which is quite a challenge for GC. Given a naive implementation, just my minor heap would occupy far more than available RAM. But it's a good way to focus the mind on the problems, and (if I pull it off) much more fun than having to use manual memory allocation.
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").
Are typical Java GCs "badly designed"? The issue with nondeterminism and complexity is well known... you trade other things off when you optimize for throughput
The whole execution contexts thing is sort of based off an assumption that the unique-&mut stuff has to do with thread safety. It doesn't. In fact, it had no bearing on thread safety in Rust for the many years before scoped threads were made possible by removing the 'static bound on Send.
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 :|
I feel that some of these issues occur due to an over-reliance on object-oriented practices, in particular 'encapsulation' of state into objects and triggering side-effects in callbacks. The usage of Rust brings painpoints to the fore, but I'll argue that the code is better done otherwise, independently of borrow checker.
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);
Because it sets what is stored in `state` it does not have to be. It could be inside the if, but there two major reasons why it should not:
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.
This assumes that the call to updatePin is idempotent (calling multiple times with same inputs does not do anything). An update of a I/O pin usually is, but other hardware (or APIs...) might not be.
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.
That sounds like an incredibly awkward way to program. You'll end up with a huge function with all your callback bodies in it, and you won't be able to easily sleep the CPU.
Splitting of work to dedicated functions is done as usual. General functions, which don't impact state, should be free-standing. Non-trivial independent functionality should be in separate functions, passing down the relevant state (subsets of the overall state).
As long as there are no side-effects, can do as much higher-order functions as desired.
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.
I think the concern around sleeping is that you don't know how long you can safely sleep for without knowing to inspect state.toggleLedTime (and every similar value - potentially error prone).
This is a really good article, and I admire the authors for the work they put in. But I can't shake the feeling that what they want to accomplish could be done with a library rather than a language extension. Can anyone explain why their issues couldn't be resolved with a particularly clever mix of traits and macros?
This paper is pretty old at this point; today the project has evolved into https://www.tockos.org/
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.
The language was not changed; there were some misunderstandings about the finer points involved here, and their design was changed instead. So overall the idea was implemented, but in already-existing Rust rather than changing the language.
Hmm the callback issue is exactly what I have run into while trying to wrap libsoundio. The user has to provide a read or write callback, but there's just no ergonomic way to actually do that nicely. At least I can't think of one. Maybe I just don't know the right combination of Arc, RefCell, etc.
"Our proposal for execution contexts in Rust is similar but considers execution threads, instead of object graphs, as the unit of isolation. Rust’s low-level interface, safe memory management, and large community make it a particularly good fit for operating system development. If future language development can address the challenges
we have demonstrated, Rust should be well positioned to
support the next generation of correct embedded operating systems."
I'm an embedded Rust developer, and was a little confused by this paper until I noticed it was written in 2015. There are now solutions to a number of the problems they pose both at the language level and the library level:
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:
Note that lazy-static does not "statically allocate a buffer" for all definitions of "statically allocate": lazy-static computes it's value on first use, and must use dynamic alloc space (ie: malloc/box)
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.
I think you misunderstand what 'statically allocate' means. That means a size known at compile time, so that the space for that variable can be assigned in the memory map of the executable. Anything involving a 'custom allocator' is doing runtime memory allocation.
> Releases first occurred in 2014! I'd be surprised if it had not been considered.
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.
Is there a solution to commenters who think monospace quoting on HN is legible on mobile?
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 >
In my opinion the best way to quote here is to start each paragraph with a leading > and then italicize the whole quotation with asterisks around each paragraph.
> 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.
You put > at the beginning of each paragraph in the excerpt to indicate that it's part of the quotation. It's easy to see at a glance that a given paragraph is part of the quote.
I'm an embedded Rust developer, and was a little confused by this paper until I noticed it was 2015. There are now solutions to a number of the problems they pose both at the language level and the library level:
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:
Mods, can we get a [2015] in the title? Both the OS described within and Rust itself have progressed significantly since this paper's original release. :)
I'm upvoting it only because the links might interest embedded developers. You're comment might be missing the point, though, where they're leveraging Rust for it's safety that requires no GC with its CPU or memory overhead. Non-determinism, too, unless using a real-time GC.
It seems that, for this very tiny OS, they're trying to do real work in interrupts. Most OSs do that, including UNIX/Linux. This has the problem that you can't block the interrupt while waiting for some thread to clear a lock.
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.
And that's the way to do it if you value latency rather than simply maximizing throughput. In real time situations latency is usually the more important factor.
One thing about those interrupt messages ('signals') in QNX parlance, they get processed before any other messages.
QNX has bounded latency. A standard test in the real time world is to hook a square wave generator up to an input that cause an interrupt, and run a program which gets activated by the interrupt and turns on an output. You watch the input and output signals on an oscilloscope. If there are any outliers, it's not real time.
(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.
> 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.
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.
Yes. Some boards emulated a hard drive using flash, with BIOS code stealing cycles from the main CPU with code in system management mode. This caused what looked like random CPU stalls at the OS level.
It's better to have one kind of thread and make it go fast than having lots of special cases for bypassing the scheduler. The old MacOS, through version 7, had that problem in a huge way. It didn't have a real CPU dispatcher. Everything event-driven was done by plugging into interrupts. So there were timer tick tasks, vertical refresh interval tasks, and, when TCP/IP networking was crammed in, a bunch of task hooks for network-related tasks. Each could make some different subset of the system calls. None of them could block. Horrid mess.
Sorry, but you're rather sure of yourself when it's not clear whether you understand the constraints kernel writers are operating under. On many platforms simply enumerating valid RAM addresses where a heap may be constructed, let alone mapping them, is going to require considerable effort. It will inevitably be necessary to use memory somewhere off the stack to achieve that effort. Yet it will also be very difficult to write a custom allocator for that environment.
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?).
You are making the exact same strawman argument as the post I was responding to. I gave a specific example of how lazy-static solves a specific problem mentioned in the paper, and as far as I can see nothing you have said refutes that.
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.
So what you're saying is, "if your use-case doesn't support kmalloc then shut up and go away, no statics for you!"? That's ridiculous, inappropriate, and generally unhelpful.
No, again that's a strawman. To explain it for the third time: I was addressing a specific complaint made in the paper and providing a solution. So far I have not seen any counterarguments against this specific solution to this specific problem.
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.
> First, a linear sequence of asynchronous operations does not appear in a linear piece of code.
Instead, it is spread across a series of many small functions.
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...
122 comments
[ 4.7 ms ] story [ 182 ms ] thread"...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.
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:
https://en.wikipedia.org/wiki/SPARK_(programming_language)
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.
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...
[1]: https://tokio.rs/
[2]: https://github.com/alexcrichton/futures-rs
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, ...
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] Despite us sharing the same name, that's not me.
1: https://nim-lang.org/docs/gc.html#realtime-support
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.
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.
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.
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.
And, as another person replied, it might not even matter at all or with a different choice of hardware.
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.
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.
Often the cost of the extra complexity + specialized hardware outweighs the benefits of using a gc:ed language.
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://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 :|
For instance in the 'Callback through closures' example, there is code like:
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.
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 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.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.
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.
Here's one in particular:
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:
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.
Make sure to check out "japaric" on GitHub; they have built a lot of awesome tooling for Rust embedded stuff.
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
Here's one in particular:
This can be solved by using the lazy_static crate:https://github.com/rust-lang-nursery/lazy-static.rs
It's "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.
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. :)
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.
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?).
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.
We detached this subthread from https://news.ycombinator.com/item?id=14107906 and marked it off-topic.
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...