0%

rCore_Tutorial_CP3

rCore_Tutorial_CP3

完成一个多道分时系统

多道程序放置与加载

由于没有文件系统和内存隔离,这里的许多操作还是以较为原始的方式进行,相较于CP2的批处理系统,多道分时操作系统可以同时将多个任务加载到内存,并在其中进行时间片轮转的操作。然而因为没有文件系统,各个程序仍然是直接以纯二进制数据的形式与操作系统编译到数据段,运行时直接加载二进制指令到系统约定的位置

但即使对于存在文件系统的操作系统而言,多道程序的运行在没有实现虚拟内存的情况下(且程序为位置相关程序),仍然需要预先与操作系统约定程序的加载地址,否则出现多个程序加载地址一致的情况则无法顺利运行。

(还是感觉没有实现内存地址隔离的话每个程序都能把整个操作系统和其他程序都视作自己的内存,然后随意读取和修改?即使对于那个时代实现了其他部分功能的操作系统而言,也会存在这种问题吧?)

任务切换

之前在批处理系统中,只分为内核栈和用户栈两个,而现在多任务系统就需要对每一个应用程序设置一个栈,每个任务的状态单独保存,这里手写了__switch函数实现两个进程(也许这里叫任务更合适?)间的切换
riscv的调用规则是a0/a1等寄存器传入参数,并a0传出返回值,就很容易看懂了。

这个所谓的内核栈和实际上用户态的栈区别还是蛮大的,内核栈实际上只是一个存储运行上下文的数据结构,并不像普通的栈一样还保存不同栈帧间的栈底指针和返回地址之类的东西,内核栈的逻辑完全受我们控制,自行在栈上加加减减进进出出

但是没事少看评论区。。。感觉大伙的基础参差不齐,或者有的人看完了后面的内容在前面的评论区说怪话,导致我有时候被其他人带歪。。。比如不看下一节硬看这个问题会看得人云里雾里,建议看完再说

为什么对比第二章的trap.S文件少了 mv sp, a0

多道程序与协作式调度

计算机的早期大家都很老实,io的时候主动靠yield把自己挂起让出CPU,如果有坏人想写垃圾程序就可以不让出一直跑。。。所以我之前说的无内存隔离恶意程序读写内核和其他程序什么的事情在这个纯真年代恐怕也不会发生。不过IO本身就属于系统调用级别的,只要操作系统直接对IO这个系统调用挂起对应进程不也行?

定义了任务控制块,终于内核要对应用程序使用一个究极数据结构进行管理了,之前的内核栈就变成了每个进程一个,放在任务控制块中,目前控制块只有一个内核栈和一个程序运行状态
derive是超级注解

#[derive(Copy, Clone)]
pub struct TaskControlBlock {
    pub task_status: TaskStatus,
    pub task_cx: TaskContext,
}

现在存在两个环境上下文了,一个taskContext和trapContext,为了使新应用能够顺利启动,在加载时直接设置了taskContext为__restore,并添加restore需要的TrapContext,当程序被switch切换到CPU上时,直接从__restore处启动,并将之前设置好的trap上下文进行恢复

为什么对比第二章的trap.S文件少了 mv sp, a0

(首先需要知道mv sp,a0是把a0的值放入sp。。。。)
这个是__restore开头的指令,这个我在CP2中给出的解释是为了一开始将内核启动栈地址调整到内核运行栈地址上(两个进程切换的时候也应该需要以此重置sp),但在trap处理中这一步是不必要的。由于程序启动的方式有所改变,此处也就不需要这个操作了。
分别看一下两个情况:

  1. 当产生trap时,控制流跳转到__alltraps处理trap,此时sp指向内核栈位置,然后调用trap_handler,虽然trap_handler是一个正常的函数调用会进行正经的压栈退栈等操作(应该是会按照编译器约定把sp压入作为新栈栈底,栈底指针指向栈底,然后生长上去,退栈之后又把sp只回栈底),调用完成后sp还是恢复到了原来的位置,并且这里面一通操作之后又把传进去的a0原封不动的返回了回来,又因为__restore__alltraps在汇编上是放在一起的,所以__alltraps结束后直接执行__restore(我这个地方一开始忘了想了好久。。。),这个时候a0和sp的值是一致的,故无需重复执行
  2. 程序加载运行时(此时即为CP2中的内核栈指针需要通过mv变化时),此时的程序上下文的还原通过__switch完成,switch无论是在启动还是在切换时,都完成了对sp的初始化,指向了当前内核栈栈顶,因此也无需将a0放入sp中

顺便看一下程序挂起的流程。主动或被动被系统调用打断,进__alltraps保存当前用户栈信息到内核栈上,进trap_handlertrap_hander中根据系统调用号进行处理,进suspend_current_and_run_next,最后进全局任务管理器的run_next_task,进__switch把当前的内核处理上下文再存进taskContext,切换到另一个任务。也正是因为所有程序在挂起时是一个trap操作,在重新得到CPU使用权时,会从上次的trap操作中返回,恢复到上次switch保存的内核上下文,完成trap处理后回到__restore,最终继续运行

分时多任务系统和抢占式调度

合理的时间片轮转环节,使用经典调度算法Round-Robin。操作系统课上好像讲了一堆调度算法,RR是不是对IO密集型不友好,拿到CPU两秒又交出去了?
直接通过时钟对CPU发起硬件中断,可以强行打断当前程序的执行,使得CPU进行时间片轮转调度程序,防止程序一直占用CPU而不放出

嵌套中断与嵌套Trap

嵌套中断可以分为两部分:在处理一个中断的过程中又被同特权级/高特权级中断所打断。默认情况下硬件会避免同特权级再次发生,但高特权级中断则是不可避免的会再次发生。

嵌套 Trap 则是指处理一个 Trap(可能是中断或异常)的过程中又再次发生 Trap ,嵌套中断是嵌套 Trap 的一个特例。在内核开发时我们需要仔细权衡哪些嵌套 Trap 应当被允许存在,哪些嵌套 Trap 又应该被禁止,这会关系到内核的执行模型。

这个就有点超出认知范围,要是出现嵌套中断后续中断被忽略可能会导致操作系统出问题吧。。。这时要对各种中断进行考虑就麻烦了,难以想象呜呜,这里似乎是直接设置寄存器忽略了所有的S级中断,只对时钟中断进行处理,防止嵌套中断的产生(毕竟就内核现在的状态而言无法处理内核态下的中断,也许可以通过内核再设立一个独立的内核用上下文,进而对内核态下的嵌套中断进行处理?)。

对于分时抢占调度,每次时钟中断时设置时间片,其实有一点点小小的bug,目前时间片以10ms为单位,恶意程序可以通过运行了9ms后主动yield释放,使得下一个程序仅能运行1ms便被中断退出,加快CPU回到自己的速度。修复方案就是在进程切换的时候设定时间片,而不是时钟中断时

这章好像略微的容易一点点

练习

编程

  1. 切换过程有手就行,在run_next_task里面print一下即可
  2. 完成时间是指所花费的时间?有一点点麻烦
  3. 浮点应用是什么,看不懂
  4. 因为是粗略估计,直接在run_next_task里面操作一下即可
    1. 超人,做不出来,思路就是内核单独还要做一个自己的栈,以此处理嵌套中断

2,4单独拿出来说一下

2

要记录用户态运行时间的话,估计在trap_handler开始处记一个时,结束时记一个时,用上次内核态出去的时间减这次进内核态的时间可以得到在用户态运行的时间。然而由于在trap_handler中可能被调度出去,在这个函数中花费的时间不能被记做内核态中花费的时间。可以再把run_next_task的switch调用前后打两个桩,再用进trap_handler的时间减switch前的时间,和switch后的时间减trap_handler末尾的时间
需要往内核对应进程上下文里面加一堆变量

4

直接在run_next_task上定义局部变量是无法完成这个任务的,因为在执行了switch前后的函数上下文已经发生了改变,switch后的栈上的start time应当是当前APP被swap out时记录下的时间,该时间进行减法得到的是APP被换出后下一次得到CPU的时间。为了记录换入换出的时间,必须在内核层面进行记录,在APP manager里面添加一个start time即可。被换入的APP必定已被换出,因此也无需对使用run_next_task启动的新APP进行额外处理。

可以把sleep中的yield注释掉,让CPU进入忙等待,这样子就能看到时间片确实是以10ms为单位进行轮转,上下文切换的开销约为200us,占时间片的2%,不算太大

魔改后的代码,APP manager里面加了个switch_start_time

    fn run_next_task(&self) {
        if let Some(next) = self.find_next_task() {
            
            let start = crate::timer::get_time_us();
            let mut inner = self.inner.exclusive_access();
            inner.switch_start_time = start;
            let current = inner.current_task;
            info!("Switch app{} to app{}", current, next);
            inner.tasks[next].task_status = TaskStatus::Running;
            inner.current_task = next;
            let current_task_cx_ptr = &mut inner.tasks[current].task_cx as *mut TaskContext;
            let next_task_cx_ptr = &inner.tasks[next].task_cx as *const TaskContext;
            drop(inner);
            // before this, we should drop local variables that must be dropped manually
            unsafe {
                __switch(current_task_cx_ptr, next_task_cx_ptr);
            }
            let inner = self.inner.exclusive_access();
            let kenerl_start_time = inner.switch_start_time;
            let end = crate::timer::get_time_us();
            info!("switch start at: {}, end at: {}", kenerl_start_time, end);
            info!("switch context take {}us", end-kenerl_start_time);
            info!("app{} switch out at {}, switch back at {}", current, start, end);
            info!("after {}us, app{} get cpu again", end-start, current);
        drop(inner);
            // go back to user mode
        }

实验练习

系统调用次数数组在trap_handler里面系统调用那个分支实现,计时功能写粗糙一点算了。。。启动的时候初始化一下,表示启动到现在的时间,把挂起的时间也算进去得了
状态直接从那个task manager里面拉就行。比较大的问题就是我对rust的借用一无所知。。。不知道这么写出来的破烂当参数返回值传来传去会不会出问题

总结

比较有意思的就是这个__switch的透明性和内核中执行__switch前后导致上下文变化的情况了,在练习第四题中能够较好的表现出来,分时系统通过硬件中断打断强行进入内核以进行调度,内核也对应的将单独的内核栈变成了对应每个进程的一个内核栈。
实际上在现代操作系统中这种概念应该进一步的强化为程序控制块,对应每个进程存储关键控制信息。该操作系统对于内核和应用的空间仍然没有隔离,若存在恶意软件则可直接读写内核数据,破坏其他程序执行

下一章是地址空间,是不是可以做虚拟内存空间隔离了