rust-kernel-riscv
is an open-source project that implements an operating system kernel on RISC-V architecture with Rust programming language. The project draws inspiration from several open-source implementations, such as xv6-riscv and zCore.
-
The kernel leverages Rust's asynchronous programming model to schedule threads in both the kernel and user space, which makes context switching more efficient and eliminates the need of allocating a separate kernel stack for each user process.
-
The kernel implements the kernel page-table isolation, which prevents the kernel space and the user space to share the same page table and mitigates potential Meltdown attacks.
- Install the
riscv64gc-unknown-none-elf
target and related components:
rustup install nightly
rustup target add riscv64gc-unknown-none-elf
rustup component add llvm-tools-preview
rustup component add rust-src
cargo install cargo-binutils
- Install QEMU with a package manager such as Homebrew:
brew install qemu
- Build and run the kernel with QEMU:
make qemu
The kernel executor handles the management and execution of tasks, which can be either user threads or kernel threads. As of the current implementation, the TaskQueue
is a wrapper around the VecDeque<Runnable>
type, which store and execute tasks in a FIFO order. The run_until_complete
function blocks the calling thread and runs all the tasks in the TaskQueue
.
lazy_static! {
static ref TASK_QUEUE: Mutex<TaskQueue> = Mutex::new(TaskQueue::new());
}
pub fn run_until_complete() {
loop {
let task = TASK_QUEUE.lock().pop_front();
if let Some(task) = task {
task.run();
} else {
break;
}
}
}
The trampoline is a dedicated page that acts as a bridge for transferring control between supervisor and user modes, which is located at the same address (0xFFFFFFFFFFFFF000
) in both kernel and user thread page tables. Identical mapping is required because the program counter must point to a valid location after switching the page table.
The trampoline contains a pair of naked functions, _enter_kernel_space
and _enter_user_space
:
-
_enter_user_space
stores callee-saved registers on the kernel stack, switches to the page table of the user thread, and restores the context (registers,sstatuc
,sepc
) of the user thread from aTrapContext
. Following these steps, it uses asret
instruction to return to user mode. -
_enter_kernel_space
stores the context (registers,sstatuc
,sepc
) of the user thread to aTrapContext
, switch to the page table of the kernel, and restores the callee-saved registers from the kernel stack. Following these steps, it uses aret
instruction to jump to thethread_loop
, which will handle the exception or interrupt.
Each user thread is represented with the executor::future::thread_loop
future. The executor runs a future with its poll
method, and the thread_loop
invokes _enter_user_space
function to enter the user mode. The _enter_user_space
returns when an exception or interrupt occurs, and the thread_loop
handles them and decide whether to continue, yield, or terminate the thread. The spawn_thread
function is used to add a new user thread to the executor.
async fn thread_loop(thread: Arc<Thread>) {
loop {
let trap_context = thread.state().lock().user_trap_context_mut();
_enter_user_space(trap_context, thread.satp());
// Invokes related methods to handle the exception or interrupt,
// which returns a variant of the `ControlFlow` enum
// (Please refer to the source code)
// Decides whether to continue, yield, or terminate the thread
match control_flow {
ControlFlow::Continue => continue,
ControlFlow::Yield => yield_now().await,
ControlFlow::Exit(exit_code) => {
thread.exit(exit_code);
break;
}
}
}
}
pub fn spawn_thread(thread: Arc<Thread>) {
let (runnable, task) = executor::spawn(thread_loop(thread));
runnable.schedule();
task.detach();
}
- The user thread is initiated using the spawn_thread function, which encapsulates it within the
thread_loop
future and incorporates it into the executor. - The executor chooses a task from the TaskQueue and polls it, executing the
thread_loop
. - To enter user mode, the
thread_loop
calls_enter_user_space
. - The user thread operates in user mode.
- If a trap (exception or interrupt) arises, the
_enter_kernel_space
specified in the stvec register is triggered, returning control to thethread_loop
. - The
thread_loop
manages the trap and determines whether to continue, yield, or terminate the thread. If it chooses to continue, thethread_loop
moves on to the next iteration.
- File system with asynchronous interface
- Virtio driver
- TCP/IP stack
- Linux-compatible system call interface
- musl libc-test