本節是以 Golang 上游 8854368cb076ea9a2b71c8b3c8f675a8e19b751c 為基準做的實驗
予焦啦!今天我們就來驗收前兩天的綜合知識,並將上下文做來支援計時器中斷。預計要完成的行為是,我們在 osinit
首次設置計時器之後,在中斷處理之後設置新的計時器,就此定時觸發下去。
與先前展示的計時器中斷時不同的是,這次我們確實儲存並回復遭到中斷時的執行狀態,並且也提供足夠的環境,讓各式各樣的例外與中斷處理得以成為可能。
stvec
與 sscratch
控制暫存器首先,在 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
當中可以使用以下方法破壞,以證明上下文的處理是有效的:
sepc
。通常這麼做會導致 sret
之後跳躍到錯誤的地方,當然無法繼續正常執行程式。t1
設為極大值。這是因為檢視 newosproc
中的倒數計時的反組譯碼的話可以發現,非常大的機率下,被中斷的都是倒數 k
變數的迴圈的減一運算,該道指令使用的暫存器,在筆者這次的實驗裡,是 t1
。如果將之設的極大,那麼回歸迴圈內之後,很有可能在倒扣結束之前下一次的計時器中斷就又發生,那麼當然就無法繼續印出了。予焦啦!今天我們用比較正規的方式完成了上下文處裡與計時器中斷處理。雖然只是綜合概念應用,但 Golang 在這方面還是有諸多需要實驗的部分。許多讀者可能會疑惑筆者如何決定 ethanol_trap
函數的放置組件,為什麼是 runtime
而不是使用 runtime/ethanol
?
事實上,筆者本來打算將它放到 main
組件裡面,再透過 go:linkname
這個編譯器指令去連結,但踩到了諸多問題。最困難且尚未釐清的是,在 Golang 組合語言碼當中,存取結構體內成員的好用功能(g_m
),似乎只有在 runtime
當中可用。這也留待後續追蹤項目吧。
無論如何,以計時器中斷為目標的本章也在這裡告一段落,我們也應該再邁向下一個目標了。各位讀者,我們明天再會!