RVOS的trap
看 xv6 的代码有点迷惑了,对于需要看 qemu 手册和 RISC-V 手册的实现不太懂,来汪辰老师的 RVOS 课程补一补,讲的真好,很精妙的设计!
05 trap和中断
mtvec 的 两种模式
dircet:
vector:一张中断向量表,处理效率更高
分清什么是硬件自动执行的,什么是我们手动来实现的,才知道怎么code
处理 trap 的时候我们可以修改 mepc 的值达到改变 mret 返回地址的目的
mtval辅助 mcause来告诉我们异常或者中断更加详细的信息
mpp占两位,因为可能是 m,s,u 这三个特权级要用 2 bit表示
没有 upp (因为他只有一种可能:自己陷入自己)
RISC-V Trap 处理流程
top half是不受我们控制的,硬件发生的过程
- 把 mstatus 的 MIE 值复制到 MPIE 中,清除 mstatus 中的 MIE 标 志位,效果是中断被禁止。
- 设置 mepc ,同时 PC 被设置为 mtvec。(需要注意的是,对于 exception, mepc 指向导致异常的指令;对于 interrupt,它指向被 中断的指令的下一条指令的位置。)
- 为什么要这样设置? 发生异常的时候相当于给了你一个机会,你可以执行你的异常处理函数,你可以在这里面解决掉你的异常,例子:缺页异常
- 而中断一般是外设产生的,要去执行一段外设的逻辑,回来的话回到下一条,达到对应用程序看来好像什么都没有发生过的效果
- 根据 trap 的种类设置 mcause,并根据需要为 mtval 设置附加信息
- 将 trap 发生之前的权限模式保存在 mstatus 的 MPP 域中,再把 hart 权限模式更改为 M(也就是说无论在任何 Level 下触发 trap, hart 首先切换到 Machine 模式)。
所以要使用 s 模式的话这些也是一样的吗?
bottom-half:软件需要做的事,以下是在汇编中写的
- 保存当前控制流的上下文信息
- 调用 c 语言的 trap handler
- 从 trap handler 返回, mepc 的值有可能需要调整
- 恢复上下文
- 执行 mret 指令返回到 trap 之前的状态
mret指令:还有sret/uret
- 当前 hart 的权限级别 = mstatus.MPP;mstatus.MPP = U(如果 hart 不支持 U 则为 M)(恢复hart trap前的权限级别)
- mstatus.MIE = mstatus.MPIE; mstatus.MPIE=1 (恢复hart trap前的中断使能)
- pc = mepc (跳转到 trap 前的执行流)
很有意思的点
1 |
|
试试第25,26行:分别注释掉的效果
只使用 25 行:panic死循环
只使用 26 行:改变了 return 到了下一条指令,那么可以返回这里异常的下一条指令
06 外部设备中断
中断分类
- 本地中断
- 软件中断
- 时钟中断
- 全局中断
- 外部中断:外设产生的中断
问题:前面提到的 mstatus.mie 和这里的 mie 寄存器什么关系?
mstatus. mie 控制全局的中断使能状态,而 mie 寄存器中的各个位控制具体的中断源是否被使能
比如你要打开时钟中断(mie里的MTIE):要保证 mstatus.mie = 1 && mie.MTIE = 1
寄存器
mie
:打开或者关闭 M/S/U 模式下对应的 External/Timer/Software 中断
mip
获取当前 M/S/U 模式 下对应的 External/Timer/Software 中断是否发
mie可以理解为是用来写的,mip可以理解为是用来读的
PLIC介绍
为什么需要PLIC,因为外部设备很多,不可能对每一个外设都设置一个引脚,来告诉 cpu 这个外设产生中断了,于是需要一个中断代理,这样我们只需要一个引脚,简化了cpu的设计复杂度
PLIC每次只放一个中断进来
中断源介绍:
1 |
|
- 0 预留不用
PLIC编程接口-寄存器
priority
设置 某一路中断源的优先级
- 内存映射地址:base + id * 4
- 每个 PLIC 中断源对应一个寄存器,用于配置该中断源的优先级
- QEMU-virt 支持 7 个优先级。 0 表示对该中断源禁用中断。 其余优先级,1 最低,7 最高
- 如果两个中断源优先级相同,则根据中断源的 ID 值进一步 区分优先级,ID 值越小的优先级越高
1 |
|
pending
:指示某一路中断源是否发生
- 每个 PLIC 包含 2 个 32 位的 Pending 寄存器,每一个 bit 对应 一个中断源,如果为 1 表示该中断源上发生了中断(进入 Pending 状态),有待 hart 处理,否则表示该中断源上当前无 中断发生
- claim 会清除掉 pending 寄存器某一位对应的状态
- 第一个 Pending 寄存器的第 0 位对应不存在的 0 号中断源,其 值永远为 0
Enable
:针对某个 hart 开启或者关闭某一路中断源
- 每个 Hart 有 2 个 Enable 寄存器 (Enable1 和 Enable2)用 于针对该 Hart 启动或者关闭某路中断源
- 每个中断源对应 Enable 寄存器的一个 bit,其中 Enable1 负责控制 1 ~ 31 号中断源;Enable2 负责控制 32 ~ 53 号中断源。 将对应的 bit 位设置为 1 表示使能该中断源,否则表示关闭该中断源
1 |
|
Threshold
:针对某个 hart 设置中断源优先级的阈值
- 每个 Hart 有 1 个 Threshold 寄存器用于设置中断优先级 的阈值
- 所有小于或者等于(<=)该阈值的中断源即使发生了也会 被 PLIC 丢弃。特别地,当阈值为 0 时允许所有中断源上发 生的中断;当阈值为 7 时丢弃所有中断源上发生的中断
Claim/Complete
:这两个是同一个寄存器, 每个 hart 一个
- Claim:对该寄存器执行读操作,获取当前 hart 发生的最高优先级的中断源 id,claim成功后会清除对应的 pending 位
- Complete:对该寄存器执行写操作,通知 PLCI 这个中断处理已经结束,可以接受下一个了
uart 为例
以 uart 这个中断源为例,我们如果要处理一个中断源,应该怎么编程
在 plic_init 阶段
- 中断源:设置该中断源优先级、设置 enable 寄存器使能该终端源
- hart:设置 threshold 阈值,使能 mie,mstatus
具体 uart 的中断处理逻辑
- trap处理具体的 cause 时,加入一个外部中断的处理
- 在外部中断处理里面加入判断,首先通过 plic_claim 获取 irq,如果 irq 是 uart 的 id 就执行它的中断流程
- 然后完成之后记得调用 plic_complete
注意在 uart_init 里面需要打开 uart 自身可以接受中断
总结:编程时需要注意有的寄存器是 hart 级别的,有的寄存器是全局的,需要区别
07定时器中断
CLINT (Core Local INTerrupt)
CLINT编程接口
mtime
:real-time 计时器
- 系统保证该计数器的值始终按照一个固定的频率递增
- 上电复位时,硬件负责将mtime的值恢复为0
mtimecmp
- 每个 hart 一个 mtimecmp 寄存器,64-bit
- 上电复位时,系统不负责设置 mtimecmp 的初值,所以要你自己来
这个寄存器的用处就是用来触发时钟中断:
- 当 mtime >= mtimecmp 时,CLINT 会产生一个 timer 中断。 如果要使能该中断需要保证全局中断打开并且 mie.MTIE 标志位 置 1
- 当 timer 中断发生时,hart 会设置 mip.MTIP,程序可以在 mtimecmp 中写入新的值清除 mip.MTIP
Tick:
- os 中最小的时间单位
- Tick 的单位(周期)由硬件定时器的周期决定(通常为 1~ 100 ms)