Skip to content

Commit 48c4ba8

Browse files
committed
Add coroutine examples
1 parent b543a03 commit 48c4ba8

File tree

2 files changed

+478
-0
lines changed

2 files changed

+478
-0
lines changed

user/src/bin/stackful_coroutine.rs

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
// we porting below codes to Rcore Tutorial v3
2+
// https://cfsamson.gitbook.io/green-threads-explained-in-200-lines-of-rust/
3+
// https://github.com/cfsamson/example-greenthreads
4+
#![no_std]
5+
#![no_main]
6+
#![feature(naked_functions)]
7+
#![feature(asm)]
8+
9+
extern crate alloc;
10+
#[macro_use]
11+
extern crate user_lib;
12+
13+
use core::arch::asm;
14+
15+
#[macro_use]
16+
use alloc::vec;
17+
use alloc::vec::Vec;
18+
19+
use user_lib::exit;
20+
21+
// In our simple example we set most constraints here.
22+
const DEFAULT_STACK_SIZE: usize = 4096; //128 got SEGFAULT, 256(1024, 4096) got right results.
23+
const MAX_TASKS: usize = 5;
24+
static mut RUNTIME: usize = 0;
25+
26+
pub struct Runtime {
27+
tasks: Vec<Task>,
28+
current: usize,
29+
}
30+
31+
#[derive(PartialEq, Eq, Debug)]
32+
enum State {
33+
Available,
34+
Running,
35+
Ready,
36+
}
37+
38+
struct Task {
39+
id: usize,
40+
stack: Vec<u8>,
41+
ctx: TaskContext,
42+
state: State,
43+
}
44+
45+
#[derive(Debug, Default)]
46+
#[repr(C)] // not strictly needed but Rust ABI is not guaranteed to be stable
47+
pub struct TaskContext {
48+
// 15 u64
49+
x1: u64, //ra: return addres
50+
x2: u64, //sp
51+
x8: u64, //s0,fp
52+
x9: u64, //s1
53+
x18: u64, //x18-27: s2-11
54+
x19: u64,
55+
x20: u64,
56+
x21: u64,
57+
x22: u64,
58+
x23: u64,
59+
x24: u64,
60+
x25: u64,
61+
x26: u64,
62+
x27: u64,
63+
nx1: u64, //new return addres
64+
}
65+
66+
impl Task {
67+
fn new(id: usize) -> Self {
68+
// We initialize each task here and allocate the stack. This is not neccesary,
69+
// we can allocate memory for it later, but it keeps complexity down and lets us focus on more interesting parts
70+
// to do it here. The important part is that once allocated it MUST NOT move in memory.
71+
Task {
72+
id,
73+
stack: vec![0_u8; DEFAULT_STACK_SIZE],
74+
ctx: TaskContext::default(),
75+
state: State::Available,
76+
}
77+
}
78+
}
79+
80+
impl Runtime {
81+
pub fn new() -> Self {
82+
// This will be our base task, which will be initialized in the `running` state
83+
let base_task = Task {
84+
id: 0,
85+
stack: vec![0_u8; DEFAULT_STACK_SIZE],
86+
ctx: TaskContext::default(),
87+
state: State::Running,
88+
};
89+
90+
// We initialize the rest of our tasks.
91+
let mut tasks = vec![base_task];
92+
let mut available_tasks: Vec<Task> = (1..MAX_TASKS).map(|i| Task::new(i)).collect();
93+
tasks.append(&mut available_tasks);
94+
95+
Runtime { tasks, current: 0 }
96+
}
97+
98+
/// This is cheating a bit, but we need a pointer to our Runtime stored so we can call yield on it even if
99+
/// we don't have a reference to it.
100+
pub fn init(&self) {
101+
unsafe {
102+
let r_ptr: *const Runtime = self;
103+
RUNTIME = r_ptr as usize;
104+
}
105+
}
106+
107+
/// This is where we start running our runtime. If it is our base task, we call yield until
108+
/// it returns false (which means that there are no tasks scheduled) and we are done.
109+
pub fn run(&mut self) {
110+
while self.t_yield() {}
111+
println!("All tasks finished!");
112+
}
113+
114+
/// This is our return function. The only place we use this is in our `guard` function.
115+
/// If the current task is not our base task we set its state to Available. It means
116+
/// we're finished with it. Then we yield which will schedule a new task to be run.
117+
fn t_return(&mut self) {
118+
if self.current != 0 {
119+
self.tasks[self.current].state = State::Available;
120+
self.t_yield();
121+
}
122+
}
123+
124+
/// This is the heart of our runtime. Here we go through all tasks and see if anyone is in the `Ready` state.
125+
/// If no task is `Ready` we're all done. This is an extremely simple scheduler using only a round-robin algorithm.
126+
///
127+
/// If we find a task that's ready to be run we change the state of the current task from `Running` to `Ready`.
128+
/// Then we call switch which will save the current context (the old context) and load the new context
129+
/// into the CPU which then resumes based on the context it was just passed.
130+
///
131+
/// NOITCE: if we comment below `#[inline(never)]`, we can not get the corrent running result
132+
#[inline(never)]
133+
fn t_yield(&mut self) -> bool {
134+
let mut pos = self.current;
135+
while self.tasks[pos].state != State::Ready {
136+
pos += 1;
137+
if pos == self.tasks.len() {
138+
pos = 0;
139+
}
140+
if pos == self.current {
141+
return false;
142+
}
143+
}
144+
145+
if self.tasks[self.current].state != State::Available {
146+
self.tasks[self.current].state = State::Ready;
147+
}
148+
149+
self.tasks[pos].state = State::Running;
150+
let old_pos = self.current;
151+
self.current = pos;
152+
153+
unsafe {
154+
switch(&mut self.tasks[old_pos].ctx, &self.tasks[pos].ctx);
155+
}
156+
157+
// NOTE: this might look strange and it is. Normally we would just mark this as `unreachable!()` but our compiler
158+
// is too smart for it's own good so it optimized our code away on release builds. Curiously this happens on windows
159+
// and not on linux. This is a common problem in tests so Rust has a `black_box` function in the `test` crate that
160+
// will "pretend" to use a value we give it to prevent the compiler from eliminating code. I'll just do this instead,
161+
// this code will never be run anyways and if it did it would always be `true`.
162+
self.tasks.len() > 0
163+
}
164+
165+
/// While `yield` is the logically interesting function I think this the technically most interesting.
166+
///
167+
/// When we spawn a new task we first check if there are any available tasks (tasks in `Parked` state).
168+
/// If we run out of tasks we panic in this scenario but there are several (better) ways to handle that.
169+
/// We keep things simple for now.
170+
///
171+
/// When we find an available task we get the stack length and a pointer to our u8 bytearray.
172+
///
173+
/// The next part we have to use some unsafe functions. First we write an address to our `guard` function
174+
/// that will be called if the function we provide returns. Then we set the address to the function we
175+
/// pass inn.
176+
///
177+
/// Third, we set the value of `sp` which is the stack pointer to the address of our provided function so we start
178+
/// executing that first when we are scheuled to run.
179+
///
180+
/// Lastly we set the state as `Ready` which means we have work to do and is ready to do it.
181+
pub fn spawn(&mut self, f: fn()) {
182+
let available = self
183+
.tasks
184+
.iter_mut()
185+
.find(|t| t.state == State::Available)
186+
.expect("no available task.");
187+
188+
let size = available.stack.len();
189+
unsafe {
190+
let s_ptr = available.stack.as_mut_ptr().offset(size as isize);
191+
192+
// make sure our stack itself is 8 byte aligned - it will always
193+
// offset to a lower memory address. Since we know we're at the "high"
194+
// memory address of our allocated space, we know that offsetting to
195+
// a lower one will be a valid address (given that we actually allocated)
196+
// enough space to actually get an aligned pointer in the first place).
197+
let s_ptr = (s_ptr as usize & !7) as *mut u8;
198+
199+
available.ctx.x1 = guard as u64; //ctx.x1 is old return address
200+
available.ctx.nx1 = f as u64; //ctx.nx2 is new return address
201+
available.ctx.x2 = s_ptr.offset(-32) as u64; //cxt.x2 is sp
202+
}
203+
available.state = State::Ready;
204+
}
205+
}
206+
207+
/// This is our guard function that we place on top of the stack. All this function does is set the
208+
/// state of our current task and then `yield` which will then schedule a new task to be run.
209+
fn guard() {
210+
unsafe {
211+
let rt_ptr = RUNTIME as *mut Runtime;
212+
(*rt_ptr).t_return();
213+
};
214+
}
215+
216+
/// We know that Runtime is alive the length of the program and that we only access from one core
217+
/// (so no datarace). We yield execution of the current task by dereferencing a pointer to our
218+
/// Runtime and then calling `t_yield`
219+
pub fn yield_task() {
220+
unsafe {
221+
let rt_ptr = RUNTIME as *mut Runtime;
222+
(*rt_ptr).t_yield();
223+
};
224+
}
225+
226+
/// So here is our inline Assembly. As you remember from our first example this is just a bit more elaborate where we first
227+
/// read out the values of all the registers we need and then sets all the register values to the register values we
228+
/// saved when we suspended exceution on the "new" task.
229+
///
230+
/// This is essentially all we need to do to save and resume execution.
231+
///
232+
/// Some details about inline assembly.
233+
///
234+
/// The assembly commands in the string literal is called the assemblt template. It is preceeded by
235+
/// zero or up to four segments indicated by ":":
236+
///
237+
/// - First ":" we have our output parameters, this parameters that this function will return.
238+
/// - Second ":" we have the input parameters which is our contexts. We only read from the "new" context
239+
/// but we modify the "old" context saving our registers there (see volatile option below)
240+
/// - Third ":" This our clobber list, this is information to the compiler that these registers can't be used freely
241+
/// - Fourth ":" This is options we can pass inn, Rust has 3: "alignstack", "volatile" and "intel"
242+
///
243+
/// For this to work on windows we need to use "alignstack" where the compiler adds the neccesary padding to
244+
/// make sure our stack is aligned. Since we modify one of our inputs, our assembly has "side effects"
245+
/// therefore we should use the `volatile` option. I **think** this is actually set for us by default
246+
/// when there are no output parameters given (my own assumption after going through the source code)
247+
/// for the `asm` macro, but we should make it explicit anyway.
248+
///
249+
/// One last important part (it will not work without this) is the #[naked] attribute. Basically this lets us have full
250+
/// control over the stack layout since normal functions has a prologue-and epilogue added by the
251+
/// compiler that will cause trouble for us. We avoid this by marking the funtion as "Naked".
252+
/// For this to work on `release` builds we also need to use the `#[inline(never)] attribute or else
253+
/// the compiler decides to inline this function (curiously this currently only happens on Windows).
254+
/// If the function is inlined we get a curious runtime error where it fails when switching back
255+
/// to as saved context and in general our assembly will not work as expected.
256+
///
257+
/// see: https://github.com/rust-lang/rfcs/blob/master/text/1201-naked-fns.md
258+
/// see: https://doc.rust-lang.org/nightly/reference/inline-assembly.html
259+
/// see: https://doc.rust-lang.org/nightly/rust-by-example/unsafe/asm.html
260+
#[naked]
261+
#[no_mangle]
262+
unsafe fn switch(old: *mut TaskContext, new: *const TaskContext) {
263+
// a0: _old, a1: _new
264+
asm!(
265+
"
266+
sd x1, 0x00(a0)
267+
sd x2, 0x08(a0)
268+
sd x8, 0x10(a0)
269+
sd x9, 0x18(a0)
270+
sd x18, 0x20(a0)
271+
sd x19, 0x28(a0)
272+
sd x20, 0x30(a0)
273+
sd x21, 0x38(a0)
274+
sd x22, 0x40(a0)
275+
sd x23, 0x48(a0)
276+
sd x24, 0x50(a0)
277+
sd x25, 0x58(a0)
278+
sd x26, 0x60(a0)
279+
sd x27, 0x68(a0)
280+
sd x1, 0x70(a0)
281+
282+
ld x1, 0x00(a1)
283+
ld x2, 0x08(a1)
284+
ld x8, 0x10(a1)
285+
ld x9, 0x18(a1)
286+
ld x18, 0x20(a1)
287+
ld x19, 0x28(a1)
288+
ld x20, 0x30(a1)
289+
ld x21, 0x38(a1)
290+
ld x22, 0x40(a1)
291+
ld x23, 0x48(a1)
292+
ld x24, 0x50(a1)
293+
ld x25, 0x58(a1)
294+
ld x26, 0x60(a1)
295+
ld x27, 0x68(a1)
296+
ld t0, 0x70(a1)
297+
298+
jr t0
299+
",
300+
options(noreturn)
301+
);
302+
}
303+
304+
#[no_mangle]
305+
pub fn main() {
306+
println!("stackful_coroutine begin...");
307+
println!("TASK 0(Runtime) STARTING");
308+
let mut runtime = Runtime::new();
309+
runtime.init();
310+
runtime.spawn(|| {
311+
println!("TASK 1 STARTING");
312+
let id = 1;
313+
for i in 0..4 {
314+
println!("task: {} counter: {}", id, i);
315+
yield_task();
316+
}
317+
println!("TASK 1 FINISHED");
318+
});
319+
runtime.spawn(|| {
320+
println!("TASK 2 STARTING");
321+
let id = 2;
322+
for i in 0..8 {
323+
println!("task: {} counter: {}", id, i);
324+
yield_task();
325+
}
326+
println!("TASK 2 FINISHED");
327+
});
328+
runtime.spawn(|| {
329+
println!("TASK 3 STARTING");
330+
let id = 3;
331+
for i in 0..12 {
332+
println!("task: {} counter: {}", id, i);
333+
yield_task();
334+
}
335+
println!("TASK 3 FINISHED");
336+
});
337+
runtime.spawn(|| {
338+
println!("TASK 4 STARTING");
339+
let id = 4;
340+
for i in 0..16 {
341+
println!("task: {} counter: {}", id, i);
342+
yield_task();
343+
}
344+
println!("TASK 4 FINISHED");
345+
});
346+
runtime.run();
347+
println!("stackful_coroutine PASSED");
348+
exit(0);
349+
}

0 commit comments

Comments
 (0)