• rCore-Tutorial-Book-v3学习笔记(三)


    概述

    第三个部分是实现一个分时多任务的系统,也就是能够在多个任务运行期间进行切换,让一个程序在等待IO时其他程序能执行而不是傻等。虽然和并行有点类似,但目前使用到的都只有一个CPU,任何时候都只有一个程序在执行。分时多任务有两种:一种是程序自己主动让出控制权,有点类似于编程语言中协程的概念;一种是由操作系统控制每个程序只能运行一个时间片,到时间了自动切换控制权,有点类似于Python中带了GIL锁的线程。由于第二种是在第一种的基础上建立的,所以这里主要展现的第二种。

    内容

    和上次相比:

    • hello_world.c修改成了hello_world0.chello_world1.chello_world2.c三个文件,以hello_world0.c为例:

      #include "user.h"
      
      int main() {
          char *s = "Program[0] print0
      ";
          isize w;
          for (int i = 0; i < 10; i++) {
              s[16] = (i % 10) + '0';
              write(FD_STDOUT, s);
              w = get_time() + 10;
              while (get_time() < w); // yield();
          }
          return 0;
      }
      

      每个程序都会输出自己的编号,相隔两次循环会阻塞自己一段时间,hello_world1.c阻塞的时间是20ms,hello_world2.c阻塞的时间是30ms。另外,因为程序比较少,所以没有像教程一样写一个用来编译的Python脚本,而是把链接脚本也分成三个,每个的起始地址不一样,makefile中分别用不同的链接脚本编译这三个文件。

    • 把原来batch.c的一部分代码放在loader.c里,负责加载用户程序和初始化;新添加了管理任务的task.c,管理计时的timer.c和控制任务切换的汇编程序switch.S

    注意点不是很多,首先是几个硬编码的部分,如SIE寄存器的初始化,第五位为1开启内核态时钟中断:

        usize sie; asm volatile("csrr %0, sie":"=r"(sie));
        sie |= (1 << 5); asm volatile("csrw sie, %0"::"r"(sie));
    

    读取时间寄存器,有一个专门的指令rdtime:

    usize get_time() {
        usize timer; asm volatile("rdtime %0":"=r"(timer)); return timer;
    }
    

    scause寄存器的第63位(从0开始)为0表示当前异常是陷入,1表示当前异常是中断(用户态的系统调用属于陷入),后面的0-62位表示陷入或者中断的具体原因。对于中断,低位为5表示内核态时钟中断:

        if (scause == (1L << 63) + 5) {
            set_next_trigger();
            suspend_current_and_run_next();
            return cx;
        }
    

    接着,为了更好的理解切换的过程,还是需要研究一下这个过程中sp寄存器的变化。同样,在程序最开始的时候,sp指向bootstack里,注意在本次任务中,每个用户程序各自拥有一个属于自己的用户栈和内核栈,所以初始化的时候init_app_cx就是给每个程序的内核栈压上TrapContext和TaskContext。

    然后进入run_first_task,进入__switch,第二句是sd sp 0(a0),这里有一点挺坑的,就是传给__switch的参数是一个“指针的指针”,就是说,TaskControlBlock里的task_cx_ptr属性存储的是指向TaskContext的指针,或者说是栈顶地址,而传入__switch的参数是指向这个属性的指针。

    那么在上面这句汇编代码中,0(a0)解引用,就是把sp(或者说是前一个任务的栈顶地址)保存在这个属性里了,最后把0(a1)传给sp,就是跳转到另一个任务的栈里,对于第一次调用而言,就是在这里从bootstack跳到了第一个任务的内核栈,切换任务之后,sp指向了新任务的TrapContext,由于ra现在指向__restore,所以程序就会再走一遍__restore的流程,进入用户态。

    另外,由于第一次调用__restore前sp就已经指向内核栈了,所以原来__restore的第一句mv sp, a0就应该去掉了。在上一部分还提到过一个内核栈重置的问题,当时是在运行完一个程序之后,要压入一个自定义的TrapContext,为了节省空间,就直接放到栈底了,而现在由于每个程序一个栈,在一个程序运行结束之后,这个栈就没有用了,同时task_cx_ptr和scratch又分别保存了当前程序的TaskContext和TrapContext地址,修改都是在对应位置,所以没有“重置栈”这一过程。

    最后分析一下输出结果(设定每个程序运行的时间片是10ms,这里IO的时间和时间片相比基本可以忽略不计):

    Program[0] print0
    Program[1] print0
    Program[2] print0
    Program[0] print1
    Program[0] print2
    Program[1] print1
    Program[2] print1
    Program[0] print3
    Program[0] print4
    Program[1] print2
    Program[2] print2
    Program[0] print5
    Program[1] print3
    Program[2] print3
    Program[0] print6
    Program[1] print4
    Program[2] print4
    Program[0] print7
    Program[1] print5
    Program[2] print5
    Program[0] print8
    Program[1] print6
    Program[2] print6
    Program[0] print9
    Application exited with code 0
    Program[2] print7
    Program[1] print7
    Program[1] print8
    Program[2] print8
    Program[1] print9
    Program[2] print9
    Application exited with code 0
    Application exited with code 0
    

    根据输出,画出的前120ms的程序运行情况如图所示:

    绿色表示当前占有CPU的程序,箭头表示输出。可以发现不管各个程序休眠多长时间,都是轮流使用CPU,到点即换人。不过值得注意的是在输出结果中出现了程序0连续输出两行的情况,对应上图第40ms和第70ms的地方。而在第100ms则没有出现这一情况。这种情况是否发生取决于程序0休眠10ms后被唤醒准备进入内核态调用输出函数和10ms的时间片到达发生时钟中断这两个事件谁先谁后。如果前者先,那么程序0进入内核态后时钟中断就不能发生了,必须等输出之后回到用户态才会发生中断,这样结合休眠前的输出,就会连着输出第二次;而如果后者先,那么在输出前控制权就被让出去了,必须等程序2和程序3都输出过后才能轮到程序0输出,这样就不会连着输出两次。再比如,如果程序0在输出之后,获取当前时间之前让出了控制权,程序1和程序2输出后,程序0才获取时间,休眠10ms然后控制权又被程序1和程序2抢走了,那么就会出现程序0一时半会输出不了的情况。所以说即使是全程只有一个程序在运行,在任务切换的时候仍然可能无法预测的竞争问题,这里也是各种程序Bug的重灾区。

    还有一个地方,就是我的输出没有出现教程那样混乱的情况,因为我打印字符串是直接将整个字符串提交到内核态,而一旦进入内核态就不会再收到时钟中断了,所以字符串都是完整输出。

  • 相关阅读:
    字符串方法
    文件上传路径转虚拟路径
    表结构转excel
    @ModelAttribute
    select
    查询详情在模态框展示
    时间
    mybatis一对多
    bootstrap tab页
    为什么不建议使用WordPress呢?
  • 原文地址:https://www.cnblogs.com/YuanZiming/p/14454168.html
Copyright © 2020-2023  润新知