Skip to content

Latest commit

 

History

History
71 lines (53 loc) · 6.95 KB

第三章笔记.md

File metadata and controls

71 lines (53 loc) · 6.95 KB

这一章的主要内容是将应用程序全部加载并分时处理。编程练习相对上一章稍微复杂了一些。

.
├── os
│   └── src
│       ├── config.rs
│       ├── loader.rs
│       ├── main.rs
│       ├── sbi.rs
│       ├── syscall
│       │   ├── fs.rs
│       │   ├── mod.rs
│       │   └── process.rs
│       ├── task
│       │   ├── context.rs
│       │   ├── mod.rs
│       │   ├── switch.rs
│       │   ├── switch.S
│       │   └── task.rs
│       ├── timer.rs
│       └── trap
│           └── mod.rs
└── user
    └── build.py

多道程序

由于还未实现虚拟内存,所以实现多道程序的方法就是把应用程序不紧密地顺序排列在内存中。

首先通过user/build.py脚本逐个修改user/src/linker.ld中的入口地址并编译应用实现了让应用知道自己实际的起始地址,然后在os/src/loader.rs中(替代了上一章batch.rs的加载功能)将应用加载到对应的地址。

任务管理与切换

首先对比本章和上一章在切换任务上的区别:上一章逐个执行任务,每个任务在退出后绝对不会再次运行;本章的任务随时可能会暂停并让出CPU资源,随后再恢复运行。
然后是暂停的两种情况,一种是主动一种是被动,对于应用程序来说主动暂停就是一个在一定时间后返回的函数,函数返回需要的时间是未知的;而被动暂停是无法被应用感知到的。

思考应用程序如果被打断,有哪些需要保存的数据:调用栈和CPU寄存器。因为各个应用代码所在的地址是互不相干的所以这部分不会受到影响。目前系统没有实现堆内存所以也不需要考虑。
对CPU寄存器的保存和恢复上一章已经实现了,上一章中在Trap时系统会先将所有需要保存的寄存器值存到内核栈中,在返回应用前恢复。
调用栈的保存相对简单一些,因为调用栈位于内存,所以只要给每个应用准备一个调用栈并维护好sp指针自然就实现了调用栈的保存。

对于换栈一个比较重要的点就是不仅应用栈会换,内核栈也会换。在目前的实现中每个应用除了有单独的应用栈还有单独的内核栈,无论应用是主动还是被动地暂停执行,在内核中都会调用一个换栈函数并切换自己的栈。有点像是有很多个相同的内核在传递CPU使用权?

现在假设只有两个应用程序并且都运行过,当要从其中一个切换到另一个时首先通过某种方式从应用程序切换到内核,这时应用程序的信息已经在内核栈中保存好了,这时内核调用换栈函数。
换栈函数经过一系列操作返回,内核栈和应用栈没有任何变化,这时内核就像完成了系统调用一样切换回应用程序继续运行。
而实际上换栈函数将内核栈换成了另一个应用程序的内核栈,那边的内核同样像完成了系统调用一样切换回应用程序,等到下一次触发切换操作时内核调用换栈函数,时间就来到了上一行的开头。

那么在所有应用都没有运行过的情况下怎么开始上面的循环呢?只要在初始化时将应用程序的入口地址和寄存器默认值存入各个内核栈中,就变成了所有应用都运行过的状态了。

有了上面的思维准备以后实现起来就清晰很多了,本章中os/src/task模块(替代了上一章batch.rs的应用管理功能)定义了一个全局的TaskManager(同样使用lazy_static)用于管理应用的运行状态和切换,应用被定义为UnInit、Ready、Running、Exited四种状态,在本章中,在TaskManager被初始化时所有应用处于Uninit状态,在第一个应用运行之前所有应用都被初始化为Ready状态,而暂停的应用会被置为Ready状态,单核情况下只有一个应用会处于Running状态,当应用主函数返回或panic即被置为Exited状态。
换栈函数依然是用汇编实现的,相比Trap的汇编内容简单一些但是用到了参数,在rust代码中提供指针参数在汇编中使用,值得学习。 选择应用上,第一个默认运行0号应用,之后的选择由调度器控制。

时钟中断与 gettime

时钟中断是时间片这个概念的硬件基础,内核通过时钟中断让应用被动的让出CPU,增强了对应用程序的管理。
riscv中中断也分了M/S两个特权级,好在rustsbi让我们的内核即使在S模式也拥有相对完整的时钟中断控制能力,在本章的实现中os/src/timer.rs文件利用riscv crate和rustsbi实现了非常短的时钟函数,两个函数用于获取时间,一个函数用于设置一个10ms的时钟中断。
这样每隔10ms当前应用就会被强制暂停由内核选择下一个要运行的程序,让调度器有了更多施展空间。然而在这样的思路下即使下一个还是相同的应用也会执行保存和恢复动作。

有了时钟的功能后不仅可以实现时钟中断还可以向应用提供时间函数。在os/src/syscall/process.rs函数中除了增加了一个sys_yield用于处理应用主动暂停,还有一个sys_get_time用于给出当前的系统时间。一测例中的接口定义为准,gettime函数给出与posix相同的时间格式TimeVal
另一个利用时钟实现的就是对应用最长运行时间的限制,在每次切入应用时保存时间,切出时累加耗时并判断是否超时即可。

调度器

在讲解抢占式调度时使用的时间片轮转算法可以说是最简单的调度器了,单纯地按序号逐个轮换应用,不考虑应用的需要。
而在编程作业中要求实现的stride算法相对来说要好了一些,由应用给出自己的优先级,调度器会让各个应用的运行时间与优先级正相关。但缺点也很明显,一是优先级完全由应用提供,二是优先级使用usize数字定义,当应用数量增长时很难确定应该选择哪个数字。

stride算法简单来说就是三种数,一个是固定的大常数BigStride,一个是各应用的优先级priority,最后一个是初始值为0的各应用的stride。每次分配时间片时分配给stride最小的应用并将stride加上BigStride/priority,直到所有应用执行完毕。

stride算法的实现也很简单,选择stride最小使用优先队列即可,本章因为还没有堆所以我使用了heapless crate的BinaryHeap(由于堆大小是在类型中定义的,所以也就不需要动态内存分配了)。类型定义为stride: BinaryHeap<(usize, usize), Min, MAX_APP_NUM>,因为tuple有默认的比较实现所以直接可用。在分配时间片时直接pop即是需要的应用,在换栈前将增加后的stride push进去即可。除此之外,对于大常数的选取要考虑到溢出的情况,所以不能太大