iT邦幫忙

2021 iThome 鐵人賽

DAY 21
1
Software Development

予焦啦!Hoddarla 專案起步:使用 Golang 撰寫 RISC-V 作業系統的初步探索系列 第 21

予焦啦!實作上下文機制

本節是以 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 當中可用。這也留待後續追蹤項目吧。

無論如何,以計時器中斷為目標的本章也在這裡告一段落,我們也應該再邁向下一個目標了。各位讀者,我們明天再會!


上一篇
予焦啦!Golang 當中的訊號(signal)機制
下一篇
予焦啦!Golang 執行緒與作業系統執行緒
系列文
予焦啦!Hoddarla 專案起步:使用 Golang 撰寫 RISC-V 作業系統的初步探索33

尚未有邦友留言

立即登入留言