予焦啦!RISC-V 的计时器中断机制

本节是以 Golang 上游 8854368cb076ea9a2b71c8b3c8f675a8e19b751c 为基准做的实验

予焦啦!在前两天的断章当中,我们已经理解到我们必须审慎处理接下来的问题,准备不同的元件才能够有机会达成最快速打通流程、最大限度利用 Golang 原生机制作为作业系统内部机制的目标。今天就来准备其中一个元件,回归基本学习一下 RISC-V 的中断,尤其是计时器中断的机制。

本节重点概念

  • RISC-V
    • 中断与例外的基本机制:midelegmedeleg
    • 计时器中断的基本机制:mtimemtimecmp
    • 中断管理:sstatussiesip

从 OpenSBI 看起

可能隔了一段时间了,但这里还是要提起一个 RISC-V 世界的基本常识:最高的执行权限发生在机器模式(M-mode),而在我们的实验当中,这个部分的系统都是由 OpenSBI 专案负责的。

至今为止,笔者在节录 QEMU 输出时,通常都会展室到这两行:

Boot HART MIDELEG         : 0x0000000000000222
Boot HART MEDELEG         : 0x000000000000b109

以 RISC-V 系统的预设行为来讲,所有的中断与例外都会进到 MTVEC 的机器模式处理向量去,但这麽做显然效能不会太好。因此有了这两个控制暂存器,让不同的中断或是例外状况,可以被委派(delegation)到作业系统模式的权限等级去。

这两个控制暂存器的意义分别是:中断委派与例外委派。其中的每个位元都是真伪的标记,代表的意义如下(粗体意为 OpenSBI 设置为预设委派给作业系统模式):

  • MIDELEG(同 MIP 状态暂存器的配置)
    • [3:0]:机器模式软件中断、保留、作业系统模式软件中断、保留
    • [7:4]:机器模式计时器中断、保留、作业系统模式计时器中断、保留
    • [11:8]:机器模式外部中断、保留、作业系统模式外部中断、保留
    • 其余保留。
  • MEDELEG(同 MCAUSE 状态暂存器的配置)
    • [3:0]指令位址未对齐、指令存取错误、不合法的指令、除错断点
    • [7:4]:读取位址未对齐、读取存取错误、储存或原子操作未对齐、储存或原子操作存取错误
    • [11:8]:来自机器模式的环境呼叫、保留、来自作业系统模式的环境呼叫、来自使用者模式的环境呼叫
    • [15:12]读取或原子操作页面错误、保留、读取页面错误存取指令页面错误

中断的部分比较直截了当,作业系统模式的中断,许多都是与装置相关的,由作业系统的驱动程序本身来处理是最直接的。计时器与软件中断也是在作业系统模式处理。

例外的部分这里只解释几例:页面错误也没有什麽理由要在机器模式处理,因为这个抽象层完全是在作业系统层级的;另一个是使用者模式的环境呼叫,也就是系统呼叫(system call),如果还要到机器模式再转,未免太没效能。

计时器中断的基本机制

RISC-V 提供的计时器中断机制基於两种记忆体映射暂存器:mtimemtimecmp。规格书当中详细地说明了为什麽这些比较适合作为记忆体映射的暂存器而非每个核心私有的控制暂存器。

主要是因为,mtime 的机制是稳定增加、频率守恒的经过时间(wall time)计时器,而不是单纯的时脉计数器。因为先进的处理器多半会支援在运作期间改变频率的功能以调节效能与用电之间的平衡,所以单纯的时脉计数器并不是 mtime 的设计理念。要达成这个目标,通常会需要类似石英震荡器之类的硬体,因此让它成为全系统共享的装置,就显得理所当然。

值得一提的是,在 OpenSBI 的相关程序码中看来,很有可能也会有些计时器设计会提供非共享式的 mtime,但笔者以 QEMU 作为实验平台,暂不考虑那些架构。

mtimecmp,则是每个核心都自己独有的,且可自由读写。当时间随着系统运行不断流逝,直到 mtime 的值变为大於或等於 mtimecmp 之时,这颗核心收到计时器中断的第一个必要条件就圆满了。

设置计时器事件

如同字面上显示的,在作业系统软件的角度来看,这些工作大多是 OpenSBI 在负责。实务上,OpenSBI 对作业系统开放一些环境呼叫(enviroment call),让作业系统可以设置过期时间,而让 OpenSBI 自己转换之後设置 mtimecmp。这个 SBI 呼叫是 sbi_set_timer,我们以组合语言实作之:

// func SetTimer()
TEXT runtime∕ethanol·SetTimer(SB), NOSPLIT|NOFRAME, $0-0
        CSRRS   CSR_TIME, ZERO, A0
        ADD     $0x1000000, A0, A0
        MOV     $0, A6
        MOV     $0x54494D45, A7
        ECALL
        RET

这个 CSRRS 指令,之前没有使用过。原本是用来设置(set)状态暂存器的意义,但这里,当来源暂存器使用 ZERO 时,这就成为了官方推荐的虚拟指令(pseudo instruction)CSRR,也就是单纯的将控制暂存器内容读取到目地暂存器。这里,我们读取的是 time,这是 mtime 在作业系统模式的一个可读取的别名。

CSRRS 可以比照 CSRRC 新增,但加完之後需要重编 Golang 工具链。

之後,我们需要设置一个下一个过期的时间点给予计时器,这里胡乱定一个值,日後再好好处理。以 QEMU 的这个平台来讲,这个值大概有 1.5 秒的体感时间可以观察。

A6A7 暂存器的赋值是 SBI 呼叫的惯例,对应到的即是 sbi_set_timer

计时器中断的其他必要条件

还有几组必要条件,散落在不同的控制暂存器里面。

mideleg

这先前已经介绍过了。

sstatus:作业系统模式系统状态暂存器

sstatus 或是 mstatus 都包含非常多面向的控制与状态位元,这里只会介绍一个索引在 1 的位元:SIE。这个位元的意义代表,作业系统模式的中断是否被启用,相当於是所有中断的总开关。

我们在今天的实验中,实作一个组合语言函数来操作这个暂存器:

// func Interrupt(on bool)
TEXT runtime∕ethanol·Interrupt(SB), NOSPLIT|NOFRAME, $0-8
        MOV     on+0(FP), A0
        MOV     SSTATUS_SIE, A1
        BEQ     A0, ZERO, clear
        CSRRS   CSR_SSTATUS, A1, ZERO
        RET
clear:
        CSRRC   CSR_SSTATUS, A1, ZERO
        RET

并在 osinit 当中启用之:

const (
        ON  = true
        OFF = false
)

func osinit() {
...
        argc = ethanol.GetArgc()
        print("argc: ", argc, "\n")

+       ethanol.Interrupt(ON)
}

siesip:作业系统模式中断启用与搁置

最後一个计时器中断的要素是这两个控制暂存器。

今天探讨的作业系统计时器中断位元在两者当中都是占据第 5 位元,也就是 0x20 的位址。在今天的实验中,我们先写入这个位元到 sie 之中,

...
        argc = ethanol.GetArgc()
        print("argc: ", argc, "\n")

        ethanol.Interrupt(ON)
+       ethanol.TimerInterrupt(ON)
...

而这个实作同样位在新增的 src/rumtime/ethanol/trap.s 当中:

// func TimerInterrupt(on bool)
TEXT runtime∕ethanol·TimerInterrupt(SB), NOSPLIT|NOFRAME, $0-8
        MOV     on+0(FP), A0
        MOV     SIE_STI, A1
        BEQ     A0, ZERO, clear 
        CSRRS   CSR_SIE, A1, ZERO
        RET
clear:
        CSRRC   CSR_SIE, A1, ZERO
        RET

这些要素都齐备了之後,就会以 scause0x8000000000000005 的中断代码,进入到 stvec 所在的位址。那麽为什麽进入之後,计时器中断不会无限触发呢?因为,sstatus 在进入中断之後,会将 SPIE 位元设为 SIE 位元的值,并将 SIE 的值设置为 0;当 SRET 指令执行之後,会反过来将 SIE 位元设为 SPIE 的值,并将 SPIE 的值设为 1。这是一种避免巢状呼叫的方式。

试跑

如果我们只是加上这些作业系统计时器中断的功能的话,也许在那个中断真的到来之前,还是会踩到没有实作的 newosproc 的那个错误。

所以我们先在 newosproc 里面插入一个无穷回圈让它卡住执行流程:

 func newosproc(mp *m) {
+       for {
+       }
        panic("newosproc: not implemented")
 }

并且,要是一切执行符合预期,那麽在 early_halt,也就是我们现在的 stvec 的中断处理函式之内,我们印完相关的状态暂存器之後,也应该要再设置一个新的过期时间,否则计时器中断就会变成仅此一次的事件。所以,我们加入以下逻辑覆盖掉原本的 WFI 行为:

diff --git a/src/runtime/rt0_opensbi_riscv64.s b/src/runtime/rt0_opensbi_riscv64.s
index 13b3626a4e..a7c9baca2f 100644
--- a/src/runtime/rt0_opensbi_riscv64.s
+++ b/src/runtime/rt0_opensbi_riscv64.s
@@ -58,6 +58,8 @@ TEXT early_halt(SB),NOSPLIT|NOFRAME,$0
        CSRRS   CSR_SEPC, ZERO, A0
        CALL    dump(SB)
 
+       CALL    runtime∕ethanol·SetTimer(SB)
+       SRET
        WFI
        JMP     early_halt(SB)

这样的处理之後,我们就会看到原本的 scausestvalsepc 输出,每隔一段时间,就会印出来的效果!

以下的实验结果可以透过今天更新的 Hoddarla repo 获得。

道歉启事:昨日如果有读者引用了 patch 之後重编工具链应该会发生编译错误,应该是印出参数和环境变数造成的一些附加效应使然。今天笔者就会将它们移除了。

...
Alloc: 0x10000 bytes 
Reserve: 0x10000 bytes, at 0x0 but at 0xffffffc500010000
Map: 0x10000 bytes, at 0xffffffc500010000
I8000000000000005
0000000000000000
ffffffc000030dd8
I8000000000000005
0000000000000000
ffffffc000030dd8
I8000000000000005
0000000000000000
ffffffc000030dd8
I8000000000000005
0000000000000000
ffffffc000030dd8
...

小结

予焦啦!整理一下 RISC-V 正常的计时器中断的一整个流程:
0. 在作业系统模式,sstatus.SIE 的中断总开关已经设置;sie.STI 计时器中断也已经设置。

  1. 作业系统模式呼叫了 sbi_set_timer,写入 mtimecmp
  2. sbi_set_timer 尾声,设定 mie.MTI
  3. 首先是机器模式的计时器中断,因为满足了机器模式中断的三个要件:
    1. mstatus.MIE 有设置,或者是中断发生在较低的权限等级。这里是後者。
    2. mie.MTImip.MTI 都已设置(後者是由於 mtime 增长到超过 mtimecmp 的缘故)。
    3. midelegMTI 位元没有设定(事实上,就算设定了也没有用)。
  4. 经过 mtvecsbi_timer_process
    1. 清除 mie.MTI
    2. 设置 mip.STI。这个效应将会连带影响到 sip.STI
  5. 经过机器模式的中断处理之後回到作业系统模式,立刻触发作业系统模式的计时器中断,因为满足了两个要件:
    1. sstatus.SIE 有设置,或者是中断发生在较低的权限等级。这里是前者。
    2. sie.STIsip.STI 都已设置。
  6. 进到 stvec 时,sstatus.SIE 被清为 0,因此不会无限触发,进而允许中断处置函数运作。

今天我们使用最直接暴力的手法先打通了,但其实这样会余下许多问题是我们目前都还没有探讨过的。不过我们就将那些问题留到明天吧。各位读者,我们明天再会!


<<:  Day13:今天来聊一下Parrot Security上的CGI scanner Nikto

>>:  JavaScript学习日记 : Day15 - 原型与继承(二)

Day 9:JSON 资料解析

本篇文章同步发表在 HKT 线上教室 部落格,线上影音教学课程已上架至 Udemy 和 Youtu...

Day#06 新增

前言 昨天已经在storyboard上将tableView连结上程序中的table,不过并没有解释到...

Chapter1-DJ最爱的音频动感图像(II)只要是认识Canvas的都觉得它很High欧

完成上传机制後,等着我们的是... 今天的一开始先花一点点时间,把昨天的事件监听做完吧!这边准备好一...

30天打造品牌特色电商网站 Day.23 关於position定位

在排版方面还有一个很重要的属性,其实前面的范例多少能见到它的身影,今天我们就来深入的认识它吧!<...

【D29】食材、厨具准备好了,准备上桌

前言 我们已经熟悉了厨房环境(开发环境)、各式各样的厨具(API),以及可以料理的食谱(商品与策略)...