in ,

Writing an OS in Rust: Async / Await, Hacker News

             Mar 0596,              

In this post we explore cooperative multitasking

and the async / await feature of Rust. We take a detailed look how async / await works in Rust, including the design of the Future trait, the state machine transformation, and pinning . We then add basic support for async / await to our kernel by creating an asynchronous keyboard task and a basic executor. This blog is openly developed on GitHub If you have any problems or questions, please open an issue there. You can also leave comments at the bottom . The complete source code for this post can be found in the (post - branch. [derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] LinkedIn profile or contact me at job @ phil-opp. com .     

Table of Contents

                

                         Cooperative Multitasking                                                        Async / Await in Rust

                 [derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]                          Futures                                               Working with Futures                                               The Async / Await Pattern                                               Pinning                                               Executors and Wakers                                               Cooperative Multitasking?                                                        Implementation                  [derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]                          Task                                               Simple Executor                                               Async Keyboard Input                                               Executor with Waker Support                                                        Summary                                                What's Next?                                                    Sequence diagram: main calls read_file and is blocked until it returns; then it calls foo() and is also blocked until it returns. The same process is repeated, but this time async_read_file is called, which directly returns a future; then foo() is called again, which now runs concurrently to the file load. The file is available before foo() returns. 🔗 Multitasking One of the fundamental features of most operating systems is multitasking , which is the ability to execute multiple tasks concurrently. For example, you probably have other programs open while looking at this post, such as a text editor or a terminal window. Even if you have only a single browser window open, there are probably various background tasks for managing your desktop windows, checking for updates, or indexing files. While it seems like all tasks run in parallel, only a single task can be executed on a CPU core at a time. To create the illusion that the tasks run in parallel, the operating system rapidly switches between active tasks so that each one can make a bit of progress. Since computers are fast, we don't notice these switches most of the time.

There are two forms of multitasking: Cooperative multitasking requires tasks to regularly give up control of the CPU so that other tasks can make progress. Preemptive multitasking uses operating system functionality to switch threads at arbitrary points in time by forcibly pausing them. In the following we will explore the two forms of multitasking in more detail and discuss their respective advantages and drawbacks. (🔗) Preemptive Multitasking The idea behind preemptive multitasking is that the operating system controls when to switch tasks. For that, it utilizes the fact that it regains control of the CPU on each interrupt. This makes it possible to switch tasks whenever new input is available to the system. For example, it would be possible to switch tasks when the mouse is moved or a network packet arrives. The operating system can also determine the exact time that a task is allowed to run by configuring a hardware timer to send an interrupt after that time. The following graphic illustrates the task switching process on a hardware interrupt: In the first row, the CPU is executing task (A1 of the program A All other tasks are paused. In the second row, a hardware interrupt arrives at the CPU. As described in the Hardware Interrupts

, the CPU immediately stops the execution of task A1 and jumps to the interrupt handler defined in the interrupt descriptor table (IDT). Through this interrupt handler, the operating system now has control of the CPU again, which allows it to switch to task B1 instead of continuing task (A1) . [dependencies.crossbeam-queue] (Saving State) Since tasks are interrupted at arbitrary points in time, they might be in the middle of some calculations. In order to be able to resume them later, the operating system must backup the whole state of the task, including its (call stack) and the values ​​of all CPU registers. This process is called a context switch .

thread for short. By using a separate stack for each task, only the register contents need to be saved on a context switch (including the program counter and stack pointer). This approach minimizes the performance overhead of a context switch, which is very important since context switches often occur up to 1001 times per second. 🔗 Discussion

The main advantage of preemptive multitasking is that the operating system can fully control the allowed execution time of a task. This way, it can guarantee that each task gets a fair share of the CPU time, without the need to trust the tasks to cooperate. This is especially important when running third-party tasks or when multiple users share a system. The disadvantage of preemption is that each task requires its own stack. Compared to a shared stack, this results in a higher memory usage per task and often limits the number of tasks in the system. Another disadvantage is that the operating system always has to save the complete CPU register state on each task switch, even if the task only used a small subset of the registers. Preemptive multitasking and threads are fundamental components of an operating system because they make it possible to run untrusted userspace programs. We will discuss these concepts in full detail in future posts. For this post, however, we will focus on cooperative multitasking, which also provides useful capabilities for our kernel.

🔗 Cooperative Multitasking Instead of forcibly pausing running tasks at arbitrary points in time, cooperative multitasking lets each task run until it voluntarily gives up control of the CPU. This allows tasks to pause themselves at convenient points in time, for example when it needs to wait for an I / O operation anyway. . The idea is that either the programmer or the compiler inserts yield [derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] operations into the program, which give up control of the CPU and allow other tasks to run. For example, a yield could be inserted after each iteration of a complex loop. It is common to combine cooperative multitasking with asynchronous operations . Instead of waiting until an operation is finished and preventing other tasks to run in this time, asynchronous operations return a "not ready" status if the operation is not finished yet. In this case, the waiting task can execute a yield operation to let other tasks run. State The drawback of cooperative multitasking is that an uncooperative task can potentially run for an unlimited amount of time. Thus, a malicious or buggy task can prevent other tasks from running and slow down or even block the whole system. For this reason, cooperative multitasking should only be used when all tasks are known to cooperate. As a counterexample, it's not a good idea to make the operating system rely on the cooperation of arbitrary userlevel programs.