找回密码
 立即注册
首页 业界区 业界 一生一芯学习:多道程序 yield-os.c

一生一芯学习:多道程序 yield-os.c

岳娅纯 3 小时前
随着处理器主频的越来越高,每次读写一次磁盘要耗费很多个时钟周期来等待磁盘操作的完成,与其傻傻等待,在这等待的过程中我们可以做更多有意义的事情,如当第一个程序需要等待输入输出的时候,切换到第二个程序来运行,第二个程序也等待输入输出的时候就可以切换到第三个程序,以此类推。
这就是多道程序的思想,要实现一个多道程序操作系统, 我们只需要实现以下两点就可以了:

  • 在内存中可以同时存在多个进程
  • 在满足某些条件的情况下, 可以让执行流在这些进程之间切换
什么是进程?  进程 = 程序 + 执行
进程是执行中的程序,除了可执行代码外还包含进程的活动信息和数据,比如用来存放函数变量、局部变量、返回值的用户栈,存放进程相关数据的数据段,内核中进程间切换的内核栈,动态分配的堆。
上下文切换
在yield-os.c中构建了两个执行流,不断交替输出A和B,基本原理就是进程A运行的时候触发了系统调用,通过自陷指令陷入到内核中,根据__am_asm_trap(),A的上下文结构(Context)将会被保存在A的栈上。系统调用完后通过__am_asm_trap()恢复A的上下文,如果此时不恢复A的上下文,而是恢复B的上下文,那么执行完__am_asm_trap()
来看下yield-os.c执行流是如何进行进程切换的。首先贴出它的代码。
这个PCB是union类型的,而不是struct类型的,原因如下:定义数据的时候把PCB的stack栈空间和cp                                                                                                                                               记录上下文指针的元数据存放在同一块内存上。即pcb.stack占满整个PCB内存,然后PCB.CP放在内存的栈底。这样在上下文恢复时用 cp 指向的地址就能直接恢复栈上保存的 Context。
  1. #define STACK_SIZE (4096 * 8)
  2. typedef union {
  3.   uint8_t stack[STACK_SIZE];
  4.   struct { Context *cp; };  //(context pointer)来记录上下文结构的位置
  5. } PCB;
  6. int main() {
  7.   cte_init(schedule);
  8.   pcb[0].cp = kcontext((Area) { pcb[0].stack, &pcb[0] + 1 }, f, (void *)1L);
  9.   pcb[1].cp = kcontext((Area) { pcb[1].stack, &pcb[1] + 1 }, f, (void *)2L);
  10.   yield();
  11.   panic("Should not reach here!");
  12. }
复制代码
第一件事先初始化一下CTE
cte_init的作用是定义了待会跳转去异常处理的地址传给mtvec,然后注册回调函数shedule`
  1. bool cte_init(Context*(*handler)(Event, Context*)) {
  2. // initialize exception entry
  3. asm volatile("csrw mtvec, %0" : : "r"(__am_asm_trap));  //把amasmtrap的地址传给mtvec
  4. user_handler = handler;
  5. return true;
  6. }
复制代码
这个
  1. static Context *schedule(Event ev, Context *prev) {
  2.   current->cp = prev;
  3.   current = (current == &pcb[0] ? &pcb[1] : &pcb[0]);
  4.   return current->cp;
  5. }
复制代码
然后把执行完cte_init(schedule)之后到了
  1.   pcb[0].cp = kcontext((Area) { pcb[0].stack, &pcb[0] + 1 }, f, (void *)1L);
  2.   pcb[1].cp = kcontext((Area) { pcb[1].stack, &pcb[1] + 1 }, f, (void *)2L);
复制代码
先来看下kcontext()的代码。第一个参数{ pcb[0].stack, &pcb[0] + 1 }就是栈空间,随后将函数名当成指针,函数f 会自动“退化”为指向该函数的指针。于是此时entry就是f了。如果指针后面赋值为mepc=(uintptr_t)entry,那么就会自动执行函数f,带上参数1。
下一行同理
  1. Context *kcontext(Area kstack, void (*entry)(void *), void *arg) {
  2.   Context *cp = (Context *)(kstack.end - sizeof(Context));
  3.   cp->mepc = (uintptr_t)entry;
  4.   cp->mstatus = 0x1800;
  5.   cp->gpr[10] = (uintptr_t)arg;   //a0传参
  6.   return cp;
  7. }
复制代码
随后陷入yield()
  1. void yield() {
  2. #ifdef __riscv_e
  3.   asm volatile("li a5, -1; ecall");
  4. #else
  5.   asm volatile("li a7, -1; ecall");
  6.   
  7. #endif
  8. }
复制代码
于是进行ecall指令
  1.   INSTPAT("0000000 00000 00000 000 00000 11100 11", ecall  , I, s->dnpc = isa_raise_intr(11,s->pc);etrace());
复制代码
然后调用isa_raise_intr(11,s->pc)函数。
  1. word_t isa_raise_intr(word_t NO, vaddr_t epc) {
  2.   /* TODO: Trigger an interrupt/exception with ``NO''. 待办事项:使用“NO”触发中断/异常。
  3.    * Then return the address of the interrupt/exception vector. 然后返回中断/异常向量的地址
  4.    */
  5.   cpu.mstatus = 0x00001800;
  6.   cpu.mepc = epc;
  7.   cpu.mcause = NO;
  8.   return cpu.mtvec;
  9. }
复制代码
此时PC会跳转到之前定义的mtvec中,也就是cte_init中的__am_asm_trap函数。
  1. __am_asm_trap:
  2.   addi sp, sp, -CONTEXT_SIZE
  3.   MAP(REGS, PUSH)
  4.   csrr t0, mcause
  5.   csrr t1, mstatus
  6.   csrr t2, mepc
  7.   STORE t0, OFFSET_CAUSE(sp)
  8.   STORE t1, OFFSET_STATUS(sp)
  9.   STORE t2, OFFSET_EPC(sp)
  10.   # set mstatus.MPRV to pass difftest
  11.   li a0, (1 << 17)
  12.   or t1, t1, a0
  13.   csrw mstatus, t1
  14.   mv a0, sp
  15.   call __am_irq_handle
  16.   mv sp, a0
  17.   LOAD t1, OFFSET_STATUS(sp)
  18.   LOAD t2, OFFSET_EPC(sp)
  19.   csrw mstatus, t1
  20.   csrw mepc, t2
  21.   MAP(REGS, POP)
  22.   addi sp, sp, CONTEXT_SIZE
  23.   mret
复制代码
目前识别出是yield之后然后调用之前注册的回调函数。也就是shedule
  1. static Context *schedule(Event ev, Context *prev) {
  2.   current->cp = prev;
  3.   current = (current == &pcb[0] ? &pcb[1] : &pcb[0]);
  4.   return current->cp;
  5. }
复制代码
可以看到cte_init()在trace中是这么传递参数的。
1.png

意思就是根据riscv地abi切换a0的值,也就是切换线程,随后
  1. static Context *schedule(Event ev, Context *prev) {
  2.   current->cp = prev;
  3.   current = (current == &pcb[0] ? &pcb[1] : &pcb[0]);
  4.   return current->cp;
  5. }
复制代码
恢复现场,切换为B线程,也就是所有寄存器,什么通用寄存器堆,mepc,mcause, mstatus, mepc都一模一样。
然后调用mret,pc变成cpu.mepc,于是跳到刚刚kcontext定义的entry中,也就是f函数里面,然后判断参数是多少进行对应的输出之后又陷入到yield,一直循环。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册