Update implementation section

This commit is contained in:
Philipp Oppermann
2020-03-19 16:58:04 +01:00
parent 326a35939a
commit 50db561774
2 changed files with 44 additions and 15 deletions

View File

@@ -766,7 +766,7 @@ We see that futures and async/await fit the cooperative multitasking pattern per
## Implementation
Now that we understand how cooperative multitasking based on futures and async/await works in Rust, it's time to add support for it to our kernel. Since the [`Future`] trait is part of the `core` library and async/await is a feature of the language itself, there is nothing special we need to do to use it in our `#![no_std]` kernel. The only requirement is that we use at least nightly-TODO of Rust because async/await was based on parts of the standard library before.
Now that we understand how cooperative multitasking based on futures and async/await works in Rust, it's time to add support for it to our kernel. Since the [`Future`] trait is part of the `core` library and async/await is a feature of the language itself, there is nothing special we need to do to use it in our `#![no_std]` kernel. The only requirement is that we use at least nightly-TODO of Rust because async/await was not `no_std` compatible before.
With a recent-enough nightly, we can start using async/await in our `main.rs`:
@@ -800,6 +800,9 @@ pub mod task;
```rust
// in src/task/mod.rs
use core::{future::Future, pin::Pin};
use alloc::boxed::Box;
pub struct Task {
future: Pin<Box<dyn Future<Output = ()>>>,
}
@@ -807,8 +810,8 @@ pub struct Task {
The `Task` struct is a newtype wrapper around a pinned, heap allocated, dynamically dispatched future with the empty type `()` as output. Let's go through it in detail:
- We require that the future associated with a task returns `()`. So tasks don't return any result, they are just executed for its side effects. For example, the `example_task` function we defined above has no return value, but it prints something to the screen as a side effect.
- The `dyn` keyword indicates that we store a [trait object] in the `Box`. This means that the type of the future is [dynamically dispatched], which makes it possible to store different types of futures in the task. This is important because each `async fn` has their own type and we want to be able to create different tasks later.
- We require that the future associated with a task returns `()`. This means that tasks don't return any result, they are just executed for its side effects. For example, the `example_task` function we defined above has no return value, but it prints something to the screen as a side effect.
- The `dyn` keyword indicates that we store a [trait object] in the `Box`. This means that the type of the future is [dynamically dispatched], which makes it possible to store different types of futures in the `Task` type. This is important because each `async fn` has their own type and we want to be able to create different tasks later.
- As we learned in the [section about pinning], the `Pin<Box>` type ensures that a value cannot be moved in memory by placing it on the heap and preventing the creation of `&mut` references to it. This is important because futures generated by async/await might be self-referential, i.e. contain pointers to itself that would be invalidated when the future is moved.
[trait object]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html
@@ -821,7 +824,7 @@ To allow the creation of new `Task` structs from futures, we create a `new` func
// in src/task/mod.rs
impl Task {
pub fn new(future: impl Future<Output = ()>) -> Task {
pub fn new(future: impl Future<Output = ()> + 'static) -> Task {
Task {
future: Box::pin(future),
}
@@ -829,15 +832,17 @@ impl Task {
}
```
The function takes an arbitrary future with output type `()` and pins it in memory through the [`Box::pin`] function. Then it wraps it in the `Task` struct and returns the new task.
The function takes an arbitrary future with output type `()` and pins it in memory through the [`Box::pin`] function. Then it wraps it in the `Task` struct and returns the new task. The `'static` lifetime is required here because the returned `Task` can live for an arbitrary time, so the future needs to be valid for that time too.
We also add a `poll` method to allow the executor to poll the corresponding future:
```rust
// in src/task/mod.rs
use core::task::{Context, Poll};
impl Task {
fn poll(&mut self, context: &mut Context) -> Poll {
fn poll(&mut self, context: &mut Context) -> Poll<()> {
self.future.as_mut().poll(context)
}
}
@@ -868,7 +873,7 @@ pub struct SimpleExecutor {
impl SimpleExecutor {
pub fn new() -> SimpleExecutor {
SimpleExecutor {
task_queue:: VecDeque::new(),
task_queue: VecDeque::new(),
}
}
@@ -885,14 +890,14 @@ The struct contains a single `task_queue` field of type [`VecDeque`], which is b
#### Dummy Waker
In order to call the `poll` method, we need to create a [`Context`] type, which wraps a [`Waker`] type. To start simple, we will first create a dummy waker that does nothing. The simplest way to do this is by implementing the [`Wake`] trait:
In order to call the `poll` method, we need to create a [`Context`] type, which wraps a [`Waker`] type. To start simple, we will first create a dummy waker that does nothing. The simplest way to do this is by implementing the unstable [`Wake`] trait for an empty `DummyWaker` struct:
[`Wake`]: https://doc.rust-lang.org/nightly/alloc/task/trait.Wake.html
```rust
// in src/task/simple_executor.rs
use alloc::task::Wake;
use alloc::{sync::Arc, task::Wake};
struct DummyWaker;
@@ -903,9 +908,31 @@ impl Wake for DummyWaker {
}
```
Since the [`Waker`] type implements the [`From<Arc<W>>`] trait for all types `W` that implement the [`Wake`] trait, we can easily create a `Waker` through `Waker::from(DummyWaker)`. We will utilize this in the following to create a simple `Executor::run` method.
The trait is still unstable, so we have to add **`#![feature(wake_trait)]`** to the top of our `lib.rs` to use it. The `wake` method of the trait is normally responsible for waking the corresponding task in the executor. However, our `SimpleExecutor` will not differentiate between ready and waiting tasks, so we don't need to do anything on `wake` calls.
[`From<Arc<W>>`]: TODO
Since wakers are normally shared between the executor and the asynchronous tasks, the `wake` method requires that the `Self` instance is wrapped in the [`Arc`] type, which implements reference-counted ownership. The basic idea is that the value is heap-allocated and the number of active references to it are counted. If the number of active references reaches zero, the value is no longer needed and can be deallocated.
[`Arc`]: https://doc.rust-lang.org/stable/alloc/sync/struct.Arc.html
To make our `DummyWaker` usable with the [`Context`] type, we need a method to convert it to the [`Waker`] defined in the core library:
```rust
// in src/task/simple_executor.rs
use core::task::Waker;
impl DummyWaker {
fn to_waker(self) -> Waker {
Waker::from(Arc::new(self))
}
}
```
The method first makes the `self` instance reference-counted by wrapping it in an [`Arc`]. Then it uses the [`Waker::from`] method to create the `Waker`. This method is available for all reference counted types that implement the [`Wake`] trait.
[`Waker::from`]: TODO
Now we have a way to create a `Waker` instance, we can use it to implement a `run` method on our executor.
#### A `run` Method
@@ -914,10 +941,13 @@ The most simple `run` method is to repeatedly poll all queued tasks in a loop un
```rust
// in src/task/simple_executor.rs
use core::task::{Context, Poll};
impl SimpleExecutor {
pub fn run(&mut self) {
while let Some(mut task) = self.task_queue.pop_front() {
let mut context = Context::from_waker(Waker::from(DummyWaker));
let waker = DummyWaker.to_waker();
let mut context = Context::from_waker(&waker);
match task.poll(&mut context) {
Poll::Ready(()) => {} // task done
Poll::Pending => self.task_queue.push_back(task),
@@ -942,7 +972,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! {
// […] initialization routines, including `init_heap`
let mut executor = SimpleExecutor::new();
executor.spawn(Task::new(example_task())):
executor.spawn(Task::new(example_task()));
executor.run();
// […] test_main, "it did not crash" message, hlt_loop
@@ -951,5 +981,4 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! {
When we run it, we see that the expected _"async number: 42"_ message is printed to the screen:
TODO image
![QEMU printing "Hello World", "async number: 42", and "It did not crash!"](qemu-simple-executor.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB