Skip to content

Commit f06ef24

Browse files
committed
feat: add cooperative multitasking with async/await and add keyboard task
1 parent 8b72531 commit f06ef24

File tree

8 files changed

+370
-24
lines changed

8 files changed

+370
-24
lines changed

kernel/Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,23 @@ features = [
3232
version = "1.0"
3333
features = ["spin_no_std"]
3434

35+
# Implements lock-free queues. Allows push without allocation via a fixed-size buffer.
36+
[dependencies.crossbeam-queue]
37+
version = "0.3.11"
38+
default-features = false
39+
features = ["alloc"]
40+
41+
# Allows for safe, one-time initialization of static variables.
42+
[dependencies.conquer-once]
43+
version = "0.2.0"
44+
default-features = false
45+
46+
# Contains the `Stream` type.
47+
[dependencies.futures-util]
48+
version = "0.3.4"
49+
default-features = false
50+
features = ["alloc"]
51+
3552
[package.metadata.bootimage]
3653
test-args = [
3754
"-device",

kernel/src/interrupts.rs

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,10 @@ extern "x86-interrupt" fn page_fault_handler(
5858
stack_frame: InterruptStackFrame,
5959
error_code: PageFaultErrorCode,
6060
) {
61-
println!("lopejwqjeqwe {:#?} {:#?}", stack_frame, error_code);
62-
6361
loop {}
6462
}
6563

66-
// as interrupções utilizam um calling convention específico.
64+
// Interruptions use a specific calling convention.
6765
extern "x86-interrupt" fn breakpoint_handler(_stack_frame: InterruptStackFrame) {
6866
loop {}
6967
}
@@ -101,28 +99,10 @@ extern "x86-interrupt" fn timer_interrupt_handler(_stack_frame: InterruptStackFr
10199
extern "x86-interrupt" fn keyboard_interrupt_handler(_stack_frame: InterruptStackFrame) {
102100
use x86_64::instructions::port::Port;
103101

104-
lazy_static! {
105-
static ref KEYBOARD: Mutex<Keyboard<layouts::Us104Key, ScancodeSet1>> =
106-
Mutex::new(Keyboard::new(
107-
ScancodeSet1::new(),
108-
layouts::Us104Key,
109-
HandleControl::Ignore
110-
));
111-
}
112-
113-
let mut keyboard = KEYBOARD.lock();
114102
let mut port = Port::new(0x60);
115-
116103
let scancode: u8 = unsafe { port.read() };
117104

118-
if let Ok(Some(key_event)) = keyboard.add_byte(scancode) {
119-
if let Some(key) = keyboard.process_keyevent(key_event) {
120-
match key {
121-
DecodedKey::Unicode(character) => print!("{}", character),
122-
DecodedKey::RawKey(key) => print!("{:?}", key),
123-
}
124-
}
125-
}
105+
crate::task::keyboard::add_scancode(scancode);
126106

127107
unsafe {
128108
PICS.lock()

kernel/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub mod gdt;
1515
pub mod interrupts;
1616
pub mod memory;
1717
pub mod serial;
18+
pub mod task;
1819
pub mod userspace;
1920

2021
pub trait Testable {

kernel/src/main.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ use bootloader_api::{
1111
entry_point,
1212
};
1313
use core::panic::PanicInfo;
14-
use kernel::{framebuffer, println, userspace};
14+
use kernel::{
15+
framebuffer, println,
16+
task::{executor::Executor, keyboard},
17+
userspace,
18+
};
1519

1620
extern crate alloc;
1721

@@ -62,14 +66,27 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
6266
);
6367

6468
#[cfg(not(test))]
69+
#[cfg(userspace)]
6570
unsafe {
6671
userspace::jump_to_userspace(physical_memory_offset);
6772
}
6873

74+
let mut executor = Executor::new();
75+
executor.spawn(Task::new(example_task()));
76+
executor.spawn(Task::new(keyboard::print_keypresses()));
77+
executor.run();
78+
6979
#[cfg(test)]
7080
test_main();
81+
}
7182

72-
kernel::hlt_loop();
83+
async fn async_number() -> u32 {
84+
42
85+
}
86+
87+
async fn example_task() {
88+
let number = async_number().await;
89+
println!("async number: {}", number);
7390
}
7491

7592
#[cfg(not(test))]

kernel/src/task/executor.rs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
use super::{Task, TaskId};
2+
use alloc::{collections::BTreeMap, sync::Arc, task::Wake};
3+
use core::task::{Context, Poll, Waker};
4+
use crossbeam_queue::ArrayQueue;
5+
6+
pub struct Executor {
7+
tasks: BTreeMap<TaskId, Task>,
8+
task_queue: Arc<ArrayQueue<TaskId>>,
9+
waker_cache: BTreeMap<TaskId, Waker>,
10+
}
11+
12+
impl Executor {
13+
pub fn new() -> Self {
14+
Executor {
15+
tasks: BTreeMap::new(),
16+
task_queue: Arc::new(ArrayQueue::new(100)),
17+
waker_cache: BTreeMap::new(),
18+
}
19+
}
20+
21+
/// Spawns a new task.
22+
///
23+
/// Because we are making a mutable loan from the executor, we can no longer execute `spawn` after the `run`
24+
/// method starts executing, plus `run` implements an infinite loop with a divergent return.
25+
/// One solution is to create a custom `Spawner` type that shares a queue with the `Executor`, a queue shared with it,
26+
/// or its own queue that is synchronized by the `Executor`.
27+
///
28+
/// Remember that Rust doesn't allow having two mutable borrows at the same time, except for reborrowing.
29+
pub fn spawn(&mut self, task: Task) {
30+
let task_id = task.id;
31+
32+
if self.tasks.insert(task.id, task).is_some() {
33+
panic!("task with same ID already in tasks");
34+
}
35+
36+
self.task_queue.push(task_id).expect("queue full");
37+
}
38+
39+
fn run_ready_tasks(&mut self) {
40+
// Destructuring is necessary because in the closure below we attempt to perform a full borrow of
41+
// self in order to obtain the waker_cache.
42+
let Self {
43+
tasks,
44+
task_queue,
45+
waker_cache,
46+
} = self;
47+
48+
while let Some(task_id) = task_queue.pop() {
49+
let task = match tasks.get_mut(&task_id) {
50+
Some(task) => task,
51+
// Task no longer exists.
52+
None => continue,
53+
};
54+
55+
let waker = waker_cache
56+
.entry(task_id)
57+
.or_insert_with(|| TaskWaker::new(task_id, task_queue.clone()));
58+
59+
let mut context = Context::from_waker(waker);
60+
61+
match task.poll(&mut context) {
62+
Poll::Ready(()) => {
63+
// If the task is complete, remove it and its curly waker. There's no reason to keep them,
64+
// since the task is finished.
65+
tasks.remove(&task_id);
66+
waker_cache.remove(&task_id);
67+
}
68+
69+
Poll::Pending => {}
70+
}
71+
}
72+
}
73+
74+
/// The executor spins.
75+
///
76+
/// Because the keyboard task, for example, prevents the tasks map from being empty, a loop with a
77+
/// divergent return value should model such tasks.
78+
pub fn run(&mut self) -> ! {
79+
loop {
80+
self.run_ready_tasks();
81+
self.sleep_if_idle();
82+
}
83+
}
84+
85+
/// It puts the CPU into sleep mode when there are no tasks in the task queue, preventing the CPU from becoming busy.
86+
fn sleep_if_idle(&self) {
87+
use x86_64::instructions::interrupts::{self, enable_and_hlt};
88+
89+
interrupts::disable(); // Prevent race conditions
90+
// Between run_ready_tasks and sleep_if_idle, an interruption may occur and the queue may not become empty, hence the new check.
91+
if self.task_queue.is_empty() {
92+
// We disabled interrupts earlier because if an interrupt happens here, we'll lose the wakeup.
93+
// After verifying that there are indeed no tasks in the queue, we re-enable interrupts and activate
94+
// the hlt instruction to enter sleep mode. This is all done atomically.
95+
enable_and_hlt();
96+
} else {
97+
// This means that after run_ready_tasks a new task was added by an interrupt, so we re-enable interrupts
98+
// and re-enter the loop.
99+
interrupts::enable();
100+
}
101+
}
102+
}
103+
104+
/// The waker's job is to push the waken task ID to the task_queue.
105+
/// Next, the `Executor` polls for the new task.
106+
struct TaskWaker {
107+
task_id: TaskId,
108+
// Ownership of task_queue is shared between wakers and executors through the Arc wrapper type,
109+
// which is based on reference counting.
110+
task_queue: Arc<ArrayQueue<TaskId>>,
111+
}
112+
113+
impl TaskWaker {
114+
fn new(task_id: TaskId, task_queue: Arc<ArrayQueue<TaskId>>) -> Waker {
115+
// The Waker type supports conversions using the From trait when the type in question implements the Wake trait.
116+
// This is because we are wrapping a type that implements the Wake trait, where this trait uses the Arc smart pointer.
117+
Waker::from(Arc::new(TaskWaker {
118+
task_id,
119+
task_queue,
120+
}))
121+
}
122+
123+
fn wake_task(&self) {
124+
self.task_queue.push(self.task_id).expect("task_queue full");
125+
}
126+
}
127+
128+
// We need to instantiate a Waker from the TaskWaker, where the simplest way is to
129+
// implement the Wake trait, which is based on Arc.
130+
// It's based on Arc because wakers are commonly shared between executors and asynchronous tasks.
131+
// The Waker type supports conversions using the From trait when the type in question implements the Wake trait.
132+
impl Wake for TaskWaker {
133+
// Since this captures ownership, it increases the number of references in Arc.
134+
fn wake(self: Arc<Self>) {
135+
self.wake_task();
136+
}
137+
138+
// Implementing this method is optional because not all data types support waking by reference.
139+
// However, implementing it provides performance benefits because it eliminates the need to modify
140+
// the reference count, for example.
141+
fn wake_by_ref(self: &Arc<Self>) {
142+
// Since our type only requires one &self reference, this is easy to resolve.
143+
self.wake_task();
144+
}
145+
}

kernel/src/task/keyboard.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Allows for the single initialization of static variables.
2+
use conquer_once::spin::OnceCell;
3+
4+
// Allows for a fixed-size queue without locks.
5+
use crossbeam_queue::ArrayQueue;
6+
use futures_util::stream::StreamExt;
7+
use futures_util::task::AtomicWaker;
8+
use pc_keyboard::{DecodedKey, HandleControl, Keyboard, ScancodeSet1, layouts};
9+
use pc_keyboard::{DecodedKey, HandleControl, ScancodeSet1, layouts};
10+
11+
use crate::{println, task::keyboard};
12+
13+
// We use `OnceCell` because `ArrayQueue::new` performs heap allocation, which is not allowed with static variables.
14+
// We don't use `lazy-static` because we need to ensure predictable queue initialization.
15+
// Otherwise, it could be initialized in interrupt handlers, which can lead to heap allocation.
16+
static SCANCODE_QUEUE: OnceCell<ArrayQueue<u8>> = OnceCell::uninit();
17+
18+
// Store the `Waker` using `AtomicWaker`. We cannot use a field in `ScancodeStream` because it needs to be visible from `add_scancode`.
19+
// `poll_next` as a consumer stores the wake.
20+
// `add_scancode` as a producer triggers the wake.
21+
static WAKER: AtomicWaker = AtomicWaker::new();
22+
23+
pub(crate) fn add_scancode(scancode: u8) {
24+
if let Ok(queue) = SCANCODE_QUEUE.try_get() {
25+
if let Err(_) = queue.push(scancode) {
26+
println!("WARNING: scancode queue full; dropping keyboard input");
27+
} else {
28+
WAKER.wake();
29+
}
30+
} else {
31+
println!("WARNING: scancode queue uninitialized");
32+
}
33+
}
34+
35+
pub struct ScancodeStream {
36+
/// Prevents the struct from being constructed outside the module.
37+
_private: (),
38+
}
39+
40+
impl ScancodeStream {
41+
pub fn new() -> Self {
42+
SCANCODE_QUEUE
43+
.try_init_once(|| ArrayQueue::new(100))
44+
.expect("ScancodeStream::new should only be called once");
45+
46+
ScancodeStream { _private: () }
47+
}
48+
}
49+
50+
impl Stream for ScancodeStream {
51+
type Item = u8;
52+
53+
fn poll_next(self: Pin<&mut Self>, ctx: &mut Context) -> Poll<Option<u8>> {
54+
let queue = SCANCODE_QUEUE
55+
.try_get()
56+
.expect("scancode queue not initialized");
57+
58+
// Prevent initialization of WAKER. This is a fast path.
59+
if let Some(scancode) = queue.pop() {
60+
return Poll::Ready(Some(scancode));
61+
}
62+
63+
WAKER.register(&ctx.waker());
64+
65+
match queue.pop() {
66+
Some(scancode) => {
67+
WAKER.take();
68+
Poll::Ready(Some(scancode))
69+
}
70+
None => Poll::Pending,
71+
}
72+
}
73+
}
74+
75+
pub async fn print_keypresses() {
76+
let mut scancodes = ScancodeStream::new();
77+
let mut keyboard = Keyboard::new(
78+
ScancodeSet1::new(),
79+
layouts::Us104Key,
80+
HandleControl::Ignore,
81+
);
82+
83+
// It repeatedly obtains a scancode from the stream.
84+
// `.next` is obtained by the `StreamExt` trait, which returns a future that resolves to the next element in the stream.
85+
while let Some(scancode) = scancodes.next().await {
86+
if let Ok(Some(key_event)) = keyboard.add_byte(scancode) {
87+
if let Some(key) = keyboard.process_keyevent(key_event) {
88+
match key {
89+
DecodedKey::Unicode(character) => print!("{}", character),
90+
DecodedKey::RawKey(key) => print!("{:?}", key),
91+
}
92+
}
93+
}
94+
}
95+
}

0 commit comments

Comments
 (0)