首先看一下讲义里提到的yield os,这个os里面只有两道程序切换的模拟内容,只要做过pa3就很容易理解:
#define STACK_SIZE (4096 * 8) typedef union { uint8_t stack[STACK_SIZE]; struct { Context *cp; }; } PCB; static PCB pcb[2], pcb_boot, *current = &pcb_boot; static void f(void *arg) { while (1) { putch("?AB"[(uintptr_t)arg > 2 ? 0 : (uintptr_t)arg]); for (int volatile i = 0; i < 100000; i++) ; yield(); } } static Context *schedule(Event ev, Context *prev) { current->cp = prev; current = (current == &pcb[0] ? &pcb[1] : &pcb[0]); return current->cp; } int main() { cte_init(schedule); pcb[0].cp = kcontext((Area) { pcb[0].stack, &pcb[0] + 1 }, f, (void *)1L); pcb[1].cp = kcontext((Area) { pcb[1].stack, &pcb[1] + 1 }, f, (void *)2L); yield(); panic("Should not reach here!"); }
首先定义了PCB结构体,里面只包含一个栈和上下文(在这里上下文只考虑通用+特殊寄存器的值)。然后声明长度2数组来放两道程序。 在main函数里,cte init注册回调函数schedule,他的功能就是在保存当前pcb的信息,并切换到另一个pcb。随后创建两个pcb上下文,然后调用yield(),从此开始执行两个程序并不断切换,达到的效果就是不断地输出AB。
在pa3里,我们实现了异常响应,程序触发yield时执行一些其他动作,然后返回到之前的位置,恢复上下文继续执行。而现在,如果我们不返回,而是从b的栈里,把程序B的上下文加载进来,那么就做到了多道程序。
在yield-os里,f()起到了类似内核线程的作用。而kconfig()的功能则是创建上下文。按照讲义+union,如果没有设置cp指针,那就是栈顶指针,反之则是cp,cp又在这个32K的内存区间里指向了上下文。
| | +---------------+ <---- kstack.end 高地址 | | | context | | | +---------------+ <--+ | | | | | | | | | | | | +---------------+ | | cp | ---+ +---------------+ <---- kstack.start 低地址 | |
栈的设计是先入后出。将程序装载入内存时,我们一般把栈放在最大地址的位置。同时,让栈顶和栈底指针都指向栈的底部,也就是最大地址处。随着栈的装入,栈顶指针逐渐向着低地址方向移动。
讲义要求我们实现kcontext函数,这个函数的功能是初始化上下文,那么我们需要做的就是生成一份寄存器并初始化再返回即可。在这里,我们首先需要初始化一份ctx,就用 context *ctx = (context *)(kstack.end - sizeof(context)),然后清零。
在riscv32里,前8个参数通过a0到a7传递,之后还有参数则通过栈传递。返回值通过a0传递。所以参数arg要放入a0。entry要放入mepc(这里我还没完全理解,ecall指令里,我们将原计划的下一条命令放入mepc,这里将entry放入mepc,那不会在和后面触发yield时被覆盖吗)。为了difftest,还需要把mstatus设置0x1800。此外,我们还需要把当前的栈顶指针放入gpr[2]--sp寄存器。而pidr用于分页目录指针,此时我们不需要,设置为NULL。
这样一来,yield-os就可以正常输出AB了。
-- ----- -- ------- ---- ----- - - - -- -------
在rt-thread里。讲义要求实现三个函数stac_init 和两个switch,配套的ev_handle也需要修改。先说ev_handle,这个函数就是回调函数,每次在触发异常响应时,在irq_handle里被调用执行。对应两个switch函数。
stack_init函数的作用是创建一套上下文。此时就不能直接调用CTE的kcontext函数,因为参数不匹配。rt-therad里还多了一个texit函数,tentry是原本要执行的内核线程,而texit是线程执行完后负责清理工作的函数。其实就是tentry执行完再执行texit的意思。 按照讲义,我们可以设置一个包裹函数当作内核线程,把tentry para texit三个参数打包成arg,传给包裹函数。包裹函数收到以后再拆开。
值得一提的是,在这里不需要用内联汇编,会把这里搞的更麻烦,只要把三个参数传给包裹函数,正常调用kcontext就行。但在实际做的时候我也遇到了一个问题:kcontext的首个参数是area结构体,也就是堆栈的首尾指针,他的大小应该设置为多少?stack_addr就是首,那尾呢?我的解决思路就是从PCB结构体里抠出来stack_size,发现是16384(或者是别的值),就设置为了这个值。另:stack_init里不可以调用rt_thread_self,因为PCB还没创建好。
两个switch函数稍微麻烦一些,但讲义里也已经给了提示。可以利用PCB里面的user_data。讲义提示user_data可能本身也是一个有用的值,那我们怎么呢?和stack_init的思路类似,我们声明一个结构体,里面包含from to 和原本的user_data三个变量。
在两个switch里,先声明结构体变量(用static,拓展生命周期,但它还是一个局部变量),用rt_thread_self取出PCB,然后拿出这个user_data,和from to 打包塞进去,然后触发Yield。在ev_handle里,再把user_data取出,此时的user_data存的是结构体指针,此时拆开,就得到了from to 和原本的user_data按需处理即可。