Compare commits

...

20 Commits
main ... ch8

Author SHA1 Message Date
Yifan Wu 82c2eca278 Issue #60: added some comments about mutex/semaphore.
3 years ago
Yifan Wu fcfd5b6db7 Add early_exit2 & Fix issue #60.
3 years ago
Yu Chen 5729debb09 add cargo fmt in Makefile, and exec make fmt
3 years ago
Yifan Wu fdeaca9bb0 Fix #69.
3 years ago
Yifan Wu e8871ec957
Merge pull request #71 from Gallium70/ch8
3 years ago
youyuyang 2e6da60a27 FIX: Avoid double-borrow when thread early exits
3 years ago
Yu Chen 15dec102fa add comments in user/src/bin/stackless_coroutine.rs
3 years ago
Yu Chen 3cb46a7333 add user/src/bin/stackless_coutine.rs
3 years ago
Yu Chen 253f5a4982 add comments about inline asm in usr/src/bin/stackful_coroutine.rs
3 years ago
Yu Chen f0a11d1444 fix typo
3 years ago
Yu Chen e8cabee7d7 add more comments
3 years ago
Yu Chen 119eb9d634 rename green_threads to stackful_coroutine, add #[inline(never)] in t_yield fun, then run correctly
3 years ago
Yu Chen 81d3ffce64 update usr_app: green_threads, but still wrong. rustc add two more instrs in switch function asm codes
3 years ago
Yu Chen 02a3880e34 Merge branch 'ch8' of github.com:rcore-os/rCore-Tutorial-v3 into ch8
3 years ago
Yu Chen 7f5fa3355f add usr app: green_threads
3 years ago
Yifan Wu cc1e55c958 Bump Rust to nightly-2022-04-11 && support debugging in release mode
3 years ago
Yifan Wu f69f17b40e Merge branch 'ch8' of https://github.com/rcore-os/rCore-Tutorial-v3 into ch8
3 years ago
Yifan Wu c40fb55764 Add test early_exit
3 years ago
Yu Chen 9ad3bb4312 update README
3 years ago
Yu Chen dc6cd26f58 add CI for build-doc
3 years ago

@ -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

@ -6,3 +6,5 @@ docker:
build_docker:
docker build -t ${DOCKER_NAME} .
fmt:
cd easy-fs; cargo fmt; cd ../easy-fs-fuse cargo fmt; cd ../os ; cargo fmt; cd ../user; cargo fmt; cd ..

@ -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

@ -20,4 +20,3 @@ pub const MMIO: &[(usize, usize)] = &[
];
pub type BlockDeviceImpl = crate::drivers::block::SDCardWrapper;

@ -3,4 +3,3 @@ pub const CLOCK_FREQ: usize = 12500000;
pub const MMIO: &[(usize, usize)] = &[(0x10001000, 0x1000)];
pub type BlockDeviceImpl = crate::drivers::block::VirtIOBlock;

@ -11,4 +11,3 @@ pub const TRAMPOLINE: usize = usize::MAX - PAGE_SIZE + 1;
pub const TRAP_CONTEXT_BASE: usize = TRAMPOLINE - PAGE_SIZE;
pub use crate::board::{CLOCK_FREQ, MMIO};

@ -1,13 +1,13 @@
mod sdcard;
mod virtio_blk;
pub use virtio_blk::VirtIOBlock;
pub use sdcard::SDCardWrapper;
pub use virtio_blk::VirtIOBlock;
use crate::board::BlockDeviceImpl;
use alloc::sync::Arc;
use easy_fs::BlockDevice;
use lazy_static::*;
use crate::board::BlockDeviceImpl;
lazy_static! {
pub static ref BLOCK_DEVICE: Arc<dyn BlockDevice> = Arc::new(BlockDeviceImpl::new());

@ -83,7 +83,11 @@ impl From<PhysPageNum> for usize {
}
impl From<VirtAddr> for usize {
fn from(v: VirtAddr) -> Self {
v.0
if v.0 >= (1 << (VA_WIDTH_SV39 - 1)) {
v.0 | (!((1 << VA_WIDTH_SV39) - 1))
} else {
v.0
}
}
}
impl From<VirtPageNum> for usize {

@ -351,26 +351,20 @@ pub fn remap_test() {
let mid_text: VirtAddr = ((stext as usize + etext as usize) / 2).into();
let mid_rodata: VirtAddr = ((srodata as usize + erodata as usize) / 2).into();
let mid_data: VirtAddr = ((sdata as usize + edata as usize) / 2).into();
assert!(
!kernel_space
.page_table
.translate(mid_text.floor())
.unwrap()
.writable(),
);
assert!(
!kernel_space
.page_table
.translate(mid_rodata.floor())
.unwrap()
.writable(),
);
assert!(
!kernel_space
.page_table
.translate(mid_data.floor())
.unwrap()
.executable(),
);
assert!(!kernel_space
.page_table
.translate(mid_text.floor())
.unwrap()
.writable(),);
assert!(!kernel_space
.page_table
.translate(mid_rodata.floor())
.unwrap()
.writable(),);
assert!(!kernel_space
.page_table
.translate(mid_data.floor())
.unwrap()
.executable(),);
println!("remap_test passed!");
}

@ -21,6 +21,14 @@ impl TaskManager {
pub fn fetch(&mut self) -> Option<Arc<TaskControlBlock>> {
self.ready_queue.pop_front()
}
pub fn remove(&mut self, task: Arc<TaskControlBlock>) {
if let Some((id, _)) = self.ready_queue
.iter()
.enumerate()
.find(|(_, t)| Arc::as_ptr(t) == Arc::as_ptr(&task)) {
self.ready_queue.remove(id);
}
}
}
lazy_static! {
@ -34,6 +42,10 @@ pub fn add_task(task: Arc<TaskControlBlock>) {
TASK_MANAGER.exclusive_access().add(task);
}
pub fn remove_task(task: Arc<TaskControlBlock>) {
TASK_MANAGER.exclusive_access().remove(task);
}
pub fn fetch_task() -> Option<Arc<TaskControlBlock>> {
TASK_MANAGER.exclusive_access().fetch()
}

@ -8,16 +8,18 @@ mod switch;
#[allow(clippy::module_inception)]
mod task;
use self::id::TaskUserRes;
use crate::fs::{open_file, OpenFlags};
use alloc::sync::Arc;
use alloc::{sync::Arc, vec::Vec};
use lazy_static::*;
use manager::fetch_task;
use process::ProcessControlBlock;
use switch::__switch;
use crate::timer::remove_timer;
pub use context::TaskContext;
pub use id::{kstack_alloc, pid_alloc, KernelStack, PidHandle};
pub use manager::{add_task, pid2process, remove_from_pid2process};
pub use manager::{add_task, remove_task, pid2process, remove_from_pid2process};
pub use processor::{
current_kstack_top, current_process, current_task, current_trap_cx, current_trap_cx_user_va,
current_user_token, run_tasks, schedule, take_current_task,
@ -86,17 +88,35 @@ pub fn exit_current_and_run_next(exit_code: i32) {
// deallocate user res (including tid/trap_cx/ustack) of all threads
// it has to be done before we dealloc the whole memory_set
// otherwise they will be deallocated twice
let mut recycle_res = Vec::<TaskUserRes>::new();
for task in process_inner.tasks.iter().filter(|t| t.is_some()) {
let task = task.as_ref().unwrap();
// if other tasks are Ready in TaskManager or waiting for a timer to be
// expired, we should remove them.
//
// Mention that we do not need to consider Mutex/Semaphore since they
// are limited in a single process. Therefore, the blocked tasks are
// removed when the PCB is deallocated.
remove_inactive_task(Arc::clone(&task));
let mut task_inner = task.inner_exclusive_access();
task_inner.res = None;
if let Some(res) = task_inner.res.take() {
recycle_res.push(res);
}
}
// dealloc_tid and dealloc_user_res require access to PCB inner, so we
// need to collect those user res first, then release process_inner
// for now to avoid deadlock/double borrow problem.
drop(process_inner);
recycle_res.clear();
let mut process_inner = process.inner_exclusive_access();
process_inner.children.clear();
// deallocate other data in user space i.e. program code/data section
process_inner.memory_set.recycle_data_pages();
// drop file descriptors
process_inner.fd_table.clear();
// remove all tasks
process_inner.tasks.clear();
}
drop(process);
// we do not have to save task context
@ -127,3 +147,8 @@ pub fn current_add_signal(signal: SignalFlags) {
let mut process_inner = process.inner_exclusive_access();
process_inner.signals |= signal;
}
pub fn remove_inactive_task(task: Arc<TaskControlBlock>) {
remove_task(Arc::clone(&task));
remove_timer(Arc::clone(&task));
}

@ -59,6 +59,18 @@ pub fn add_timer(expire_ms: usize, task: Arc<TaskControlBlock>) {
timers.push(TimerCondVar { expire_ms, task });
}
pub fn remove_timer(task: Arc<TaskControlBlock>) {
let mut timers = TIMERS.exclusive_access();
let mut temp = BinaryHeap::<TimerCondVar>::new();
for condvar in timers.drain() {
if Arc::as_ptr(&task) != Arc::as_ptr(&condvar.task) {
temp.push(condvar);
}
}
timers.clear();
timers.append(&mut temp);
}
pub fn check_timer() {
let current_ms = get_time_ms();
let mut timers = TIMERS.exclusive_access();

@ -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::{exit, thread_create};
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,24 @@
#![no_std]
#![no_main]
#[macro_use]
extern crate user_lib;
extern crate alloc;
use user_lib::{exit, thread_create, sleep};
pub fn thread_a() -> ! {
println!("into thread_a");
sleep(1000);
// the following message cannot be seen since the main thread has exited before
println!("exit thread_a");
exit(1)
}
#[no_mangle]
pub fn main() -> i32 {
thread_create(thread_a as usize, 0);
sleep(100);
println!("main thread exited.");
exit(0)
}

@ -0,0 +1,349 @@
// 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