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