[rCore学习笔记 021]多道程序与分时任务
写在前面
本随笔是非常菜的菜鸡写的。如有问题请及时提出。
可以联系:1160712160@qq.com
GitHhub:https://github.com/WindDevil (目前啥也没有
导读
这里就是第三章的开头了,由于我的巨菜,导致天天半天理解不了关键点所在,唉,实在是太折磨人.
遵照上一章开头的时候的优良传统,我个人感觉每次手册给出feature都要想一想上一章对于这个功能我们是怎么实现的,现在的功能是有什么好处.这样可以加深印象.
提高系统的性能和效率是操作系统的核心目标之一,本章展现了操作系统在性能上的一系列功能改进:
- 通过提前加载应用程序到内存,减少应用程序切换开销
- 通过协作机制支持程序主动放弃处理器,提高系统执行效率
- 通过抢占机制支持程序被动放弃处理器,保证不同程序对处理器资源使用的公平性,也进一步提高了应用对 I/O 事件的响应效率
同样地,一定要去阅读官方文档,可能我关注的点并不够全面.
这里进行自己的头脑风暴,思考实现方式:
- 提前加载应用程序到内存的方法:
- 使用更多的内存,把所有的应用程序一股脑地加载进去,然后把在内存中的地址返回
- 如果我不是把所有的应用程序全部都加载在内存中,那么我们是怎么知道等一下要运行哪个应用程序的呢?
- 实现协作机制和抢占机制的方法,感觉它写的是应用程序主动放弃处理器,但是实际上做实现的时候我又想利用类似于中断的方式解决问题,就是保存上下文然后跳转那一套东西,至于如何计算时间,就很难想到其内部实现,我感觉这时候一定要使用到定时器了.
总而言之,脑子里还是之前使用RT-Thread和FreeRTOS的时候的幼稚的.APP1->delay->抢占->APP2->APP1这种想法.
这时候就需要更深入的了解.
可以在这里了解到,在 协作式 模式下,需要程序员主动在程序中写一个 主动放弃处理器 的调用请求.
而对于 抢占式 , 则是使用 中断 直接打断程序运行,从而保证一个应用程序在执行完 一段时间 以后一定会让出处理器,这里截取一段官方文档的描述:
操作系统可进一步利用某种以固定时长为时间间隔的外设中断(比如时钟中断)来强制打断一个程序的执行,这样一个程序只能运行一段时间(可以简称为一个时间片, Time Slice)就一定会让出处理器,且操作系统可以在处理外设的 I/O 响应后,让不同应用程序分时占用处理器执行,并可通过统计程序占用处理器的总执行时间,来评估运行的程序对处理器资源的消耗。我们把这种运行方式称为 分时共享(Time Sharing) 或 抢占式多任务(Multitasking) ,也可合并在一起称为 分时多任务 。
这里就从脑海的深处想起来关于 裸机编程 的东西,就是使用 定时器 中断来实现分时复用,那么和使用 抢占式 的分时复用有什么区别呢?
- 定时器的数量有限
- 使用软定时器解决问题对于固定行为固定时长的程序有效,但是对于含有各种复杂分支的程序就比较难办
这里还有一个问题,就是对于 波形采样 这样 时间敏感 型的任务,可以放心把它交给OS吗?还是说需要结合 定时器中断 和 OS 调度.那么怎么结合呢?
最后截取官方文档对这章实现的定位和分类:
本章所介绍的多道程序和分时多任务系统都有一些共同的特点:在内存中同一时间可以驻留多个应用,而且所有的应用都是在系统启动的时候分别加载到内存的不同区域中。由于目前计算机系统中只有一个处理器核,所以同一时间最多只有一个应用在执行(即处于运行状态),剩下的应用处于就绪状态或等待状态,需要内核将处理器分配给它们才能开始执行。一旦应用开始执行,它就处于运行状态了。
本章主要是设计和实现建立支持 多道程序 的二叠纪“锯齿螈” 1 初级操作系统、支持 多道程序 的三叠纪“始初龙” 2 协作式操作系统和支持 分时多任务 的三叠纪“腔骨龙” 3 抢占式操作系统,从而对可支持运行一批应用程序的多种执行环境有一个全面和深入的理解,并可归纳抽象出 任务 、 任务切换 等操作系统的概念。
实践体验
同样地,我们可以很方便地利用rCore-Tutorial-v3
,体验这两种操作系统的实现.
首先我们需要切换到文件夹下cd ~/App/rCore-Tutorial-v3
.
体验多道程序操作系统
切换到多道操作系统并且运行:
git checkout ch3-coop
cd os
make run
这里注意,如果你修改了分支的内容导致不能切换分支,你可以使用git checkout -- .
来丢弃所有的更改,这样就可以切换到新的分支.
运行结果:
[rustsbi] RustSBI version 0.3.1, adapting to RISC-V SBI v1.0.0
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ < | |
| |\ \----.| `--' |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
[rustsbi] Implementation : RustSBI-QEMU Version 0.2.0-alpha.2
[rustsbi] Platform Name : riscv-virtio,qemu
[rustsbi] Platform SMP : 1
[rustsbi] Platform Memory : 0x80000000..0x88000000
[rustsbi] Boot HART : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000f02
[rustsbi] Firmware Address : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
[rustsbi] pmp04: 0x88000000..0x00000000 (-wr)
[kernel] Hello, world!
AAAAAAAAAA [1/5]
BBBBBBBBBB [1/2]
CCCCCCCCCC [1/3]
AAAAAAAAAA [2/5]
BBBBBBBBBB [2/2]
CCCCCCCCCC [2/3]
AAAAAAAAAA [3/5]
Test write_b OK!
[kernel] Application exited with code 0
CCCCCCCCCC [3/3]
AAAAAAAAAA [4/5]
Test write_c OK!
[kernel] Application exited with code 0
AAAAAAAAAA [5/5]
Test write_a OK!
[kernel] Application exited with code 0
All applications completed!
这个运行结果是这样的 [ 1/5 ]的意思是执行到了第五次执行中的第一次,这样就很容易理解这个程序的结果了,也就是轮流在执行都已经被加载到内存中的分别能输出一串A
,B
,C
的三个程序.
然后执行到限制的次数为止.
体验分时多任务操作系统
cd ~/App/rCore-Tutorial-v3
,再次回到根文件下,切换到分时多任务操作系统并运行:
git checkout ch3
cd os
make run
运行结果:
[rustsbi] RustSBI version 0.3.1, adapting to RISC-V SBI v1.0.0
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ < | |
| |\ \----.| `--' |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
[rustsbi] Implementation : RustSBI-QEMU Version 0.2.0-alpha.2
[rustsbi] Platform Name : riscv-virtio,qemu
[rustsbi] Platform SMP : 1
[rustsbi] Platform Memory : 0x80000000..0x88000000
[rustsbi] Boot HART : 0
[rustsbi] Device Tree Region : 0x87000000..0x87000f02
[rustsbi] Firmware Address : 0x80000000
[rustsbi] Supervisor Address : 0x80200000
[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
[rustsbi] pmp02: 0x80000000..0x80200000 (---)
[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
[rustsbi] pmp04: 0x88000000..0x00000000 (-wr)
[kernel] Hello, world!
power_3 [10000/200000]
power_3 [20000/200000]
power_3 [30000/200000]
power_3 [40000/200000]
power_3 [50000/200000]
power_3 [60000/200000]
power_3 [70000/200000]
power_3 [80000/200000]
power_3 [90000/200000]
power_3 [100000/200000]
power_3 [110000/200000]
power_3 [120000/200000]
power_3 [130000/200000]
power_3 [140000/200000]
power_3 [150000/200000]
power_3 [160000/200000]
power_3 [170000/200000]
power_3 [180000/200000]
power_3 [190000/200000]
power_3 [200000/200000]
3^200000 = 871008973(MOD 998244353)
Test power_3 OK!
[kernel] Application exited with code 0
power_5 [10000/140000]
power_5 [20000/140000]
power_5 [30000/140000]
power_5 [40000/140000]
power_5 [50000/140000]
power_5 [60000/140000]
power_5 [70000/140000]
power_5 [80000/140000]
power_5 [90000/140000]
power_5 [100000/140000]
power_5 [110000/140000]
power_5 [120000/140000]
power_7 [10000/160000]
power_7 [20000/160000]
power_7 [30000/160000]
power_7 [40000/160000]
power_7 [50000/160000]
power_7 [60000/160000]
power_7 [70000/160000]
power_7 [80000/160000]
power_7 [90000/160000]
power_7 [100000/160000]
power_7 [110000/160000]
power_7 [120000/160000]
power_7 [130000/160000]
power_7 [140000/160000]
power_7 [150000/160000]
power_7 [160000/160000]
7^160000 = 667897727(MOD 998244353)
Test power_7 OK!
[kernel] Application exited with code 0
power_5 [130000/140000]
power_5 [140000/140000]
5^140000 = 386471875(MOD 998244353)
Test power_5 OK!
[kernel] Application exited with code 0
Test sleep OK!
[kernel] Application exited with code 0
All applications completed!
官方文档对它的解析为:
分时多任务系统应用分为两种。编号为 00/01/02 的应用分别会计算质数 3/5/7 的幂次对一个大质数取模的余数,并会将结果阶段性输出。编号为 03 的应用则会等待三秒钟之后再退出。以 k210 平台为例,我们将会看到 00/01/02 三个应用分段完成它们的计算任务,而应用 03 由于等待时间过长总是最后一个结束执行。
代码结构
这是本章 多道程序操作系统 的代码结构:
这是上一章的代码结构,可以看到本章的 多道任务 是多了几个APP的框图在U-Mode
:
这是更进一步的 协作式分时操作系统 的框图:
首先先说我自己看到这张图的感受,
- 在原本的多道系统的基础上为每个APP都增加了一个保存上下文的栈.
- 把AppManager拆分成Loader和TaskManager.
- 增加了任务状态和任务上下文的概念,应该是用于分时复用,但是不知道具体怎么实现的.
- 为CPU增加了一条时间线,应该是用于分时复用,但是不知道具体怎么实现的.
通过查看官方文档,我们可以另外补充到一些我们不知道的东西:
如果当前应用程序正在运行,则该应用对应的任务处于运行(Running)状态;如果该应用主动放弃处理器,则该应用对应的任务处于就绪(Ready)状态。操作系统进行任务切换时,需要把要暂停任务的上下文(即任务用到的通用寄存器)保存起来,把要继续执行的任务的上下文恢复为暂停前的内容,这样就能让不同的应用协同使用处理器了。
这是更进一步的 抢占式多任务操作系统 的框图:
同样地,先观察这个图讲出自己的感受:
- 初看和 协作式 没有什么不同之处
- 细看发现在
Trap_handler
,里边增加了Timer
的调度,可能和我想得一样是用定时器来触发中断然后实现时间片的轮转.
实际上官方文档的描述也大致如此.
本章代码导读
这一部分的作者写得太好了,我直接无话可说.直接点击观看吧.
本章的重点是实现对应用之间的协作式和抢占式任务切换的操作系统支持。与上一章的操作系统实现相比,有如下一些不同的情况导致实现上也有差异:
- 多个应用同时放在内存中,所以他们的起始地址是不同的,且地址范围不能重叠
- 应用在整个执行过程中会暂停或被抢占,即会有主动或被动的任务切换
这些实现上差异主要集中在对应用程序执行过程的管理、支持应用程序暂停的系统调用和主动切换应用程序所需的时钟中断机制的管理。
对于第一个不同情况,需要对应用程序的地址空间布局进行调整,每个应用的地址空间都不相同,且不能重叠。这并不要修改应用程序本身,而是通过一个脚本 build.py
来针对每个应用程序修改链接脚本 linker.ld
中的 BASE_ADDRESS
,让编译器在编译不同应用时用到的 BASE_ADDRESS
都不同,且有足够大的地址间隔。这样就可以让每个应用所在的内存空间是不同的。
对于第二个不同情况,需要实现任务切换,这就需要在上一章的 Trap 上下文切换的基础上,再加上一个 Task 上下文切换,才能完成完整的任务切换。这里面的关键数据结构是表示应用执行上下文的 TaskContext
数据结构和具体完成上下文切换的汇编语言编写的 __switch
函数。一个应用的执行需要被操作系统管理起来,这是通过 TaskControlBlock
数据结构来表示应用执行上下文的动态执行过程和状态(运行态、就绪态等)。而为了做好应用程序第一次执行的前期初始化准备, TaskManager
数据结构的全局变量实例 TASK_MANAGER
描述了应用程序初始化所需的数据, 而对 TASK_MANAGER
的初始化赋值过程是实现这个准备的关键步骤。
应用程序可以在用户态执行中主动暂停,这需要有新的系统调用 sys_yield
的实现来支持;为了支持抢占应用执行的抢占式切换,还要添加对时钟中断的处理。有了时钟中断,就可以在确定时间间隔内打断应用的执行,并主动切换到另外一个应用,这部分主要是通过对 trap_handler
函数中进行扩展,来完成在时钟中断产生时可能进行的任务切换。 TaskManager
数据结构的成员函数 run_next_task
来具体实现基于任务控制块的任务切换,并会具体调用 __switch
函数完成硬件相关部分的任务上下文切换。