[rCore学习笔记 022]多道程序与分时任务
写在前面
本随笔是非常菜的菜鸡写的。如有问题请及时提出。
可以联系:1160712160@qq.com
GitHhub:https://github.com/WindDevil (目前啥也没有
思考
上一节我们也提到了关于多道程序的放置和加载问题的事情.对比上一章的加载,我们需要把所有的APP全部都加载到内存中.
在这一节的描述中,官方文档提出了:
但我们也会了解到,每个应用程序需要知道自己运行时在内存中的不同位置,这对应用程序的编写带来了一定的麻烦。而且操作系统也要知道每个应用程序运行时的位置,不能 任意移动应用程序所在的内存空间 ,即不能在运行时根据内存空间的动态空闲情况,把应用程序 调整到合适的空闲空间 中。
这里其实我脑子里是非常难受的,就是关于这个 调整到合适的空闲空间中 , 因为上一章的程序也没有这个功能,我感觉是后续的内容可能会涉及到对于 碎片空间 的利用.
多道程序的放置
回想我们上一章的时候让我们惊叹的link_app.S
和对应的build.rs
脚本,我们可以猜想到大概也是要通过build.rs
来修改每个APP的链接地址.
可是build.py
已经忘记了,唉,不知道这个记忆力需要学到啥时候才能学完.
回顾link_app.S
,可以看到,实际上在.data
段保存了所有的APP:
.align 3
.section .data
.global _num_app
_num_app:
.quad 7
.quad app_0_start
.quad app_1_start
.quad app_2_start
.quad app_3_start
.quad app_4_start
.quad app_5_start
.quad app_6_start
.quad app_6_end
.section .data
.global app_0_start
.global app_0_end
app_0_start:
.incbin "../user/target/riscv64gc-unknown-none-elf/release/00hello_world.bin"
app_0_end:
.section .data
.global app_1_start
.global app_1_end
app_1_start:
.incbin "../user/target/riscv64gc-unknown-none-elf/release/01store_fault.bin"
app_1_end:
.section .data
.global app_2_start
.global app_2_end
app_2_start:
.incbin "../user/target/riscv64gc-unknown-none-elf/release/02power.bin"
app_2_end:
.section .data
.global app_3_start
.global app_3_end
app_3_start:
.incbin "../user/target/riscv64gc-unknown-none-elf/release/03priv_inst.bin"
app_3_end:
.section .data
.global app_4_start
.global app_4_end
app_4_start:
.incbin "../user/target/riscv64gc-unknown-none-elf/release/04priv_csr.bin"
app_4_end:
.section .data
.global app_5_start
.global app_5_end
app_5_start:
.incbin "../user/target/riscv64gc-unknown-none-elf/release/test1_write0.bin"
app_5_end:
.section .data
.global app_6_start
.global app_6_end
app_6_start:
.incbin "../user/target/riscv64gc-unknown-none-elf/release/test1_write1.bin"
app_6_end:
这时候脑子里浮现出一个想法,那么这难道不算全部都加载到内存里了吗?
很显然不是,只是链接在了.data
段.
查看user
下的link.ld
,你可以看到所有的APP的起始地址都是0x80400000
:
OUTPUT_ARCH(riscv)
ENTRY(_start)
BASE_ADDRESS = 0x80400000;
SECTIONS
{
. = BASE_ADDRESS;
.text : {
*(.text.entry)
*(.text .text.*)
}
.rodata : {
*(.rodata .rodata.*)
*(.srodata .srodata.*)
}
.data : {
*(.data .data.*)
*(.sdata .sdata.*)
}
.bss : {
start_bss = .;
*(.bss .bss.*)
*(.sbss .sbss.*)
end_bss = .;
}
/DISCARD/ : {
*(.eh_frame)
*(.debug*)
}
}
所以如果想要所有的APP都能够加载在一起,那么需要修改的是user
下的link.ld
.
为什么要这么做,官方文档做出了描述:
之所以要有这么苛刻的条件,是因为目前的操作系统内核的能力还是比较弱的,对应用程序通用性的支持也不够(比如不支持加载应用到内存中的任意地址运行),这也进一步导致了应用程序编程上不够方便和通用(应用需要指定自己运行的内存地址)。事实上,目前应用程序的编址方式是基于绝对位置的,并没做到与位置无关,内核也没有提供相应的地址重定位机制。
因此,通过在user
下写一个build.py
来对每一个APP生成一个链接文件,(所以还是python好用吗):
# user/build.py
import os
base_address = 0x80400000
step = 0x20000
linker = 'src/linker.ld'
app_id = 0
apps = os.listdir('src/bin')
apps.sort()
for app in apps:
app = app[:app.find('.')]
lines = []
lines_before = []
with open(linker, 'r') as f:
for line in f.readlines():
lines_before.append(line)
line = line.replace(hex(base_address), hex(base_address+step*app_id))
lines.append(line)
with open(linker, 'w+') as f:
f.writelines(lines)
os.system('cargo build --bin %s --release' % app)
print('[build.py] application %s start with address %s' %(app, hex(base_address+step*app_id)))
with open(linker, 'w+') as f:
f.writelines(lines_before)
app_id = app_id + 1
这个文件是对link.ld
里的0x80400000
进行修改,每一个步长为0x20000
,修改好了之后就开始使用cargo build --bin
来 单独 构建对应APP.
这时候就体现了我的想当然,上一部分的学习中,我们学到build.rs
会在执行cargo run
之前被调用,这时候我们就盲目地认为build.py
也会被调用.
实际上不是这样的,我们需要在make build
的过程中调用它,因此需要修改user/Makefile
.
增加:
APPS := $(wildcard $(APP_DIR)/*.rs)
...
elf: $(APPS)
@python3 build.py
...
这里会有一些我看不太懂的地方,我们询问通义千问:
- 使用
$(APPS)
是检查这些文件有没有更新 - 使用
@
是指静默运行指令
但是我们会发现当前AI的局限性,他们是懂得,我总感觉还少点什么少点提纲挈领的东西.
于是我们可以查询Makefile教程和示例指南 (foofun.cn).
Makefile语法:
Makefile由一组 rules 组成。 rule通常如下所示:
targets: prerequisites
command
command
command
- targets (目标) 是文件名,用空格分隔。 通常,每个rule只有一个。
- commands (命令) 是通常用于创建目标的一系列步骤。 这些 需要以制表符 开头,不可以是空格。
- prerequisites(先决条件) 也是文件名,用空格分隔。 在运行目标的命令之前,这些文件需要存在。 这些也称为 dependencies(依赖项)
可以看到,这一句基本语法,比我们凭借想象和经验的理解要好上很多倍.这个$(APPS)
我们把它归类为prerequisites
,自然就可以理解makefile在工作时会尝试检查文件的存在.
同样我们可以知道使用$()
是引用变量,使用$(fn, arguments)
是调用函数,这个不要搞不清楚,具体的还是看Makefile教程和示例指南 (foofun.cn).
这里有两个TIPS:
- 搜索的时候增加
filetype:pdf
在寻找成体系的理论性的东西的时候很好用 - 搜索的时候用
英文+cookbook
的方式往往能够找到很好的工程手册
这就说明了开源世界的重要性,做完rCore,我想我们应该去贡献一下开源世界.
官方的文件还添加了:
...
clean:
@cargo clean
.PHONY: elf binary build clean
clean
的具体实现不再赘述,而.PHONY
的意思是 伪目标(phony targets) ,用于列出那些并非真实文件的目标,而是代表某种操作的标签.
声明了伪目标,make
的过程中就不会去寻找这些文件存在与否,但是本身makefile有很强大的解析功能,因此 大部分情况不声明.PHONY
也是没关系的 .
多道应用程序的加载
思考上一章中应用程序的加载是通过结构体AppManager
的load_app
方法来实现.
unsafe fn load_app(&self, app_id: usize) {
if app_id >= self.num_app {
println!("All applications completed!");
//panic!("Shutdown machine!");
shutdown(false);
}
println!("[kernel] Loading app_{}", app_id);
// clear app area
core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, APP_SIZE_LIMIT).fill(0);
let app_src = core::slice::from_raw_parts(
self.app_start[app_id] as *const u8,
self.app_start[app_id + 1] - self.app_start[app_id],
);
let app_dst = core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, app_src.len());
app_dst.copy_from_slice(app_src);
// Memory fence about fetching the instruction memory
// It is guaranteed that a subsequent instruction fetch must
// observes all previous writes to the instruction memory.
// Therefore, fence.i must be executed after we have loaded
// the code of the next app into the instruction memory.
// See also: riscv non-priv spec chapter 3, 'Zifencei' extension.
asm!("fence.i");
}
可以看到实际上是在.data
段把APP直接拷贝到内存之中.
但是本章是没这个环节的,是把应用程序一股脑加载到内存中.
这里脑子里冒出来一个问题,为什么不直接就地运行APP(指直接把sp
寄存器指向链接到的位置).这里忽略了在.data
段的APP是不能 写入 的.
那么对于已经分别设置为不同的BASE_ADDRESS
的APP,我们要想办法把他们从.data
中加载到内存中.
替代上一节的batch.rs
,我们创建os/src/loader.rs
,里边有load_apps
和get_base_i
以及``:
// os/src/loader.rs
pub fn load_apps() {
extern "C" { fn _num_app(); }
let num_app_ptr = _num_app as usize as *const usize;
let num_app = get_num_app();
let app_start = unsafe {
core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1)
};
// load apps
for i in 0..num_app {
let base_i = get_base_i(i);
// clear region
(base_i..base_i + APP_SIZE_LIMIT).for_each(|addr| unsafe {
(addr as *mut u8).write_volatile(0)
});
// load app from data section to memory
let src = unsafe {
core::slice::from_raw_parts(
app_start[i] as *const u8,
app_start[i + 1] - app_start[i]
)
};
let dst = unsafe {
core::slice::from_raw_parts_mut(base_i as *mut u8, src.len())
};
dst.copy_from_slice(src);
}
unsafe {
asm!("fence.i");
}
}
fn get_base_i(app_id: usize) -> usize {
APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT
}
pub fn get_num_app() -> usize {
extern "C" {
fn _num_app();
}
unsafe { (_num_app as usize as *const usize).read_volatile() }
}
可以看到在load_apps
中,首先使用get_base_i
计算当前的APP的偏置地址,然后使用和上一章相同的方法,把APP的内容加载进去.而get_num_app
则负责直接获取APP的数量.
同样地,我们即使使用的是多道程序放置及加载的程序,那么我们仍然需要 内核栈 和 用户栈 .
另外,在官方的实现中,使用了一个config.rs
用来储存 用户层APP 的各项配置.
//! Constants used in rCore
pub const USER_STACK_SIZE: usize = 4096 * 2;
pub const KERNEL_STACK_SIZE: usize = 4096 * 2;
pub const MAX_APP_NUM: usize = 4;
pub const APP_BASE_ADDRESS: usize = 0x80400000;
pub const APP_SIZE_LIMIT: usize = 0x20000;
因为程序之间的数据是不能共享的,而且也为了防止出现上下文错误,因此需要给每一个APP设置一套 用户栈 和 内核栈 :
#[repr(align(4096))]
#[derive(Copy, Clone)]
struct KernelStack {
data: [u8; KERNEL_STACK_SIZE],
}
#[repr(align(4096))]
#[derive(Copy, Clone)]
struct UserStack {
data: [u8; USER_STACK_SIZE],
}
static KERNEL_STACK: [KernelStack; MAX_APP_NUM] = [KernelStack {
data: [0; KERNEL_STACK_SIZE],
}; MAX_APP_NUM];
static USER_STACK: [UserStack; MAX_APP_NUM] = [UserStack {
data: [0; USER_STACK_SIZE],
}; MAX_APP_NUM];
impl KernelStack {
fn get_sp(&self) -> usize {
self.data.as_ptr() as usize + KERNEL_STACK_SIZE
}
pub fn push_context(&self, trap_cx: TrapContext) -> usize {
let trap_cx_ptr = (self.get_sp() - core::mem::size_of::<TrapContext>()) as *mut TrapContext;
unsafe {
*trap_cx_ptr = trap_cx;
}
trap_cx_ptr as usize
}
}
impl UserStack {
fn get_sp(&self) -> usize {
self.data.as_ptr() as usize + USER_STACK_SIZE
}
}
同时,因为目前所有的APP都已经加载,因此不需要保存每个APP在未加载时候的位置,因此对AppManager
进行裁剪,只保留当前APP和APP总数的功能,同时在lazy_static
里边使用get_num_app
简化操作:
struct AppManager {
num_app: usize,
current_app: usize,
}
impl AppManager {
pub fn get_current_app(&self) -> usize {
self.current_app
}
pub fn move_to_next_app(&mut self) {
self.current_app += 1;
}
}
lazy_static! {
static ref APP_MANAGER: UPSafeCell<AppManager> = unsafe {
UPSafeCell::new({
let num_app = get_num_app();
AppManager {
num_app,
current_app: 0,
}
})
};
}
同样地,我们也需要定制一个上下文,使用__restore
利用这个上下文 恢复(实际上可以理解为配置上下文) 到 用户态 .
这时候脑子里的流出就不是单纯的sp
和sscratch
在 用户态 和 内核态 互换了,而是__restore
把第一个参数a0
里的函数入口entry
送入了sp
,然后又通过后续一系列操作把以这个sp
为基准的sscratch
也配置进去.这样就实现了多个APP上下文的切换.
这里截取一小段__restore
:
...
mv sp, a0
ld t0, 32*8(sp)
ld t1, 33*8(sp)
ld t2, 2*8(sp)
csrw sstatus, t0
csrw sepc, t1
csrw sscratch, t2
...
那么怎么制定这个上下文呢,我们可以想到TrapContext
结构体的两个组成部分一个是 用户栈的位置 一个是 APP入口 位置,这里偷取官方的代码,
pub fn init_app_cx(app_id: usize) -> usize {
KERNEL_STACK[app_id].push_context(TrapContext::app_init_context(
get_base_i(app_id),
USER_STACK[app_id].get_sp(),
))
}
然后改造上一章写得run_next_app
即可,这里的关键点在于1. 去掉加载APP的环节 2. 因为去掉加载APP的环节,因此需要在切换而不是在加载的时候判断APP是不是运行结束:
pub fn run_next_app() -> ! {
let mut app_manager = APP_MANAGER.exclusive_access();
let current_app = app_manager.get_current_app();
if current_app >= app_manager.num_app-1 {
println!("All applications completed!");
shutdown(false);
}
app_manager.move_to_next_app();
drop(app_manager);
// before this we have to drop local variables related to resources manually
// and release the resources
extern "C" {
fn __restore(cx_addr: usize);
}
unsafe {
__restore(init_app_cx(current_app));
}
panic!("Unreachable in batch::run_current_app!");
}
随后需要在代码里解决一些依赖问题,
- 在
main.rs
里增加pub mod loader
- 把
batch::run_next_app
换成loader::run_next_app
- 在
main
函数中把batch
的初始化和运行修改为loader::load_apps();
和loader::run_next_app();
尝试运行
根据评论区的经验,我建议大家先执行一下clean
:
cd user
make clean
make build
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!
[kernel] trap init end
Hello, world!
[kernel] Application exited with code 0
Into Test store_fault, we will insert an invalid store operation...
Kernel should kill this application!
[kernel] PageFault in application, kernel killed it.
3^10000=5079(MOD 10007)
3^20000=8202(MOD 10007)
3^30000=8824(MOD 10007)
3^40000=5750(MOD 10007)
3^50000=3824(MOD 10007)
3^60000=8516(MOD 10007)
3^70000=2510(MOD 10007)
3^80000=9379(MOD 10007)
3^90000=2621(MOD 10007)
3^100000=2749(MOD 10007)
Test power OK!
[kernel] Application exited with code 0
Try to execute privileged instruction in U Mode
Kernel should kill this application!
[kernel] IllegalInstruction in application, kernel killed it.
All applications completed!
[rCore学习笔记 022]多道程序与分时任务的更多相关文章
- Python Web学习笔记之多道程序设计技术和操作系统的特性
采用了多道程序设计技术的操作系统具有如下特性 : ① 并发性.它 是指两个或两个以上的事件或活动在同一时间间隔内发生.操作系统是一个并发系统,并发性是它的重要特征,操作系统的并发性指计算机系统中同时存 ...
- 【原】Java学习笔记022 - 字符串
package cn.temptation; public class Sample01 { public static void main(String[] args) { // 字符串 // 定义 ...
- 学习笔记——OS——引论
学习笔记--OS--引论 操作系统的定义 操作系统是一组管理计算机硬件资源的软件集合: 用户和计算机硬件之间的接口 控制和管理硬件资源 实现对计算机资源的抽象 计算机系统硬件 冯诺依曼体系结构和哈佛结 ...
- 操作系统学习笔记4 | CPU管理 && 多进程图像
操作系统的核心功能就是管理计算机硬件,而CPU就是计算机中最核心的硬件.而通过学习笔记3的简史回顾,操作系统通过多进程图像实现对CPU的管理.所以多进程图像是操作系统的核心图像. 参考资料: 课程:哈 ...
- Linux 学习笔记
Linux学习笔记 请切换web视图查看,表格比较大,方法:视图>>web板式视图 博客园不能粘贴图片吗 http://wenku.baidu.com/view/bda1c3067fd53 ...
- 操作系统学习笔记(五)--CPU调度
由于第四章线程的介绍没有上传视频,故之后看书来补. 最近开始学习操作系统原理这门课程,特将学习笔记整理成技术博客的形式发表,希望能给大家的操作系统学习带来帮助.同时盼望大家能对文章评论,大家一起多多交 ...
- 学习笔记之Java程序设计实用教程
Java程序设计实用教程 by 朱战立 & 沈伟 学习笔记之JAVA多线程(http://www.cnblogs.com/pegasus923/p/3995855.html) 国庆休假前学习了 ...
- 操作系统学习笔记----进程/线程模型----Coursera课程笔记
操作系统学习笔记----进程/线程模型----Coursera课程笔记 进程/线程模型 0. 概述 0.1 进程模型 多道程序设计 进程的概念.进程控制块 进程状态及转换.进程队列 进程控制----进 ...
- Python学习笔记九
Python学习笔记之九 为什么要有操作系统 管理硬件,提供接口. 管理调度进程,并且将多个进程对硬件的竞争变得有序. 操作系统发展史 第一代计算机:真空管和穿孔卡片 没有操作系统,所有的程序设计直接 ...
- R语言与机器学习学习笔记
人工神经网络(ANN),简称神经网络,是一种模仿生物神经网络的结构和功能的数学模型或计算模型.神经网络由大量的人工神经元联结进行计算.大多数情况下人工神经网络能在外界信息的基础上改变内部结构,是一种自 ...
随机推荐
- 实验一:Wireshark工具的使用
1.0 [实验目的] 了解Wireshark.TCP协议的概念,掌握Wireshark抓包工具的使用.FTP的搭建和登录,学会对Wireshark抓包结果的分析. 2.0[知识点] Wireshark ...
- Vulnhub Mercy Walkthrough
Recon 首先进行二层扫描. ┌──(kali㉿kali)-[~] └─$ sudo netdiscover -r 192.168.80.0/24 Currently scanning: Finis ...
- Sealos 5.0 正式发布,云本应该是操作系统
把所有资源抽象成一个整体,一切皆应用,这才是云应该有的样子. 2018 年 8 月 15 日 Sealos 提交了第一行代码. 随后开源社区以每年翻倍的速度高速增长. 2022 年我们正式创业,经历一 ...
- C#如何创建一个可快速重复使用的项目模板
写在前面 其实很多公司或者资深的开发都有自己快速创建项目的脚手架的,有的是魔改代码生成器实现,有的直接基于T4,RazorEngine等模板引擎打造:但无论如何,其最终目的其实就是搭建一个自定义项目模 ...
- K-means聚类是一种非常流行的聚类算法
K-means聚类是一种非常流行的聚类算法,它的目标是将n个样本划分到k个簇中,使得每个样本属于与其最近的均值(即簇中心)对应的簇,从而使得簇内的方差最小化.K-means聚类算法简单.易于实现,并且 ...
- Freertos学习:08-信号量
--- title: rtos-freertos-08-ipc-semaphore date: 2020-06-23 11:01:12 categories: tags: - freertos - i ...
- opengauss Need repair修复
问题描述:opengauss集群在做切换的时候,或者增删节点的时候,很容易发生节点repair,找不到主库的情况,这种情况需要把主库使用primary角色启动,然后build重建从库,就可以恢复集群 ...
- Linux查看系统占用
## 查看内存占用 #CPU占用最多的前10个进程: ps auxw|head -1;ps auxw|sort -rn -k3|head -10 #内存消耗最多的前10个进程 ps auxw|head ...
- Log4Net配置详解及输出自定义消息类示例
1.简单使用实例 1.1 添加log4net.dll的引用. 在NuGet程序包中搜索log4net并添加,此次我所用版本为2.0.17.如下图: 1.2 添加配置文件 右键项目,添加新建项, ...
- debian12 创建本地harbor镜像库
前言 harbor是一个docker/podman镜像管理库,可用于存储私人镜像.现将本人在debian12系统搭建harbor镜像库的过程记录下来,留作后续参考. 可以参考github harbor ...