Compare commits

...

17 Commits

@ -0,0 +1,25 @@
name: Build Rust Doc
on: [push]
env:
CARGO_TERM_COLOR: always
jobs:
build-doc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build doc
run: |
rustup target add riscv64gc-unknown-none-elf
rustup component add llvm-tools-preview
rustup component add rust-src
cd os
cargo doc --no-deps --verbose
- name: Deploy to Github Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./os/target/riscv64gc-unknown-none-elf/doc
destination_dir: ${{ github.ref_name }}

1
.gitignore vendored

@ -5,6 +5,7 @@ os/src/link_app.S
os/src/linker.ld
os/last-*
os/Cargo.lock
os/.gdb_history
user/target/*
user/.idea/*
user/Cargo.lock

@ -1,8 +1,12 @@
# rCore-Tutorial-v3
rCore-Tutorial version 3.5. See the [Documentation in Chinese](https://rcore-os.github.io/rCore-Tutorial-Book-v3/).
rCore-Tutorial API Docs. See the [API Docs of Ten OSes ](#OS-API-DOCS)
Official QQ group number: 735045051
## news
- 2021.11.20: Now we are updating our labs. Please checkout chX-dev Branches for our current new labs. (Notice: please see the [Dependency] section in the end of this doc)
- 25/01/2022: Version 3.6.0 is on the way! Now we directly update the code on chX branches, please periodically check if there are any updates.
## Overview
@ -12,7 +16,7 @@ This project aims to show how to write an **Unix-like OS** running on **RISC-V**
* Platform supported: `qemu-system-riscv64` simulator or dev boards based on [Kendryte K210 SoC](https://canaan.io/product/kendryteai) such as [Maix Dock](https://www.seeedstudio.com/Sipeed-MAIX-Dock-p-4815.html)
* OS
* concurrency of multiple processes
* concurrency of multiple processes each of which contains mutiple native threads
* preemptive scheduling(Round-Robin algorithm)
* dynamic memory management in kernel
* virtual memory
@ -21,15 +25,199 @@ This project aims to show how to write an **Unix-like OS** running on **RISC-V**
* **only 4K+ LoC**
* [A detailed documentation in Chinese](https://rcore-os.github.io/rCore-Tutorial-Book-v3/) in spite of the lack of comments in the code(English version is not available at present)
## Prerequisites
### Install Rust
See [official guide](https://www.rust-lang.org/tools/install).
Install some tools:
```sh
$ rustup target add riscv64gc-unknown-none-elf
$ cargo install cargo-binutils --vers =0.3.3
$ rustup component add llvm-tools-preview
$ rustup component add rust-src
```
### Install Qemu
Here we manually compile and install Qemu 5.0.0. For example, on Ubuntu 18.04:
```sh
# install dependency packages
$ sudo apt install autoconf automake autotools-dev curl libmpc-dev libmpfr-dev libgmp-dev \
gawk build-essential bison flex texinfo gperf libtool patchutils bc \
zlib1g-dev libexpat-dev pkg-config libglib2.0-dev libpixman-1-dev git tmux python3 python3-pip
# download Qemu source code
$ wget https://download.qemu.org/qemu-5.0.0.tar.xz
# extract to qemu-5.0.0/
$ tar xvJf qemu-5.0.0.tar.xz
$ cd qemu-5.0.0
# build
$ ./configure --target-list=riscv64-softmmu,riscv64-linux-user
$ make -j$(nproc)
```
Then, add following contents to `~/.bashrc`(please adjust these paths according to your environment):
```
export PATH=$PATH:/home/shinbokuow/Downloads/built/qemu-5.0.0
export PATH=$PATH:/home/shinbokuow/Downloads/built/qemu-5.0.0/riscv64-softmmu
export PATH=$PATH:/home/shinbokuow/Downloads/built/qemu-5.0.0/riscv64-linux-user
```
Finally, update the current shell:
```sh
$ source ~/.bashrc
```
Now we can check the version of Qemu:
```sh
$ qemu-system-riscv64 --version
QEMU emulator version 5.0.0
Copyright (c) 2003-2020 Fabrice Bellard and the QEMU Project developers
```
### Install RISC-V GNU Embedded Toolchain(including GDB)
Download the compressed file according to your platform From [Sifive website](https://www.sifive.com/software)(Ctrl+F 'toolchain').
Extract it and append the location of the 'bin' directory under its root directory to `$PATH`.
For example, we can check the version of GDB:
```sh
$ riscv64-unknown-elf-gdb --version
GNU gdb (SiFive GDB-Metal 10.1.0-2020.12.7) 10.1
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
```
### Install serial tools(Optional, if you want to run on K210)
```sh
$ pip3 install pyserial
$ sudo apt install python3-serial
```
## Run our project
TODO:
### Qemu
```sh
$ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
$ cd rCore-Tutorial-v3/os
$ make run
```
After outputing some debug messages, the kernel lists all the applications available and enter the user shell:
```
/**** APPS ****
mpsc_sem
usertests
pipetest
forktest2
cat
initproc
race_adder_loop
threads_arg
race_adder_mutex_spin
race_adder_mutex_blocking
forktree
user_shell
huge_write
race_adder
race_adder_atomic
threads
stack_overflow
filetest_simple
forktest_simple
cmdline_args
run_pipe_test
forktest
matrix
exit
fantastic_text
sleep_simple
yield
hello_world
pipe_large_test
sleep
phil_din_mutex
**************/
Rust user shell
>>
```
You can run any application except for `initproc` and `user_shell` itself. To run an application, just input its filename and hit enter. `usertests` can run a bunch of applications, thus it is recommended.
Type `Ctrl+a` then `x` to exit Qemu.
### K210
Before chapter 6, you do not need a SD card:
```sh
$ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
$ cd rCore-Tutorial-v3/os
$ make run BOARD=k210
```
From chapter 6, before running the kernel, we should insert a SD card into PC and manually write the filesystem image to it:
```sh
$ cd rCore-Tutorial-v3/os
$ make sdcard
```
By default it will overwrite the device `/dev/sdb` which is the SD card, but you can provide another location. For example, `make sdcard SDCARD=/dev/sdc`.
After that, remove the SD card from PC and insert it to the slot of K210. Connect the K210 to PC and then:
```sh
$ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
$ cd rCore-Tutorial-v3/os
$ make run BOARD=k210
```
Type `Ctrl+]` to disconnect from K210.
## Rustdoc
Currently it can only help you view the code since only a tiny part of the code has been documented.
You can open a doc html of `os` using `cargo doc --no-deps --open` under `os` directory.
### OS-API-DOCS
The API Docs for Ten OS
1. [Lib-OS API doc](https://learningos.github.io/rCore-Tutorial-v3/ch1/os/index.html)
1. [Batch-OS API doc](https://learningos.github.io/rCore-Tutorial-v3/ch2/os/index.html)
1. [MultiProg-OS API doc](https://learningos.github.io/rCore-Tutorial-v3/ch3-coop/os/index.html)
1. [TimeSharing-OS API doc](https://learningos.github.io/rCore-Tutorial-v3/ch3/os/index.html)
1. [AddrSpace-OS API doc](https://learningos.github.io/rCore-Tutorial-v3/ch4/os/index.html)
1. [Process-OS API doc](https://learningos.github.io/rCore-Tutorial-v3/ch5/os/index.html)
1. [FileSystem-OS API doc](https://learningos.github.io/rCore-Tutorial-v3/ch6/os/index.html)
1. [IPC-OS API doc](https://learningos.github.io/rCore-Tutorial-v3/ch7/os/index.html)
1. [SyncMutex-OS API doc](https://learningos.github.io/rCore-Tutorial-v3/ch8/os/index.html)
1. [IODevice-OS API doc](https://learningos.github.io/rCore-Tutorial-v3/ch9/os/index.html)
## Working in progress
Now we are still updating our project, you can find latest changes on branches `chX-dev` such as `ch1-dev`. We are intended to publish first release 3.5.0 after completing most of the tasks mentioned below.
Our first release 3.5.0 (chapter 1-7) has been published.
There will be 9 chapters in our next release 3.6.0, where 2 new chapters will be added:
* chapter 8: synchronization on a uniprocessor
* chapter 9: I/O devices
Overall progress: ch7
Current version is 3.6.0-alpha.1 and we are still working on it.
Here are the updates since 3.5.0:
### Completed
@ -43,12 +231,18 @@ Overall progress: ch7
* [x] flush all block cache to disk after a fs transaction which involves write operation
* [x] replace `spin::Mutex` with `UPSafeCell` before SMP chapter
* [x] add codes for a new chapter about synchronization & mutual exclusion(uniprocessor only)
* [x] bug fix: we should call `find_pte` rather than `find_pte_create` in `PageTable::unmap`
* [x] clarify: "check validity of level-3 pte in `find_pte` instead of checking it outside this function" should not be a bug
* [x] code of chapter 8: synchronization on a uniprocessor
* [x] switch the code of chapter 6 and chapter 7
* [x] support signal mechanism in chapter 7/8(only works for apps with a single thread)
* [x] Add boards/ directory and support rustdoc, for example you can use `cargo doc --no-deps --open` to view the documentation of a crate
### Todo(High priority)
* [ ] support Allwinner's RISC-V D1 chip
* [ ] bug fix: we should call `find_pte` rather than `find_pte_create` in `PageTable::unmap`
* [ ] bug fix: check validity of level-3 pte in `find_pte` instead of checking it outside this function
* [ ] review documentation, current progress: 5/9
* [ ] support user-level sync primitives in chapter 8
* [ ] code of chapter 9: device drivers based on interrupts, including UART and block devices
* [ ] use old fs image optionally, do not always rebuild the image
* [ ] add new system calls: getdents64/fstat
* [ ] shell functionality improvement(to be continued...)
@ -61,7 +255,6 @@ Overall progress: ch7
* [ ] provide smooth debug experience at a Rust source code level
* [ ] format the code using official tools
### Crates
We will add them later.

@ -8,4 +8,7 @@ edition = "2018"
[dependencies]
spin = "0.7.0"
lazy_static = { version = "1.4.0", features = ["spin_no_std"] }
lazy_static = { version = "1.4.0", features = ["spin_no_std"] }
[profile.release]
debug = true

@ -20,4 +20,7 @@ easy-fs = { path = "../easy-fs" }
[features]
board_qemu = []
board_k210 = []
board_k210 = []
[profile.release]
debug = true

@ -14,6 +14,11 @@ SBI ?= rustsbi
BOOTLOADER := ../bootloader/$(SBI)-$(BOARD).bin
K210_BOOTLOADER_SIZE := 131072
# Building mode argument
ifeq ($(MODE), release)
MODE_ARG := --release
endif
# KERNEL ENTRY
ifeq ($(BOARD), qemu)
KERNEL_ENTRY_PA := 0x80200000
@ -106,4 +111,11 @@ debug: build
tmux split-window -h "riscv64-unknown-elf-gdb -ex 'file $(KERNEL_ELF)' -ex 'set arch riscv:rv64' -ex 'target remote localhost:1234'" && \
tmux -2 attach-session -d
.PHONY: build env kernel clean disasm disasm-vim run-inner switch-check fs-img
gdbserver: build
@qemu-system-riscv64 -machine virt -nographic -bios $(BOOTLOADER) -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA) -s -S
gdbclient:
@riscv64-unknown-elf-gdb -ex 'file $(KERNEL_ELF)' -ex 'set arch riscv:rv64' -ex 'target remote localhost:1234'
.PHONY: build env kernel clean disasm disasm-vim run-inner switch-check fs-img gdbserver gdbclient

@ -53,7 +53,9 @@ pub fn rust_main() -> ! {
trap::init();
trap::enable_timer_interrupt();
timer::set_next_trigger();
task::stackless_coroutine::kernel_stackless_coroutine_test();
fs::list_apps();
task::kernel_stackful_coroutine_test();
task::add_initproc();
task::run_tasks();
panic!("Unreachable in rust_main!");

@ -5,6 +5,7 @@ use alloc::vec::Vec;
use core::fmt::{self, Debug, Formatter};
use lazy_static::*;
#[derive(Clone)]
pub struct FrameTracker {
pub ppn: PhysPageNum,
}

@ -242,8 +242,17 @@ impl MemorySet {
//*self = Self::new_bare();
self.areas.clear();
}
pub fn kernel_copy() -> Self {
let areas = KERNEL_SPACE.exclusive_access().areas.clone();
Self {
page_table: PageTable::from_token(kernel_token()),
areas: areas,
}
}
}
#[derive(Clone)]
pub struct MapArea {
vpn_range: VPNRange,
data_frames: BTreeMap<VirtPageNum, FrameTracker>,

@ -2,9 +2,9 @@ use crate::trap::trap_return;
#[repr(C)]
pub struct TaskContext {
ra: usize,
sp: usize,
s: [usize; 12],
pub ra: usize,
pub sp: usize,
pub s: [usize; 12],
}
impl TaskContext {
@ -15,6 +15,7 @@ impl TaskContext {
s: [0; 12],
}
}
pub fn goto_trap_return(kstack_ptr: usize) -> Self {
Self {
ra: trap_return as usize,

@ -1,6 +1,6 @@
use super::ProcessControlBlock;
use crate::config::{KERNEL_STACK_SIZE, PAGE_SIZE, TRAMPOLINE, TRAP_CONTEXT_BASE, USER_STACK_SIZE};
use crate::mm::{MapPermission, PhysPageNum, VirtAddr, KERNEL_SPACE};
use crate::mm::{MapPermission, PhysPageNum, VirtAddr, KERNEL_SPACE, PhysAddr};
use crate::sync::UPSafeCell;
use alloc::{
sync::{Arc, Weak},
@ -70,6 +70,7 @@ pub struct KernelStack(pub usize);
pub fn kstack_alloc() -> KernelStack {
let kstack_id = KSTACK_ALLOCATOR.exclusive_access().alloc();
let (kstack_bottom, kstack_top) = kernel_stack_position(kstack_id);
//println!("kstack_alloc kstack_bottom: {:#x?}, kstack_top: {:#x?}", kstack_bottom, kstack_top);
KERNEL_SPACE.exclusive_access().insert_framed_area(
kstack_bottom.into(),
kstack_top.into(),
@ -82,6 +83,8 @@ impl Drop for KernelStack {
fn drop(&mut self) {
let (kernel_stack_bottom, _) = kernel_stack_position(self.0);
let kernel_stack_bottom_va: VirtAddr = kernel_stack_bottom.into();
// let kernel_stack_bottom_pa: PhysAddr = kernel_stack_bottom.into();
// println!("kstack_drop kstack_bottom: va: {:#x?}, pa: {:#x?}", kernel_stack_bottom_va, kernel_stack_bottom_pa);
KERNEL_SPACE
.exclusive_access()
.remove_area_with_start_vpn(kernel_stack_bottom_va.into());
@ -221,3 +224,41 @@ impl Drop for TaskUserRes {
self.dealloc_user_res();
}
}
use alloc::alloc::{alloc, dealloc, Layout};
#[derive(Clone)]
pub struct KStack(usize);
const STACK_SIZE: usize = 0x8000;
impl KStack {
pub fn new() -> KStack {
let bottom =
unsafe {
alloc(Layout::from_size_align(STACK_SIZE, STACK_SIZE).unwrap())
} as usize;
KStack(bottom)
}
pub fn top(&self) -> usize {
self.0 + STACK_SIZE
}
}
use core::fmt::{self, Debug, Formatter};
impl Debug for KStack {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_fmt(format_args!("KStack:{:#x}", self.0))
}
}
impl Drop for KStack {
fn drop(&mut self) {
unsafe {
dealloc(
self.0 as _,
Layout::from_size_align(STACK_SIZE, STACK_SIZE).unwrap()
);
}
}
}

@ -5,6 +5,7 @@ mod process;
mod processor;
mod signal;
mod switch;
pub mod stackless_coroutine;
#[allow(clippy::module_inception)]
mod task;
@ -25,6 +26,9 @@ pub use processor::{
pub use signal::SignalFlags;
pub use task::{TaskControlBlock, TaskStatus};
pub use stackless_coroutine::kernel_stackless_coroutine_test;
pub fn suspend_current_and_run_next() {
// There must be an application running.
let task = take_current_task().unwrap();
@ -127,3 +131,82 @@ pub fn current_add_signal(signal: SignalFlags) {
let mut process_inner = process.inner_exclusive_access();
process_inner.signals |= signal;
}
#[no_mangle]
pub fn kthread_create(f: fn()) {
println!("kthread_create");
// create kernel thread
let new_tcb = TaskControlBlock::create_kthread(f);
// let kernel_stack = new_tcb.get_kernel_stack();
let new_task = Arc::new(new_tcb);
// add kernel thread into TASK_MANAGER
// println!("add task");
add_task(Arc::clone(&new_task));
}
#[no_mangle]
pub fn kernel_stackful_coroutine_test() {
println!("kernel_stackful_coroutine_test");
kthread_create( ||
{
let id = 1;
println!("kernel thread {:?} STARTING", id);
for i in 0..10 {
println!("kernel thread: {} counter: {}", id, i);
}
println!("kernel thread {:?} FINISHED", id);
kthread_stop();
}
);
kthread_create( ||
{
let id = 2;
println!("kernel thread {:?} STARTING", 2);
for i in 0..10 {
println!("kernel thread: {} counter: {}", 2, i);
kthread_yield();
}
println!("kernel thread {:?} FINISHED", 2);
kthread_stop();
}
);
kthread_create( ||
{
let id = 3;
println!("kernel thread {:?} STARTING", 3);
for i in 0..10 {
println!("kernel thread: {} counter: {}", 3, i);
kthread_yield();
}
println!("kernel thread {:?} FINISHED", 3);
kthread_stop();
}
);
}
pub fn kthread_stop(){
do_exit();
}
#[no_mangle]
pub fn do_exit(){
println!("kthread do exit");
exit_kthread_and_run_next(0);
panic!("Unreachable in sys_exit!");
}
pub fn kthread_yield(){
suspend_current_and_run_next();
}
#[no_mangle]
pub fn exit_kthread_and_run_next(exit_code: i32) {
println!("exit_kthread_and_run_next");
// we do not have to save task context
let mut _unused = TaskContext::zero_init();
schedule(&mut _unused as *mut _);
}

@ -256,4 +256,30 @@ impl ProcessControlBlock {
pub fn getpid(&self) -> usize {
self.pid.0
}
pub fn kernel_process() -> Arc<Self>{
let memory_set = MemorySet::kernel_copy();
let process = Arc::new(
ProcessControlBlock {
pid: super::pid_alloc(),
inner: unsafe {
UPSafeCell::new(
ProcessControlBlockInner {
is_zombie: false,
memory_set: memory_set,
parent: None,
children: Vec::new(),
exit_code: 0,
fd_table: Vec::new(),
signals: SignalFlags::empty(),
tasks: Vec::new(),
task_res_allocator: RecycleAllocator::new(),
mutex_list: Vec::new(),
semaphore_list: Vec::new(),
condvar_list: Vec::new(),
})
},
});
process
}
}

@ -0,0 +1,121 @@
// https://blog.aloni.org/posts/a-stack-less-rust-coroutine-100-loc/
// https://github.com/chyyuu/example-coroutine-and-thread/tree/stackless-coroutine-x86
use core::future::Future;
use core::pin::Pin;
use core::task::{Context, Poll};
use core::task::{RawWaker, RawWakerVTable, Waker};
extern crate alloc;
use alloc::collections::VecDeque;
use alloc::boxed::Box;
enum State {
Halted,
Running,
}
struct Task {
state: State,
}
impl Task {
fn waiter<'a>(&'a mut self) -> Waiter<'a> {
Waiter { task: self }
}
}
struct Waiter<'a> {
task: &'a mut Task,
}
impl<'a> Future for Waiter<'a> {
type Output = ();
fn poll(mut self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Self::Output> {
match self.task.state {
State::Halted => {
self.task.state = State::Running;
Poll::Ready(())
}
State::Running => {
self.task.state = State::Halted;
Poll::Pending
}
}
}
}
struct Executor {
tasks: VecDeque<Pin<Box<dyn Future<Output = ()>>>>,
}
impl Executor {
fn new() -> Self {
Executor {
tasks: VecDeque::new(),
}
}
fn push<C, F>(&mut self, closure: C)
where
F: Future<Output = ()> + 'static,
C: FnOnce(Task) -> F,
{
let task = Task {
state: State::Running,
};
self.tasks.push_back(Box::pin(closure(task)));
}
fn run(&mut self) {
let waker = create_waker();
let mut context = Context::from_waker(&waker);
while let Some(mut task) = self.tasks.pop_front() {
match task.as_mut().poll(&mut context) {
Poll::Pending => {
self.tasks.push_back(task);
}
Poll::Ready(()) => {}
}
}
}
}
pub fn create_waker() -> Waker {
// Safety: The waker points to a vtable with functions that do nothing. Doing
// nothing is memory-safe.
unsafe { Waker::from_raw(RAW_WAKER) }
}
const RAW_WAKER: RawWaker = RawWaker::new(core::ptr::null(), &VTABLE);
const VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);
unsafe fn clone(_: *const ()) -> RawWaker {
RAW_WAKER
}
unsafe fn wake(_: *const ()) {}
unsafe fn wake_by_ref(_: *const ()) {}
unsafe fn drop(_: *const ()) {}
#[no_mangle]
pub fn kernel_stackless_coroutine_test() {
println!("kernel stackless coroutine Begin..");
let mut exec = Executor::new();
println!(" Create futures");
for instance in 1..=3 {
exec.push(move |mut task| async move {
println!(" Kernel Task {}: begin state", instance);
task.waiter().await;
println!(" Kernel Task {}: next state", instance);
task.waiter().await;
println!(" Kernel Task {}: end state", instance);
});
}
println!(" Running");
exec.run();
println!(" Done");
println!("kernel stackless coroutine PASSED");
}

@ -68,6 +68,42 @@ impl TaskControlBlock {
},
}
}
pub fn create_kthread(f: fn()) -> Self{
use crate::mm::{KERNEL_SPACE, PhysPageNum, VirtAddr, PhysAddr};
let process = ProcessControlBlock::kernel_process();
let process = Arc::downgrade(&process);
let kstack = kstack_alloc();
let kernelstack = crate::task::id::KStack::new();
let kstack_top = kernelstack.top();
let mut context = TaskContext::zero_init();
let context_addr = &context as *const TaskContext as usize;
let pa = PhysAddr::from(context_addr);
let context_ppn = pa.floor();
context.ra = f as usize;
context.sp = kstack_top;
//println!("context ppn :{:#x?}", context_ppn);
Self {
process,
kstack:KernelStack(kstack_top),
//kstack,
inner: unsafe {
UPSafeCell::new(TaskControlBlockInner {
res: None,
trap_cx_ppn: context_ppn,
task_cx: context,
task_status: TaskStatus::Ready,
exit_code: None,
})
},
}
}
}
#[derive(Copy, Clone, PartialEq)]

@ -1 +1 @@
nightly-2022-01-19
nightly-2022-04-11

@ -10,3 +10,6 @@ edition = "2018"
buddy_system_allocator = "0.6"
bitflags = "1.2.1"
riscv = { git = "https://github.com/rcore-os/riscv", features = ["inline-asm"] }
[profile.release]
debug = true

@ -0,0 +1,22 @@
#![no_std]
#![no_main]
#[macro_use]
extern crate user_lib;
extern crate alloc;
use user_lib::{thread_create, exit};
use alloc::vec::Vec;
pub fn thread_a() -> ! {
for i in 0..1000 { print!("{}", i); }
exit(1)
}
#[no_mangle]
pub fn main() -> i32 {
thread_create(thread_a as usize, 0);
println!("main thread exited.");
exit(0)
}

@ -0,0 +1,352 @@
// we porting below codes to Rcore Tutorial v3
// https://cfsamson.gitbook.io/green-threads-explained-in-200-lines-of-rust/
// https://github.com/cfsamson/example-greenthreads
#![no_std]
#![no_main]
#![feature(naked_functions)]
#![feature(asm)]
extern crate alloc;
#[macro_use]
extern crate user_lib;
use core::arch::asm;
#[macro_use]
use alloc::vec;
use alloc::vec::Vec;
use user_lib::{exit};
// In our simple example we set most constraints here.
const DEFAULT_STACK_SIZE: usize = 4096; //128 got SEGFAULT, 256(1024, 4096) got right results.
const MAX_TASKS: usize = 5;
static mut RUNTIME: usize = 0;
pub struct Runtime {
tasks: Vec<Task>,
current: usize,
}
#[derive(PartialEq, Eq, Debug)]
enum State {
Available,
Running,
Ready,
}
struct Task {
id: usize,
stack: Vec<u8>,
ctx: TaskContext,
state: State,
}
#[derive(Debug, Default)]
#[repr(C)] // not strictly needed but Rust ABI is not guaranteed to be stable
pub struct TaskContext {
// 15 u64
x1: u64, //ra: return addres
x2: u64, //sp
x8: u64, //s0,fp
x9: u64, //s1
x18: u64, //x18-27: s2-11
x19: u64,
x20: u64,
x21: u64,
x22: u64,
x23: u64,
x24: u64,
x25: u64,
x26: u64,
x27: u64,
nx1: u64, //new return addres
}
impl Task {
fn new(id: usize) -> Self {
// We initialize each task here and allocate the stack. This is not neccesary,
// we can allocate memory for it later, but it keeps complexity down and lets us focus on more interesting parts
// to do it here. The important part is that once allocated it MUST NOT move in memory.
Task {
id,
stack: vec![0_u8; DEFAULT_STACK_SIZE],
ctx: TaskContext::default(),
state: State::Available,
}
}
}
impl Runtime {
pub fn new() -> Self {
// This will be our base task, which will be initialized in the `running` state
let base_task = Task {
id: 0,
stack: vec![0_u8; DEFAULT_STACK_SIZE],
ctx: TaskContext::default(),
state: State::Running,
};
// We initialize the rest of our tasks.
let mut tasks = vec![base_task];
let mut available_tasks: Vec<Task> = (1..MAX_TASKS).map(|i| Task::new(i)).collect();
tasks.append(&mut available_tasks);
Runtime {
tasks,
current: 0,
}
}
/// This is cheating a bit, but we need a pointer to our Runtime stored so we can call yield on it even if
/// we don't have a reference to it.
pub fn init(&self) {
unsafe {
let r_ptr: *const Runtime = self;
RUNTIME = r_ptr as usize;
}
}
/// This is where we start running our runtime. If it is our base task, we call yield until
/// it returns false (which means that there are no tasks scheduled) and we are done.
pub fn run(&mut self){
while self.t_yield() {}
println!("All tasks finished!");
}
/// This is our return function. The only place we use this is in our `guard` function.
/// If the current task is not our base task we set its state to Available. It means
/// we're finished with it. Then we yield which will schedule a new task to be run.
fn t_return(&mut self) {
if self.current != 0 {
self.tasks[self.current].state = State::Available;
self.t_yield();
}
}
/// This is the heart of our runtime. Here we go through all tasks and see if anyone is in the `Ready` state.
/// If no task is `Ready` we're all done. This is an extremely simple scheduler using only a round-robin algorithm.
///
/// If we find a task that's ready to be run we change the state of the current task from `Running` to `Ready`.
/// Then we call switch which will save the current context (the old context) and load the new context
/// into the CPU which then resumes based on the context it was just passed.
///
/// NOITCE: if we comment below `#[inline(never)]`, we can not get the corrent running result
#[inline(never)]
fn t_yield(&mut self) -> bool {
let mut pos = self.current;
while self.tasks[pos].state != State::Ready {
pos += 1;
if pos == self.tasks.len() {
pos = 0;
}
if pos == self.current {
return false;
}
}
if self.tasks[self.current].state != State::Available {
self.tasks[self.current].state = State::Ready;
}
self.tasks[pos].state = State::Running;
let old_pos = self.current;
self.current = pos;
unsafe {
switch(&mut self.tasks[old_pos].ctx, &self.tasks[pos].ctx);
}
// NOTE: this might look strange and it is. Normally we would just mark this as `unreachable!()` but our compiler
// is too smart for it's own good so it optimized our code away on release builds. Curiously this happens on windows
// and not on linux. This is a common problem in tests so Rust has a `black_box` function in the `test` crate that
// will "pretend" to use a value we give it to prevent the compiler from eliminating code. I'll just do this instead,
// this code will never be run anyways and if it did it would always be `true`.
self.tasks.len() > 0
}
/// While `yield` is the logically interesting function I think this the technically most interesting.
///
/// When we spawn a new task we first check if there are any available tasks (tasks in `Parked` state).
/// If we run out of tasks we panic in this scenario but there are several (better) ways to handle that.
/// We keep things simple for now.
///
/// When we find an available task we get the stack length and a pointer to our u8 bytearray.
///
/// The next part we have to use some unsafe functions. First we write an address to our `guard` function
/// that will be called if the function we provide returns. Then we set the address to the function we
/// pass inn.
///
/// Third, we set the value of `sp` which is the stack pointer to the address of our provided function so we start
/// executing that first when we are scheuled to run.
///
/// Lastly we set the state as `Ready` which means we have work to do and is ready to do it.
pub fn spawn(&mut self, f: fn()) {
let available = self
.tasks
.iter_mut()
.find(|t| t.state == State::Available)
.expect("no available task.");
let size = available.stack.len();
unsafe {
let s_ptr = available.stack.as_mut_ptr().offset(size as isize);
// make sure our stack itself is 8 byte aligned - it will always
// offset to a lower memory address. Since we know we're at the "high"
// memory address of our allocated space, we know that offsetting to
// a lower one will be a valid address (given that we actually allocated)
// enough space to actually get an aligned pointer in the first place).
let s_ptr = (s_ptr as usize & !7) as *mut u8;
available.ctx.x1 = guard as u64; //ctx.x1 is old return address
available.ctx.nx1 = f as u64; //ctx.nx2 is new return address
available.ctx.x2 = s_ptr.offset(-32) as u64; //cxt.x2 is sp
}
available.state = State::Ready;
}
}
/// This is our guard function that we place on top of the stack. All this function does is set the
/// state of our current task and then `yield` which will then schedule a new task to be run.
fn guard() {
unsafe {
let rt_ptr = RUNTIME as *mut Runtime;
(*rt_ptr).t_return();
};
}
/// We know that Runtime is alive the length of the program and that we only access from one core
/// (so no datarace). We yield execution of the current task by dereferencing a pointer to our
/// Runtime and then calling `t_yield`
pub fn yield_task() {
unsafe {
let rt_ptr = RUNTIME as *mut Runtime;
(*rt_ptr).t_yield();
};
}
/// So here is our inline Assembly. As you remember from our first example this is just a bit more elaborate where we first
/// read out the values of all the registers we need and then sets all the register values to the register values we
/// saved when we suspended exceution on the "new" task.
///
/// This is essentially all we need to do to save and resume execution.
///
/// Some details about inline assembly.
///
/// The assembly commands in the string literal is called the assemblt template. It is preceeded by
/// zero or up to four segments indicated by ":":
///
/// - First ":" we have our output parameters, this parameters that this function will return.
/// - Second ":" we have the input parameters which is our contexts. We only read from the "new" context
/// but we modify the "old" context saving our registers there (see volatile option below)
/// - Third ":" This our clobber list, this is information to the compiler that these registers can't be used freely
/// - Fourth ":" This is options we can pass inn, Rust has 3: "alignstack", "volatile" and "intel"
///
/// For this to work on windows we need to use "alignstack" where the compiler adds the neccesary padding to
/// make sure our stack is aligned. Since we modify one of our inputs, our assembly has "side effects"
/// therefore we should use the `volatile` option. I **think** this is actually set for us by default
/// when there are no output parameters given (my own assumption after going through the source code)
/// for the `asm` macro, but we should make it explicit anyway.
///
/// One last important part (it will not work without this) is the #[naked] attribute. Basically this lets us have full
/// control over the stack layout since normal functions has a prologue-and epilogue added by the
/// compiler that will cause trouble for us. We avoid this by marking the funtion as "Naked".
/// For this to work on `release` builds we also need to use the `#[inline(never)] attribute or else
/// the compiler decides to inline this function (curiously this currently only happens on Windows).
/// If the function is inlined we get a curious runtime error where it fails when switching back
/// to as saved context and in general our assembly will not work as expected.
///
/// see: https://github.com/rust-lang/rfcs/blob/master/text/1201-naked-fns.md
/// see: https://doc.rust-lang.org/nightly/reference/inline-assembly.html
/// see: https://doc.rust-lang.org/nightly/rust-by-example/unsafe/asm.html
#[naked]
#[no_mangle]
unsafe fn switch(old: *mut TaskContext, new: *const TaskContext) {
// a0: _old, a1: _new
asm!("
sd x1, 0x00(a0)
sd x2, 0x08(a0)
sd x8, 0x10(a0)
sd x9, 0x18(a0)
sd x18, 0x20(a0)
sd x19, 0x28(a0)
sd x20, 0x30(a0)
sd x21, 0x38(a0)
sd x22, 0x40(a0)
sd x23, 0x48(a0)
sd x24, 0x50(a0)
sd x25, 0x58(a0)
sd x26, 0x60(a0)
sd x27, 0x68(a0)
sd x1, 0x70(a0)
ld x1, 0x00(a1)
ld x2, 0x08(a1)
ld x8, 0x10(a1)
ld x9, 0x18(a1)
ld x18, 0x20(a1)
ld x19, 0x28(a1)
ld x20, 0x30(a1)
ld x21, 0x38(a1)
ld x22, 0x40(a1)
ld x23, 0x48(a1)
ld x24, 0x50(a1)
ld x25, 0x58(a1)
ld x26, 0x60(a1)
ld x27, 0x68(a1)
ld t0, 0x70(a1)
jr t0
", options( noreturn)
);
}
#[no_mangle]
pub fn main() {
println!("stackful_coroutine begin...");
println!("TASK 0(Runtime) STARTING");
let mut runtime = Runtime::new();
runtime.init();
runtime.spawn(|| {
println!("TASK 1 STARTING");
let id = 1;
for i in 0..4 {
println!("task: {} counter: {}", id, i);
yield_task();
}
println!("TASK 1 FINISHED");
});
runtime.spawn(|| {
println!("TASK 2 STARTING");
let id = 2;
for i in 0..8 {
println!("task: {} counter: {}", id, i);
yield_task();
}
println!("TASK 2 FINISHED");
});
runtime.spawn(|| {
println!("TASK 3 STARTING");
let id = 3;
for i in 0..12 {
println!("task: {} counter: {}", id, i);
yield_task();
}
println!("TASK 3 FINISHED");
});
runtime.spawn(|| {
println!("TASK 4 STARTING");
let id = 4;
for i in 0..16 {
println!("task: {} counter: {}", id, i);
yield_task();
}
println!("TASK 4 FINISHED");
});
runtime.run();
println!("stackful_coroutine PASSED");
exit(0);
}

@ -0,0 +1,129 @@
// https://blog.aloni.org/posts/a-stack-less-rust-coroutine-100-loc/
// https://github.com/chyyuu/example-coroutine-and-thread/tree/stackless-coroutine-x86
#![no_std]
#![no_main]
use core::future::Future;
use core::pin::Pin;
use core::task::{Context, Poll};
use core::task::{RawWaker, RawWakerVTable, Waker};
extern crate alloc;
use alloc::collections::VecDeque;
use alloc::boxed::Box;
#[macro_use]
extern crate user_lib;
enum State {
Halted,
Running,
}
struct Task {
state: State,
}
impl Task {
fn waiter<'a>(&'a mut self) -> Waiter<'a> {
Waiter { task: self }
}
}
struct Waiter<'a> {
task: &'a mut Task,
}
impl<'a> Future for Waiter<'a> {
type Output = ();
fn poll(mut self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Self::Output> {
match self.task.state {
State::Halted => {
self.task.state = State::Running;
Poll::Ready(())
}
State::Running => {
self.task.state = State::Halted;
Poll::Pending
}
}
}
}
struct Executor {
tasks: VecDeque<Pin<Box<dyn Future<Output = ()>>>>,
}
impl Executor {
fn new() -> Self {
Executor {
tasks: VecDeque::new(),
}
}
fn push<C, F>(&mut self, closure: C)
where
F: Future<Output = ()> + 'static,
C: FnOnce(Task) -> F,
{
let task = Task {
state: State::Running,
};
self.tasks.push_back(Box::pin(closure(task)));
}
fn run(&mut self) {
let waker = create_waker();
let mut context = Context::from_waker(&waker);
while let Some(mut task) = self.tasks.pop_front() {
match task.as_mut().poll(&mut context) {
Poll::Pending => {
self.tasks.push_back(task);
}
Poll::Ready(()) => {}
}
}
}
}
pub fn create_waker() -> Waker {
// Safety: The waker points to a vtable with functions that do nothing. Doing
// nothing is memory-safe.
unsafe { Waker::from_raw(RAW_WAKER) }
}
const RAW_WAKER: RawWaker = RawWaker::new(core::ptr::null(), &VTABLE);
const VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);
unsafe fn clone(_: *const ()) -> RawWaker {
RAW_WAKER
}
unsafe fn wake(_: *const ()) {}
unsafe fn wake_by_ref(_: *const ()) {}
unsafe fn drop(_: *const ()) {}
#[no_mangle]
pub fn main() -> i32 {
println!("stackless coroutine Begin..");
let mut exec = Executor::new();
println!(" Create futures");
for instance in 1..=3 {
exec.push(move |mut task| async move {
println!(" Task {}: begin state", instance);
task.waiter().await;
println!(" Task {}: next state", instance);
task.waiter().await;
println!(" Task {}: end state", instance);
});
}
println!(" Running");
exec.run();
println!(" Done");
println!("stackless coroutine PASSED");
0
}
Loading…
Cancel
Save