hermit-os / kernel

A Rust-based, lightweight unikernel.
http://hermit-os.org
Apache License 2.0
1.16k stars 85 forks source link

Rethink scheduling using `Future`s #787

Open mkroening opened 1 year ago

mkroening commented 1 year ago

It might be interesting to revision scheduling by making all tasks a Future.

joboet commented 1 year ago

This would need a bit of planning beforehand, as there are multiple ways/levels of implementing this:

Thread wraps userspace or syscall

For my (unreleased) kernel, I'm using a kernel-thread-per-core design where the user threads are handles with an execute method that returns a result indicating the trap reason (syscall number and parameters or interrupt number). The kernel main loop then matches on this enum and performs the required actions. This has the benefit of working nicely with the ownership and borrowing rules but requires something akin to syscall numbers.

struct UserThread { .. }

impl UserThread {
   fn execute(&mut self, retval: isize) -> Return { .. }
}

enum Return {
   Syscall(u8 or fn(..) -> usize, Parameters),
   Interrupt(u8),
}

// Execute these futures in the scheduler
async fn thread(mut user: UserThread) {
    let mut retval = 0;
    loop {
        match user.execute(retval) {
            // Handle syscall/interrupt
        }
    }
}

Thread wraps userspace wraps syscall

Another way of doing this would be to make all blocking syscalls async and wrap them in handlers that, on receiving Poll::Pending, restore the execution state before userspace was entered, where a Poll::Pending result is passed back to the scheduler. This eliminates any jump tables but requires allocating the syscall futures on the heap or on the user stack.

extern "C" fn syscall_wrapper(..params) -> isize {
    let mut future = pin!(syscall(..params));
    loop {
        match future.as_mut().poll(scheduler.waker()) {
            Poll::Ready(retval) => return retval,
            Poll::Pending => {
                exchange_context_with_kernel_context(terminate: false/true);
            }
        }
    }
}

struct Thread { .. }

// Could be a separate method instead of a `Future` implementation.
impl Future for Thread {
    fn poll(..) -> Poll<()> {
        let terminate = execute_userspace();
        if terminate {
            Poll::Ready(())
        } else {
            Poll::Pending
        }
    }
}
mkroening commented 1 year ago

The first option sounds really interesting. I had thought of something similar to the second option, I think, but I have not looked deeper into this topic. I also did not discuss with @stlankes if this was something that he would be interested in adopting.

What level of interest do you have in this, @joboet? Would you be interested in contributing something like this? If not, I will look into this myself, eventually, but that will probably take a while. :)

joboet commented 1 year ago

My last exam for this semester is on Saturday, and I'll have plenty of time over the summer break, so sure, I can do it :smile:!

mkroening commented 1 year ago

Great! 🥳 You are also welcome to join our Zulip for coordination, if you like. :)