pa4 多道程序和nemu运行RT-thread

首先看一下讲义里提到的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按需处理即可。

posted @ 2024-11-16 23:18  namezhyp  阅读(30)  评论(0编辑  收藏  举报