rCore_Lab3
本章任务
1、通过提前加载应用程序到内存,减少应用程序切换开销
2、通过协作机制支持程序主动放弃处理器,提高系统执行效率
3、通过抢占机制支持程序被动放弃处理器,保证不同程序对处理器资源使用的公平性,也进一步提高了应用对 I/O 事件的响应效率
本章主要是设计和实现建立支持 多道程序 的二叠纪“锯齿螈” 1 初级操作系统、支持 多道程序 的三叠纪“始初龙” 2 协作式操作系统和支持 分时多任务 的三叠纪“腔骨龙” 3 抢占式操作系统,从而对可支持运行一批应用程序的多种执行环境有一个全面和深入的理解,并可归纳抽象出 任务 、 任务切换 等操作系统的概念。
rust知识点
map与find
usize转可变结构体类型
struct MyStruct {
// 结构体字段
}
fn main() {
// 假设你有一个指向 MyStruct 的 usize 值
let my_struct_address: usize = /* ... */;
let my_struct: &mut MyStruct;
unsafe {
my_struct = &mut *(my_struct_address as *mut MyStruct);
}
// 现在可以使用 my_struct 作为对结构体的可变引用
}
可变结构体类型转usize
struct MyStruct {
// 结构体字段
}
fn main() {
let mut my_struct = MyStruct {
// 初始化字段
};
let address = &my_struct as *const MyStruct as usize;
println!("The address of my_struct is: {}", address);
}
RISC-V知识
嵌套中断与嵌套Trap
嵌套中断可以分为两部分:在处理一个中断的过程中又被同特权级/高特权级中断所打断。默认情况下硬件会避免同特权级再次发生,但高特权级中断则是不可避免的会再次发生。
嵌套 Trap 则是指处理一个 Trap(可能是中断或异常)的过程中又再次发生 Trap ,嵌套中断是嵌套 Trap 的一个特例。在内核开发时我们需要仔细权衡哪些嵌套 Trap 应当被允许存在,哪些嵌套 Trap 又应该被禁止,这会关系到内核的执行模型。
mtime与mtimecmp
mtime 是64位的CSR,用来统计处理器自上电以来经过了多少个内置时钟的时钟周期
mtimecmp : 一旦计数器 mtime
的值超过了 mtimecmp
,就会触发一次时钟中断。这使得我们可以方便的通过设置 mtimecmp
的值来决定下一次时钟中断何时触发
sstatus的FS字段
RISC-V 的 sstatus
寄存器中的 FS
字段是与浮点数相关的状态字段。它用来指示浮点单元(FPU)的当前使用状态。这个字段主要用于操作系统层面,帮助操作系统高效地管理浮点运算环境,尤其是在上下文切换时。
FS
字段通常有以下几种状态:
- Off:表示没有使用浮点单元,不需要保存或恢复浮点寄存器的内容。
- Initial:表示浮点寄存器已经被使用,但其内容还没有被第一个浮点指令改变。
- Clean:表示浮点寄存器被使用过,且其内容可能已经被改变,但与内存中的备份一致,因此不需要在上下文切换时保存。
- Dirty:表示浮点寄存器被使用且其内容已经被改变,且与内存中的备份不一致,因此在上下文切换时需要保存其内容。
在进行上下文切换时,操作系统会检查 FS
字段的状态,以决定是否需要保存和恢复浮点寄存器的内容,从而提高系统的效率。在一些没有使用浮点运算或者浮点运算不频繁的应用中,这可以显著减少不必要的数据保存和恢复操作,提高系统性能
作业
编程题
1、扩展内核,能够显示操作系统切换任务的过程。
加上一句话得了
2、扩展内核,能够统计每个应用执行后的完成时间:用户态完成时间和内核态完成时间。
这道题的实现我和答案不一样,不过在实践作业上计算运行时间和这道题的参考答案差不多
这道题我是通过打log的方式(本来想懒省事,不过没想到最后没躲得了)
3、 编写浮点应用程序A,并扩展内核,支持面向浮点应用的正常切换与抢占。
这里需要注意的就是,需要在main函数开启sstatus的fs字段
main.rs
use riscv::register::sstatus;
use riscv::register::mstatus::FS;
unsafe { sstatus::set_fs(FS::Clean) }; //
reap.S
.altmacro
.macro SAVE_GP n
sd x\n, \n*8(sp)
.endm
.macro LOAD_GP n
ld x\n, \n*8(sp)
.endm
# 任务三---扩展内核---保存浮点寄存器(32个)
.macro SAVE_FP n
fsd f\n, (\n+34)*8(sp)
.endm
.macro LOAD_FP n
fld f\n, (\n+34)*8(sp)
.endm
.section .text
.globl __alltraps
.globl __restore
.align 2
__alltraps:
csrrw sp, sscratch, sp
# now sp->kernel stack, sscratch->user stack
# allocate a TrapContext on kernel stack
addi sp, sp, -34*8-32*8
# save general-purpose registers
sd x1, 1*8(sp)
# skip sp(x2), we will save it later
sd x3, 3*8(sp)
# skip tp(x4), application does not use it
# save x5~x31
.set n, 5
.rept 27
SAVE_GP %n
.set n, n+1
.endr
# we can use t0/t1/t2 freely, because they were saved on kernel stack
csrr t0, sstatus
csrr t1, sepc
sd t0, 32*8(sp)
sd t1, 33*8(sp)
# read user stack from sscratch and save it on the kernel stack
csrr t2, sscratch
# 浮点寄存器
.set n,0
.rept 32
SAVE_FP %n
.set n,n+1
.endr
sd t2, 2*8(sp)
# set input argument of trap_handler(cx: &mut TrapContext)
mv a0, sp
call trap_handler
__restore:
# now sp->kernel stack(after allocated), sscratch->user stack
# restore sstatus/sepc
ld t0, 32*8(sp)
ld t1, 33*8(sp)
ld t2, 2*8(sp)
csrw sstatus, t0
csrw sepc, t1
csrw sscratch, t2
# restore general-purpuse registers except sp/tp
ld x1, 1*8(sp)
ld x3, 3*8(sp)
.set n, 5
.rept 27
LOAD_GP %n
.set n, n+1
.endr
# 浮点寄存器
.set n,0
.rept 32
LOAD_FP %n
.set n,n+1
.endr
# release TrapContext on kernel stack
addi sp, sp, 34*8+32*8
# now sp->kernel stack, sscratch->user stack
csrrw sp, sscratch, sp
sret
4、 编写应用程序或扩展内核,能够统计任务切换的大致开销。
没搞懂题目要我做什么,是只计算通过__switch的时间还是要包含用户app,不过没关系,我们通过打log一样能很好的分析os
不知道为什么,就算我使用 get_time_us函数,得到微妙,精度任然不够,因为这样得到的单次switch的结果不是0就是1,我必须使用get_time();函数才能保障精度
于是乎,我只在switch前后写了两个简单的get_time,之后打log完事
run_next_task.rs
不过需要注意的是,这里的switch执行流程有些好玩:
-
如果task是初次经过这个switch,由于初始化的时候返回地址ra设置的是user的入口点,故初代task经过__switch后回直接ret到用户入口点,这里__switch函数上下设置的时间差包含了用户app的时间
-
如果task是第二次经过这个switch,其是其上次来到这里所设置的返回地址,即__switch的下一条指令,即这里
它的上下时间差也包含了app的时间
-
03sleep.rs文件循环使用sys_yield系统调用,其每次切换都从03app切换到03app,你没有看错,就是原地切换,这就导致switch设置的时间差就是通过switch的时间差
5、*** 扩展内核,支持在内核态响应中断。
略,还没仔细研究呢(byd课后作业也太多了)
6、*** 扩展内核,支持在内核运行的任务(简称内核任务),并支持内核任务的抢占式切换
同上
问答题
1、 协作式调度与抢占式调度的区别是什么?
协作式调度中,进程主动放弃 (yield) 执行资源;抢占式调度中,进程会被强制打断暂停,释放资源让给别的进程。
2、中断、异常和系统调用有何异同之处?
相同点 : 都走trap handler
不同点:
- 中断的来源是异步的外部事件,由外设、时钟、别的hart等外部来源
- 异常是CPU正在执行的指令产生的
- 系统调用是操作系统给应用程序的接口,由程序主动触发
3、 RISC-V支持哪些中断/异常?
4、如何判断进入操作系统内核的起因是由于中断还是异常
match scause.cause() 匹配判断
5、在 RISC-V 中断机制中,PLIC 和 CLINT 各起到了什么作用?
CLINT 处理时钟中断 (MTI
) 和核间的软件中断 (MSI
);PLIC 处理外部来源的中断 (MEI
)
6、** 基于RISC-V 的操作系统支持中断嵌套?
RISC-V原生不支持中断嵌套。(在S态的内核中)只有 sstatus
的 SIE
位为 1 时,才会开启中断,再由 sie
寄存器控制哪些中断可以触发。触发中断时,sstatus.SPIE
置为 sstatus.SIE
,而 sstatus.SIE
置为0;当执行 sret
时,sstatus.SIE
置为 sstatus.SPIE
,而 sstatus.SPIE
置为1。这意味着触发中断时,因为 sstatus.SIE
为0,所以无法再次触发中断。
7、本章提出的任务的概念与前面提到的进程的概念之间有何区别与联系?
区别: 无地址空间隔离
联系: 任务和进程都有自己独立的栈、上下文信息,任务是进程的“原始版本”
8、简单描述一下任务的地址空间中有哪些类型的数据和代码
.text、.data、.rodata、.bss
9、任务控制块保存哪些内容?
栈地址、常用寄存器、PC、进程状态
10、任务上下文切换需要保存与恢复哪些内容?
常用寄存器、栈、PC
11、特权级上下文和任务上下文有何异同?
特权级上下文保存了所有寄存器,任务上下文保存了一些寄存器,并且特权级上下文涉及到了特权级的切换,任务上下文只涉及到了同权限的切换
12、上下文切换为什么需要用汇编语言实现?
使用高级语言切换上下文的时候可能无意之间修改了某些寄存器或栈空间,不好操作
13、有哪些可能的时机导致任务切换?
(1)、时间片轮转
(2)、进程终止
14、在设计任务控制块时,为何采用分离的内核栈和用户栈,而不用一个栈?
如果共用一个栈,用户将很难有可能访问甚至修改内核栈的数据,这对于操作系统的安全性有极大的影响
15、我们已经在 rCore 里实现了不少操作系统的基本功能:特权级、上下文切换、系统调用……为了让大家对相关代码更熟悉,我们来以另一个操作系统为例,比较一下功能的实现。看看换一段代码,你还认不认识操作系统。
阅读 Linux 源代码,特别是 riscv
架构相关的代码,回答以下问题:
- Linux 正常运行的时候,
stvec
指向哪个函数?是哪段代码设置的stvec
的值? - Linux 里进行上下文切换的函数叫什么?(对应 rCore 的
__switch
) - Linux 里,和 rCore 中的
TrapContext
和TaskContext
这两个类型大致对应的结构体叫什么? - Linux 在内核态运行的时候,
tp
寄存器的值有什么含义?sscratch
的值是什么? - Linux 在用户态运行的时候,
sscratch
的值有什么含义? - Linux 在切换到内核态的时候,保存了和用户态程序相关的什么状态?
- Linux 在内核态的时候,被打断的用户态程序的寄存器值存在哪里?在 C 代码里如何访问?
- Linux 是如何根据系统调用编号找到对应的函数的?(对应 rCore 的
syscall::syscall()
函数的功能) - Linux 用户程序调用
ecall
的参数是怎么传给系统调用的实现的?系统调用的返回值是怎样返回给用户态的?
阅读代码的时候,可以重点关注一下如下几个文件,尤其是第一个 entry.S
,当然也可能会需要读到其它代码:
arch/riscv/kernel/entry.S
(与 rCore 的switch.S
对比)arch/riscv/include/asm/current.h
arch/riscv/include/asm/processor.h
arch/riscv/include/asm/switch_to.h
arch/riscv/kernel/process.c
arch/riscv/kernel/syscall_table.c
arch/riscv/kernel/traps.c
include/linux/sched.h
此外,推荐使用 https://elixir.bootlin.com 阅读 Linux 源码,方便查找各个函数、类型、变量的定义及引用情况。
一些提示:
-
Linux 支持各种架构,查找架构相关的代码的时候,请认准文件名中的
arch/riscv
。 -
为了同时兼容 RV32 和 RV64,Linux 在汇编代码中用了几个宏定义。例如,
REG_L
在 RV32 上是lw
,而在 RV64 上是ld
。同理,REG_S
在 RV32 上是sw
,而在 RV64 上是sd
。 -
如果看到
#ifdef CONFIG_
相关的预处理指令,是 Linux 根据编译时的配置启用不同的代码。一般阅读代码时,要么比较容易判断出这些宏有没有被定义,要么其实无关紧要。比如,Linux 内核确实应该和 rCore 一样,是在 S-mode 运行的,所以CONFIG_RISCV_M_MODE
应该是没有启用的。 -
汇编代码中可能会看到有些
TASK_
和 PT_ 开头的常量,找不到定义。这些常量并没有直接写在源码里,而是自动生成的。在汇编语言中需要用到的很多
struct
里偏移量的常量定义可以在arch/riscv/kernel/asm-offsets.c
文件里找到。其中,OFFSET(NAME, struct_name, field)
指的是NAME
的值定义为field
这一项在struct_name
结构体里,距离结构体开头的偏移量。最终这些代码会生成asm/asm-offsets.h
供汇编代码使用。 -
#include <asm/unistd.h>
在arch/riscv/include/uapi/asm/unistd.h
,#include <asm-generic/unistd.h>
在include/uapi/asm-generic/unistd.h
。
先略了,因为我感觉我是真没时间了,复试后再看
实验
直接上代码
可优化的点/注意点
这里不能设置为局部变量,因为你的栈就4096的大小,需要开辟到堆区上或者直接扩栈,要不就不用定长数组
还有就是这里我直接懒省事,申请了全局可变变量,更安全的做法是像os一样搞个智能指针
其他的也没啥需要注意的了