
Writing an OS in Rust: Async/Await - phil-opp
https://os.phil-opp.com/async-await/
======
jerf
Is async/await a good idea for an OS kernel, even a toy one? Cooperative
multitasking tends to break down at scale, because the probability that all
the "threads" you're cooperating with are playing nice goes to zero as the
number of threads increases. An OS will tend to have a concentrated number of
the pathological cases in it as it deals with hardware and all the other
hardest timing & concurrency problems.

It's a viable option for user-space programs because you can far more tightly
characterize them and program them from top to bottom to work with that
paradigm nice. Embedding islands of cooperative multitasking in a sea of pre-
emptive multitasking seems to make a lot more sense than the other way around.

However, this post is a question, not a statement. If, for example, a Linux
kernel developer posts "nah, it's no biggie, the Linux kernel is effectively
structured the same", for instance, I would not quibble.

~~~
CuriousSkeptic
Probably not at all related to the OP. But I found the thought entertaining,
so offering it as such.

One way to address you concern would be to guarantee that task always
terminate in a bounded amount of instructions.

One way to solve that would be offer a limited buffer in which the program
instructions for the task might be expressed, and a language to express them
in that is strongly normalizing (think something like Morte)

~~~
elihu
The Linux kernel makes a somewhat similar distinction between code that's
executing in an atomic context and code that isn't. Atomic code can get stuck
in an infinite loop or whatever, but what it isn't allowed to do is to go to
sleep (i.e. put itself into a sleeping state and invoke the scheduler).

Atomic tasks are generally expected to do a finite amount of work that can be
finished in a short time without waiting for anything else (except maybe a
spinlock, which should be held by some other similarly-atomic task that will
finish in a short time).

------
steveklabnik
This is also just a fantastic introduction to async/await in Rust, regardless
of the OS bits. Another amazing article by Phil.

> The only requirement is that we use at least nightly 2020-03-25 of Rust
> because async/await was not no_std compatible before.

There's some fun stuff here that's omitted (which makes sense, of course). It
was always a design constraint of async/await in Rust that you could use it
without the standard library. However, the initial implementation required
thread local storage. The future trait's poll method took a &mut Context, and
generators didn't support passing context _in_ to them when resuming. This
meant that the context would be placed in TLS, and would pull it back out when
needed. Generators are unstable, partially for this reason. However, this was
fixed, and that means TLS is no longer a requirement here! See
[https://github.com/rust-lang/rust/pull/68524](https://github.com/rust-
lang/rust/pull/68524) for more details.

------
m0th87
Is it possible to context switch between userspace processes directly, without
going through the kernel, i.e. a kind of fast, inter-process cooperative
multitasking? I know earlier operating systems used inter-process cooperative
multitasking, but I'm guessing they still went through the scheduler?

I was trying to figure out if QNX does this with `MsgSend`, as QNX is renowned
for being a fast microkernel, but it wasn't clear to me. According to Animats,
"There's no scheduling delay; the control transfer happens immediately, almost
like a coroutine. There's no CPU switch, so the data that's being sent is in
the cache the service process will need." [1] According to wikipedia, "If the
receiving process is waiting for the message, control of the CPU is
transferred at the same time, without a pass through the CPU scheduler." [2]

It seems like `MsgSend` circumvents the scheduler, but does it circumvent
context switching to the kernel entirely too?

1:
[https://news.ycombinator.com/item?id=9872640](https://news.ycombinator.com/item?id=9872640)

2:
[https://en.wikipedia.org/wiki/QNX#Technology](https://en.wikipedia.org/wiki/QNX#Technology)

~~~
retrac
It's not possible without specific hardware support.

Context-switching on most current platforms with virtual memory requires
fiddling with things like page mappings, and that simply must be done from
supervisor mode.

The ability to switch tasks in hardware has existed on some historical
machines. For example the GEC 4000 series basically implemented a microkernel,
including the scheduler, in hardware. Switching to another process was done
with a single hardware instruction.

I don't think anything current has such features besides the long-obsolete and
unused taskswitching feature on x86 processors.

------
zackmorris
I've shied away from async/await because I haven't seen a good writeup on how
to make it deterministic. Come to think of it, all of the times I've
encountered it, there was no way to analyze the codepaths and prove that every
exceptional situation and failure mode was handled. Maybe I missed something
along the way?

So my feeling about it is that it may turn out to be an evolutionary dead end,
or anti-pattern at the least. My gut feeling is that async/await is
functionally equivalent to the the synchronous/blocking/coroutine/channel
system of other languages like Go. Could we write a thin wrapper that converts
async/await to that or vice versa?

This is the primary reason why I've stuck to synchronous/blocking PHP with all
of its flaws instead of Node.js. I think this is a fundamental thing that
shouldn't be glossed over and accepted so readily into other languages.

~~~
animalnewbie
I am no expert and I wish there were a start from 0 guide with examples not
involving external crates but my sense is that you can write your own executor
and do whatever you want. (Maybe I'm wrong?)

~~~
steveklabnik
You're not wrong: that is basically what this article is, and it includes
writing an executor!

------
fbhdev
Aside from the cooperative multitasking discussion, I thoroughly enjoyed how
you discussed the rust async/await concepts in detail and learned a lot from
it. I find your article providing a lot more value than the rust
documentation, which is unfortunate since that makes the learning curve
somewhat difficult.

~~~
phil-opp
Great to hear that! I also found the official documentation a bit short on
background information, so I decided to write my own explanation rather than
link to something existing. Given that the async/await implementation is still
quite young, I'm sure that the official docs will improve over time.

------
amelius
An OS with async/await sounds awfully similar to Windows 3.1 with its
cooperative multitasking model.

What's old is new again ...

~~~
hope-striker
Didn't most old operating systems use cooperative multitasking? I remember, at
least, that classic Mac OS (i.e. pre-OSX) didn't use preemptive multitasking,
either.

Anyway, this SO answer[0] explains why early Linux, much like the hobby kernel
in this article, used cooperative scheduling inside the kernel, and only
preempted user-space stuff.

[0]:
[https://stackoverflow.com/a/16766562](https://stackoverflow.com/a/16766562)

~~~
bestouff
MacOS may have been a little late. Windows NT, OS/2, Linux did preemptive
multitasking since beginning of 90s, even AmigaOS had it in the 80s.

------
throwlaplace
great project for this lull.

i've been going through
[http://craftinginterpreters.com/](http://craftinginterpreters.com/) in rust
(in order to learn rust) and it's a fantastic learning experience. the
language is straightforward after you understand the basics (smart pointers
and generics) and if you have a good ide with completion (CLion).
lifetimes/borrow checker aren't as painful as people would have you believe if
you use heap allocation (i.e. Box, Rc<RefCell>). now obviously heap allocation
isn't optimal but for getting started it enables a smooth progression.

