予焦啦!实作上下文机制

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

予焦啦!今天我们就来验收前两天的综合知识,并将上下文做来支援计时器中断。预计要完成的行为是,我们在 osinit 首次设置计时器之後,在中断处理之後设置新的计时器,就此定时触发下去。

与先前展示的计时器中断时不同的是,这次我们确实储存并回复遭到中断时的执行状态,并且也提供足够的环境,让各式各样的例外与中断处理得以成为可能。

本节重点概念

  • RISC-V
    • 掌握 sscratch 控制暂存器
  • Golang
    • 在组合语言中使用资料结构

设置 stvecsscratch 控制暂存器

首先,在 osinit 函数之中,再加入两个控制暂存器的设置:

+// The entry of ethanol kernel
+func ethanol_trap()
+
 func osinit() {
        ncpu = 1
        getg().m.procid = 2
@@ -130,9 +135,11 @@ func osinit() {
        argc = ethanol.GetArgc()
        print("argc: ", argc, "\n")
 
+       ethanol.SetSscratch(uintptr(0))
        ethanol.TimerInterrupt(ON)
        ethanol.Interrupt(ON)
        ethanol.SetTimer()
+       ethanol.SetStvec(ethanol_trap)
 }

其中,两个函数的宣告分别放置在 src/runtime/ethanol/trap.s 档案中:

+
+// func SetSscratch(s uintptr)
+TEXT runtime∕ethanol·SetSscratch(SB), NOSPLIT|NOFRAME, $0-8
+       MOV     s+0(FP), A0
+       CSRRW   CSR_SSCRATCH, A0, ZERO
+       RET
+
+// func SetStvec(f func())
+TEXT runtime∕ethanol·SetStvec(SB), NOSPLIT|NOFRAME, $0-8
+       MOV     f+0(FP), A0
+       MOV     0(A0), A0
+       CSRRW   CSR_STVEC, A0, ZERO
+       RET

为了支援 CSR_SSCRATCH,也需要在 csr.h 当中新增定义才行。

值得注意的是 Golang 的 ABI 中,将函数作为参数传递的话,它的函数位址会位在第一个位址里面,所以在 SetStvec 多一行取值的动作。

这里我们直接将 sscratch 暂存器设为 0,因为笔者这里打算抄 Linux 维护 sscratch 的做法,那就是

  • 来自作业系统模式,则 sscratch 内容为 0。
  • 来自使用者模式,则 sscratch 内容为当前对应的核心执行绪。Linux 里面是型别为 struct task_struct 的指标,而笔者打算让 ethanol 存放当前共常式所属的执行绪(g.m)。

但使用者模式的实作,还不到时候。无论如何我们该把它设为零,因为还在作业系统模式就可能会发生计时器中断了。

等待计时器中断之处

我们延伸 newosproc 的部分的处理,

 //go:nowritebarrier
 func newosproc(mp *m) {
        for {
+               var i, j int
+               i = 16
+               j = 10000000
+               for i > 0 {
+                       print("i = ", i, "\n")
+                       for k := j*i + j; k > 0; k -= 1 {
+                       }
+                       i -= 1
+               }
        }
        panic("newosproc: not implemented")
 }

这个概念是忙等待(busy waiting),让 i = 16 ... i = 1 可以不断印出且周而复始的同时,等待计时器中断的来临。如果,上下文或是计时器中断没有妥善处理的话,都可以观测到卡住的状况。

中断进入点的 ethanol_trap

一开始的处理

与 Linux 较相似,

         ECALL
         RET
+
+// func ethanol_trap()
+TEXT runtime·ethanol_trap(SB),NOSPLIT|TOPFRAME,$0
+       CSRRW   CSR_SSCRATCH, g, g
+       BNE     g, ZERO, from_user
+from_kernel:
+       CSRRS   CSR_SSCRATCH, ZERO, g
+       MOV     (g_m)(g), g
+from_user:
+       MOV     T0, (m_mOS+mOS_tmp0)(g)
+       MOV     (m_gsignal)(g), T0
+       MOV     (g_stack+stack_hi)(T0), T0
+
+       ADDI    $-288, T0, T0
+       MOV     SP, 8(T0)
+       MOV     T0, SP
+       CSRRW   CSR_SSCRATCH, ZERO, g
+       MOV     g, 208(SP)
+       MOV     (g_m)(g), T0
+       MOV     (m_mOS+mOS_tmp0)(T0), T0

如果原先 sscratch 的内容不是 0,那表示来自使用者空间,那麽跳跃到 from_user 之後,这个取出来的执行绪(存放在 g),用来存取 m.tmp0 的偏移量,并将 t0 先存进去,好於後续挪用。

然後,运行完相当於 m.gsignal.stack.hi 的运算并存放到 t0 之後,这就是在中断处理当中,向原先设计用来处理非同步讯号的 gsignal 共常式借用它的堆叠空间。

之後,预留 288,也就是 36 个暂存器的空间。我们在这里先将旧的堆叠指标存放到指定的位址,这麽一来之後就能够正规的继续将 sp 当作堆叠指标来用。先前进入时,存在 sscratch 之中的 g 也应存取出来,并存放在堆叠中。最後的部分,则是将 t0 原先的内容提取回来。

回到最一开始,若是来自作业系统空间,只需将当时的执行绪(相当於g.m)提取出来,即可以和使用者空间运用一样的条件。

储存上下文

+       MOV     RA, 0x00(SP)
+       // skip the SP
+       MOV     GP, 0x10(SP)
+       MOV     TP, 0x18(SP)
+       MOV     T0, 0x20(SP)
+       MOV     T1, 0x28(SP)
+       MOV     T2, 0x30(SP)
+       MOV     S0, 0x38(SP)
+       MOV     S1, 0x40(SP)
+       MOV     A0, 0x48(SP)
+       MOV     A1, 0x50(SP)
+       MOV     A2, 0x58(SP)
+       MOV     A3, 0x60(SP)
+       MOV     A4, 0x68(SP)
+       MOV     A5, 0x70(SP)
+       MOV     A6, 0x78(SP)
+       MOV     A7, 0x80(SP)
+       MOV     S2, 0x88(SP)
+       MOV     S3, 0x90(SP)
+       MOV     S4, 0x98(SP)
+       MOV     S5, 0xA0(SP)
+       MOV     S6, 0xA8(SP)
+       MOV     S7, 0xB0(SP)
+       MOV     S8, 0xB8(SP)
+       MOV     S9, 0xC0(SP)
+       MOV     S10, 0xC8(SP)
+       // skip the g
+       MOV     T3, 0xD8(SP)
+       MOV     T4, 0xE0(SP)
+       MOV     T5, 0xE8(SP)
+       MOV     T6, 0xF0(SP)
+       MOV     T6, 0xF0(SP)
+       CSRRW   CSR_SCAUSE, ZERO, T6
+       MOV     T6, 0xFF(SP)
+       CSRRW   CSR_SEPC, ZERO, T6
+       MOV     T6, 0x100(SP)
+       CSRRW   CSR_STVAL, ZERO, T6
+       MOV     T6, 0x108(SP)

所有的通用暂存器都需储存之外,还有三组状态暂存器,先前我们只是在 dump 当中显示,但以正规的中断或例外处理来讲,这些都很有可能是必要的讯息。

回复上下文

+       MOV     0x100(SP), T6
+       CSRRW   CSR_SEPC, T6, ZERO
+       MOV     0x00(SP), RA
+       MOV     0x10(SP), GP
+       MOV     0x18(SP), TP
+       MOV     0x20(SP), T0
+       MOV     0x28(SP), T1
+       MOV     0x30(SP), T2
+       MOV     0x38(SP), S0
+       MOV     0x40(SP), S1
+       MOV     0x48(SP), A0
+       MOV     0x50(SP), A1
+       MOV     0x58(SP), A2
+       MOV     0x60(SP), A3
+       MOV     0x68(SP), A4
+       MOV     0x70(SP), A5
+       MOV     0x78(SP), A6
+       MOV     0x80(SP), A7
+       MOV     0x88(SP), S2
+       MOV     0x90(SP), S3
+       MOV     0x98(SP), S4
+       MOV     0xA0(SP), S5
+       MOV     0xA8(SP), S6
+       MOV     0xB0(SP), S7
+       MOV     0xB8(SP), S8
+       MOV     0xC0(SP), S9
+       MOV     0xC8(SP), S10
+       MOV     0xD0(SP), g
+       MOV     0xD8(SP), T3
+       MOV     0xE0(SP), T4
+       MOV     0xE8(SP), T5
+       MOV     0xF0(SP), T6
+       MOV     0x08(SP), SP
+       SRET

回复时的重点是 sepc,当然要记得先取回;又,需要小心不能毁了堆叠的位址,所以这里将 sp 留到最後。sret 指令就是回到中断或例外当时的位址去。

中间的部分:先处理时间中断

+
+       // TODO: setup g for the handling
+       CALL    runtime∕ethanol·SetTimer(SB)
+       // TODO: reset sscratch for user space

这里现在是待办事项多於实际完成事项的状态。理论上,在储存後到回复完这之间,我们也是可以撰写 Golang 档案来处理的。而若 Golang 档案会介入,那麽必然不可少了的就是当前共常式应该要安份的待在 g 暂存器(正式的 RISC-V 助忆符为 s11)里面。

又,完成中断或例外处理之前,若是准备回到使用者空间,也应该要将 sscratch 重新指定回当前的 g.m 执行绪才行。但这里也先留待後续实作了。

试跑

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

...
i = 14
i = 13
i = 12
i = 11
i = 10
i = 9
i = 8
i = 7
i = 6
i = 5
i = 4
i = 3
i = 2
i = 1
i = 16
i = 15
...

应该会有这样子一直印出来的效果才对喔!

事实上,这也是本系列开赛之时的进度,对应到 debut 分支之中。

ethanol_trap 当中可以使用以下方法破坏,以证明上下文的处理是有效的:

  1. 不回复或是毁掉 sepc。通常这麽做会导致 sret 之後跳跃到错误的地方,当然无法继续正常执行程序。
  2. t1 设为极大值。这是因为检视 newosproc 中的倒数计时的反组译码的话可以发现,非常大的机率下,被中断的都是倒数 k 变数的回圈的减一运算,该道指令使用的暂存器,在笔者这次的实验里,是 t1。如果将之设的极大,那麽回归回圈内之後,很有可能在倒扣结束之前下一次的计时器中断就又发生,那麽当然就无法继续印出了。

小结

予焦啦!今天我们用比较正规的方式完成了上下文处里与计时器中断处理。虽然只是综合概念应用,但 Golang 在这方面还是有诸多需要实验的部分。许多读者可能会疑惑笔者如何决定 ethanol_trap 函数的放置组件,为什麽是 runtime 而不是使用 runtime/ethanol

事实上,笔者本来打算将它放到 main 组件里面,再透过 go:linkname 这个编译器指令去连结,但踩到了诸多问题。最困难且尚未厘清的是,在 Golang 组合语言码当中,存取结构体内成员的好用功能(g_m),似乎只有在 runtime 当中可用。这也留待後续追踪项目吧。

无论如何,以计时器中断为目标的本章也在这里告一段落,我们也应该再迈向下一个目标了。各位读者,我们明天再会!


<<:  Day 16. 常见模板 Template App Apache / Nginx by Zabbix agent 介绍

>>:  Day16 RTCPeerConnection: Offer / Answer

资料分析成熟度模型(Data Analytics Maturity Model, DAMM)

资料平台的建构从基础设施建设开始,配合业务需求,以大数据技术作为战略的基石。 基础设施 包括硬体资源...

Promise

前言 不知道大家学习英语的时候有没有过明明语法规则都记清楚了,却还是不清楚实际如何运用的经验,或是只...

Date & time

上一篇在实作 EtaResponseMapper 的时候我们用了 Java 8 开始有的 Insta...

D27 - 压测工具go-ycsb

官网对於性能测试的描述,分别提供了sysbench以及TPC-C的测试数据。在v5.2的改版後,也对...

【Day 28】情境模拟:如何沟通有互动效果的介面 !?

与工程师沟通有互动效果的介面要注意什麽 !? 互动效果相较静态的设计稿实作时间比较久、建置成本高,需...