本節是以 Golang 上游 7ee4c1665477c6cf574cb9128deaf9d00906c69f 為基準做的實驗
予焦啦!昨天我們終於抵達了 Hello World
,並也看到了它的效應,但花了更多時間理解並紀錄 UART 提供給軟體的程式設計。藉由監控運行在 QEMU 上的 Linux 的串列裝置驅動程式(serial device drive)行為,我們有一套範本,可以用來處理 UART 輸入的中斷。
所以,今天我們就拿起這把新武器,挑戰一下命令列吧。
fmt.Scanf
函數前兩天埋首於程式碼中閱讀,並使用 GDB 來回尋訪 Linux 處理外部中斷的一整組行為,就是為了接下來的實作。實作分為兩部分,首先我們會先刻出一個可以處理外部中斷的基本外殼,確保真的能夠取得 UART 輸入之後,再填補如何應對輸入的部分。有了這兩個部分組合在一起,要組成一個類似命令列的使用者互動環境,也就不是難事。
又,這幾天的節奏都很快,今天筆者也沒有減速的意思。確實有很多技術債、很多以長遠發展來看應該要好好考慮的做法,但發展至今,先以階段性目標優先。
更直接地說就是,理想上 ethanol 核心應該要能夠好好地解析裝置樹,從中取得 PLIC 與 UART 的相關資訊再加以使用,但這裡筆者會先略過那些部分,而以編譯時可決定的常數代替裝置資訊。還有,為了簡化 PLIC 設定時的 context 選擇,筆者決定移除 QEMU 啟動時的 smp
選項,如此一來整個系統就是單純的單核心組態。
runtime
組件部分首先,比照我們當初啟用的計時器中斷,我們也在 osinit
函數裡面啟用外部中斷:
ethanol.TimerInterrupt(ON)
+ ethanol.ExternalInterrupt(ON)
ethanol.Interrupt(ON)
在 src/runtime/ethanol/trap.go
與 trap.s
當中,當然也要有相對應的處置;與計時器中斷時的做法九成像,這裡就不再佔版面。總之,這裡會將 sie
控制暫存器的值,由原先只有啟用計時器中斷的 0x20
,增加為也啟用了外部中斷的 0x220
。
另外,在負責分流中斷與例外的 ethanol_trap1
函數裡面,我們也新增一個外部中斷的區域:
case TIMER_INTERRUPT:
ethanol.SetTimer()
_g_.m.life -= 1
+ case EXTERNAL_INTERRUPT:
+ throw("external!!!")
default:
throw("cannot handle other traps now")
main
組件部分當然,只有這樣是沒有用的。我們經過前兩日的研究,已經知道,沒有 PLIC 中的設定與真正外部裝置的設定,即使 CPU 單方面啟用外部中斷,要是中斷進不來,也不會有任何作用。
所以這裡筆者在 ethanol/main.go
裡加入一些程式碼,順便也試試看一些比較像 Golang 的寫法吧。
main
說起 func main() {
- fmt.Println("Hello World!")
+ uart := uart{device{0xfffffff000000000, 0x10000000}}
+ plic := plic{device{0xfffffff000200000, 0xc000000}}
+
+ alld := make([]driver, 2)
+ alld[0] = uart
+ alld[1] = plic
+
+ for _, d := range alld {
+ d.deviceInit()
+ }
+
+ fmt.Printf("hdla > ")
+ for {
+ }
...
將 PLIC 與 UART 都做成 device
,也就是裝置物件。裝置物件有兩個成員變數,分別是 pa
,也就是對應到第 2 個參數的裝置物理位址,另一個則是作業系統模式在虛擬記憶體啟用的狀態底下能夠使用的虛擬位址 va
。之所以使用這兩個成員,是因為我們需要將這兩個位址之間的轉換建立起來才行。
再次,這裡也是技術債。能夠使用的虛擬記憶體位址是需要好好管裡的,這裡貿然決定使用的這兩個位址,是筆者先行考察確定不會與他者衝突的虛擬位址。
接下來製作 alld
,其型別為 driver
的切片(slice),包含先前宣告的兩個裝置物件。之所以 driver
型別能夠容納這兩個物件,是因為 driver
是一個介面型別(interface type),而之後我們會看到 plic
和 uart
物件都實作了 driver
物件要求的函數,所以這裡可以如此賦值。
最後的迴圈就是走訪 alld
,並執行 driver
介面當中的 deviceInit
函數。最後就是印出一個命令列提示 hdla >
,然後卡在無窮迴圈裡面,否則整個系統本身就會馬上結束了。
上面提到了 4 種型別,是先前都沒有出現過的,分別定義如下:
+type driver interface {
+ deviceInit()
+ write(off, val uintptr)
+ read(off uintptr) uintptr
+}
+
+type device struct {
+ va uintptr
+ pa uintptr
+}
+
+type uart struct {
+ device
+}
+
+type plic struct {
+ device
+}
如引用所示,uart
與 plic
裝置物件都只包含 device
作為一個匿名成員。這約略可以看成 Golang 版的繼承。這裡的做法可以理解為不涉及到函數方法的繼承。
兩種結構使用的 deviceInit
當然必須有所不同,如下所示:
+func (u uart) deviceInit() {
+ ethanol.MemoryMap(u.va, u.pa, 0x1000)
+ // Setup IER
+ u.write(uintptr(0x1), uintptr(1))
+}
+
+func (p plic) deviceInit() {
+ // Setup mapping
+ ethanol.MemoryMap(p.va, p.pa, 0x200000)
+ ethanol.MemoryMap(p.va+0x200000, p.pa+0x200000, 0x200000)
+ // Priority of UART (10)
+ p.write(uintptr(0x28), uintptr(1))
+ // Priority threshold of context 1
+ p.write(uintptr(0x201000), uintptr(0))
+ // Enable bit of UART (10) for context 1
+ p.write(uintptr(0x2080), uintptr(0x400))
+}
由於虛擬到實體的位址對應已經在
src/runtime/ethanol
裡面做好了,所以為了引用MemoryMap
函數,必須要在 Golang 檔案的開頭處引用runtime/ethanol
,這裡省略。
UART 的狀態比較單純。只需要在中斷啟用暫存器(偏移量為 1
)的地方設置第 0 個位元,則可以啟動接收字元的中斷。這裡呼叫了寫入函數,後續我們會在看到其中的細節。
PLIC 就較複雜些。就算作業系統的單核心只需要處理 context 1,根據規格,也仍然會使用到超過 2MB 的映射。所以這裡我們對應了兩組記憶體虛擬頁面,然後進行三組寫入的設定。前兩者是關於中斷優先權的設定,我們就比照正常運行時的 Linux 系統,將允許的閾值設為 0,並將 UART 所代表的優先權設為 1。最後一筆,則是啟用 UART 裝置作為一個外部中斷的來源。
至於這兩種裝置的 write
函數分別為何,
+func (u uart) write(off, val uintptr) {
+ writeb(u.va, off, uint8(val))
+}
+
+func (p plic) write(off, val uintptr) {
+ writew(p.va, off, uint32(val))
+}
它們使用的,具有型別資訊的 writex
函數,可參考這次新增的 ethanol/helper.s
檔案,
// func writeb(addr, offset uintptr, val uint8)
TEXT main·writeb(SB), NOSPLIT|NOFRAME, $0-17
MOV addr+0(FP), A0
MOV offset+8(FP), A1
MOVBU val+16(FP), A2
ADD A0, A1, A0
MOVB A2, 0(A0)
RET
// func writew(addr, offset uintptr, val uint32)
TEXT main·writew(SB), NOSPLIT|NOFRAME, $0-20
MOV addr+0(FP), A0
MOV offset+8(FP), A1
MOVWU val+16(FP), A2
ADD A0, A1, A0
MOVW A2, 0(A0)
RET
需注意的是邏輯上,這種寫入 MMIO 區域的動作,在 RISC-V 裡面都需要
fence
指令,使得輸入輸出與記憶體讀寫的順序符合預期,但這個部分筆者也先略過了。當然,邏輯上是有瑕疵的。
在 main
函數執行到底,印出 hdla >
的提示字元之後,隨意敲擊一個鍵,就會看到我們的外部中斷大獲成功:
Boot HART MEDELEG : 0x000000000000b109
Memory Base: 0x80000000
Memory Size: 0x40000000
hdla > fatal error: external!!!
goroutine 0 [idle]:
runtime.throw({0xffffffc000098a58, 0xb})
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/panic.go:965 +0x60
runtime.ethanol_trap1(0xffffffcf04037ee0)
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/os_opensbi.go:314 +0xdc
runtime.ethanol_trap()
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/sys_opensbi_riscv64.s:78 +0xe4
goroutine 1 [running]:
goroutine running on other thread; stack unavailable
處理輸入與輸出大不相同的地方在於,後者是為人所熟悉的(四處都有 Hello World)、主動的、同步的行為,前者則是被動的非同步行為,並且實際上橫跨許多抽象層,才能夠成功獲得輸入的結果。
以一般的 read
系統呼叫來講,很有可能的情境是,作業系統讓進行該系統呼叫的使用者執行緒進入睡眠模式,然後自己就做別的事去了。之後,作業系統一如往常地處理各種裝置中斷時,發現某個來自鍵盤的中斷,於是進入到鍵盤的驅動程式去處理。相關的資料則被儲存在某個緩衝區,等待其他核心執行緒接手,發現該資料應該要接到某個虛擬終端(pseudo terminal),於是就進行這個嫁接;而後也許就叫醒正在該終端工作的、正在睡眠狀態的執行緒,於是一個 read
呼叫才取得這個資料。
無論如何,我們現在就是要試著把簡單版本的這個過程做進來。和昨日我們印出的 write
函數相對的,我們一樣在 src/os/file_opensbi.go
裡面可以找到一個還沒有實作東西的 read
函數。
fmt.Scanf
到 read
的連通性試試將之插入一個 panic
呼叫在 read
函數中,然後在 main.main
主函數中呼叫 fmt.Scanf
,分別像這樣:
func (f *File) read(b []byte) (n int, err error) {
+ panic("???")
return 0, nil
以及,
func main() {
- fmt.Println("Hello World!")
+ var s string
+ fmt.Scanf("%s", &s)
+ fmt.Println(s)
重新編譯並執行後,確實可以跑出錯誤的回溯。這裡就先省略了。除了證明這兩者的連通性,也能夠玩個小遊戲來讓這個 Scanf
回傳字串到 s
裡面,然後再透過後續的 Println
函數印出。遊戲性地修改 read
函數如下:
+var countdown = 6
+
func (f *File) read(b []byte) (n int, err error) {
- return 0, nil
+ switch countdown {
+ case 5:
+ b[0] = 'H'
+ case 4:
+ b[0] = 'e'
+ case 3:
+ b[0] = 'l'
+ case 2:
+ b[0] = 'l'
+ case 1:
+ b[0] = 'o'
+ case 0:
+ b[0] = ' '
+ }
+ countdown = countdown - 1
+ return 1, nil
}
重新編譯後應該就可以看到,在 main
函數當中已經可以印出 Hello
字樣。雖然筆者中間省略了很多介紹,但憑藉著我們對於這兩種介面的認識,就可以理解這麼作能夠成功的原因。fmt.Scanf
給定格式字串 %s
的行為是,遭遇到廣義的空白字元之前,都繼續累積下來;遭遇到之後,則回傳。經過層層累積抵達的最後在 file
組件內的 read
函數,其行為則是回傳所取得的字元數量,並將那些字元存放在傳入的 b
字元切片中。所以這裡隨著一個倒數變數,逐步將 Hello
存入,上層會一直呼叫進來直到遭遇我們最後傳入的空白字元。
上一小節只是展示我們對於 Golang 的 fmt
組件輸入方法的基本理解,接下來是該擬定作戰計畫了。讓我們試著達到這個流程吧:
main
函數裡面跑一個簡單的無窮迴圈,包著 fmt.Scanf
函數,試圖不斷取得使用者的輸入。搭配適當的輸出,應該可以讓它看起來很像命令列。ethanol_trap1
裡面,如先前的小節所描述的,已經能夠感應到外部中斷事件。回顧昨日的 UART 行為紀錄,我們合理地處理(包含宣告(claim)與完成(complete))中斷事件,以確保 PLIC 和 UART 的狀態都維持正常而能夠持續接受使用者輸入。THR
去,這麼一來使用者就能看到自己的輸入。10
寫回去,就能夠通知 PLIC 說,UART 裝置的這個中斷已經被解決了。所以 PLIC 應該也不是問題。3.
步驟中取得的字元,傳遞給 1.
步驟中的 read
函數。fmt.Scanf
回傳之後,針對取得的字串判斷處理一下,大致上就有個類似命令列的東西了。筆者不確定各位讀者閱讀至此覺得哪一個最具挑戰性,但筆者認為是 6.
。這個字元到底該怎麼跨越執行緒傳遞過去呢?先把簡單的都打通,最後再來處理這個部份好了。
1.
與 7.
的命令列邏輯與一般的 Golang 程式沒什麼兩樣,
+ fmt.Printf("hdla > ")
+ for {
+ var s string
+ fmt.Scanf("%s", &s)
+ fmt.Printf("\n")
+ if s == "exit" {
+ os.Exit(0)
+ } else if s == "cheers" {
+ fmt.Println("Hoddarla!")
+ fmt.Println("Hoddarla is a OS project powered by RISC-V and Golang.")
+ }
+ fmt.Printf("hdla > ")
+ }
讓 exit
命令能夠相當於離開整個程式,且定義 cheers
命令讓它輸出訊息。沒有什麼大不了的。
2.
、3.
、4.
、5.
的中斷處理邏輯讓人煩惱的是,ethanol_trap1
身在 runtime
組件,而想要控制的 plic
與 uart
都是 main
組件裡面才定義的 device
結構且實作了 driver
介面。如果可以的話,在 main
組件裡面執行簡單得多。
所以這裡我們借用 Golang 的 linkname
命令,讓外部中斷處理轉一手到 main
裡面的 eisr
函數處理。之所以這樣命名,是取外部中斷處理常式(External Interrupt Service Routine)。我們先如此改寫 ethanol_trap1
:
+//go:linkname eisr main.eisr
+func eisr(ctxt *rv64ctxt)
+
//go:nosplit
func ethanol_trap1(ctxt *rv64ctxt) {
_g_ := getg()
...
case TIMER_INTERRUPT:
ethanol.SetTimer()
_g_.m.life -= 1
+ case EXTERNAL_INTERRUPT:
+ print("user input")
+ fn := eisr
+ fn(ctxt)
default:
在中斷的情況下,先取得 eisr
作為一個函數指標,在予以呼叫。這迂迴的寫法在正常的 Golang 裡面也會使用到。因為 runtime
組件是其它組件的根源相依組件,所以它若要呼叫其它組件的函數,則使用這樣的作法,能夠讓連結器有所準備。
回到 ethanol/main.go
的部份,當然就是必須新增 eisr
函數:
+var plic0 plic
+var uart0 uart
+
+func eisr(c uintptr) {
+ pp := uint32(plic0.read(uintptr(0x201004)))
+ up := uint8(uart0.read(uintptr(0x0)))
+ uart0.write(uintptr(0x0), uintptr(up))
+ plic0.write(uintptr(0x201004), uintptr(pp))
+}
func main() {
- uart := uart{device{0xfffffff000000000, 0x10000000}}
- plic := plic{device{0xfffffff000200000, 0xc000000}}
+ uart0 = uart{device{0xfffffff000000000, 0x10000000}}
+ plic0 = plic{device{0xfffffff000200000, 0xc000000}}
...
再欠個技術債吧,這裡我們讓兩個裝置成為全域的,好讓 main
與 eisr
都能夠拿到。在 eisr
當中,第一行與最後一行是 PLIC 的宣告與完成。第二行是讀取接收暫存器;理論上,驅動程式的標準行為應該是要先檢查 UART 本身擱置的中斷類型為何,但我們只有啟用一種中斷,所以就連那一步都先省略了。第三行寫回偏移量 0 的位置,是因為直接寫的時候代表輸出。
執行看看的話,可以發現,輸入字元和 user input
字樣都會一併顯示出來。但是由於這些字元都沒有經過 fmt.Scanf
的過程,所以我們的命令列提示字元 hdla >
的部份都沒有動靜。應該會有類似下圖的操作結果(輸入 exit
與四個空格):
Memory Base: 0x80000000
Memory Size: 0x20000000
hdla > user input
euser input
xuser input
iuser input
tuser input
user input
user input
user input
6.
使字元穿透組件所以我們只剩下最後一個關卡。既然都用 Golang 來做這個專案了,那麼就試試看 Golang 的經典同步機制:頻道(channel)吧!
我們可以讓 read
在頻道的等待端,等待著接收一個字元;另一方面,在 eisr
裡面則是設定一個頻道的傳輸端,使字元穿透過去。首先是在 read
函數的新增:
+var UartChannel chan byte
+
func (f *File) read(b []byte) (n int, err error) {
- return 0, nil
+ b[0] = <-UartChannel
+ return 1, nil
}
然後是 main
組件內的一些新增:
func eisr(c uintptr) {
pp := uint32(plic0.read(uintptr(0x201004)))
up := uint8(uart0.read(uintptr(0x0)))
+ os.UartChannel <- byte(up)
uart0.write(uintptr(0x0), uintptr(up))
plic0.write(uintptr(0x201004), uintptr(pp))
}
func main() {
...
alld[0] = uart0
alld[1] = plic0
+ os.UartChannel = make(chan byte)
看起來很棒吧!但是編譯之後,試跑下去,還來不及等到命令列出現以進行使用者輸入的實驗,就會遇到錯誤並有以下錯誤回溯訊息:
Memory Base: 0x80000000
Memory Size: 0x20000000
hdla > fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
os.(*File).read(...)
/home/noner/FOSS/hoddarla/Hoddarla/go/src/os/file_opensbi.go:74
os.(*File).Read(0x0, {0xffffffcf0000e1d0, 0x1, 0x4})
/home/noner/FOSS/hoddarla/Hoddarla/go/src/os/file.go:119 +0x40
...
fmt.Scanf(...)
/home/noner/FOSS/hoddarla/Hoddarla/go/src/fmt/scan.go:81
main.main()
/home/noner/FOSS/hoddarla/Hoddarla/ethanol/main.go:98 +0x264
原來是因為,主執行緒順著執行,一路到了 read
函數內,就會因為這個等待頻道的傳輸而被阻塞(blocking)。Golang 執行期進而判斷,已經沒有其它共常式可以排程,那就相當於這整個程式已經不可能再有狀態的改變了;所有的共常式都在睡眠狀態,這是死結(deadlock)!
好吧,那麼我們有沒有可能把它繞過去?這樣看起來應該硬是排一個共常式給它就可以了吧?
...
for _, d := range alld {
d.deviceInit()
}
+
+ go func() {
+ for {
+ }
+ }()
+
fmt.Printf("hdla > ")
...
重新編譯是可以執行,也不會出現死結的錯誤,但是一旦給了輸入事件的中斷,根據行為分類,有兩種(或以上)不同的錯誤;但共同點都是,我們可以看到:
fatal error: unexpected exception
而這是我們加在 ethanol_trap1
裡面的。目前我們處理了計時器中斷與外部中斷,但所有例外的部份都沒有處置,只有 default
預設印出這個訊息,通知使用者發生了一個非預期的例外。我們使用 GDB 停在這裡看看,可以發現是 runtime.runqput
函數裡面,需要取用 _p_
的時候出的問題。
我們在這整個系列文裡面,大量地遭遇過 Golang 當中的抽象物件共常式(g
)與執行緒(m
),但是一直都沒有提過處理器資源(p
)。筆者自己其實也還不太清楚這個抽象物件的真正功能,但可以確定的是,有時候執行緒會需要擁有處理器資源才能夠進行某些操作,這也是我們現在遭遇的問題之一。
作為參考,我們啟用 Golang 的除錯環境變數(GODEBUG=schedtrace=1,scheddetail=1
)當作核心參數,再執行一遍:
SCHED 116ms: gomaxprocs=1 idleprocs=0 threads=3 spinningthreads=0 idlethreads=1 runqueue=0 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0
P0: status=1 schedtick=4 syscalltick=0 m=0 runqsize=0 gfreecnt=0 timerslen=0
M2: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
M1: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=2 dying=0 spinning=false blocked=false lockedg=-1
M0: p=0 curg=5 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
G1: status=4(chan receive) m=-1 lockedm=-1
G2: status=4(force gc (idle)) m=-1 lockedm=-1
G3: status=4(GC sweep wait) m=-1 lockedm=-1
G4: status=4(GC scavenge wait) m=-1 lockedm=-1
G5: status=2() m=0 lockedm=-1
我們可以看到,現在的 Hoddarla/ethanol 核心,只有一個處理器資源,但有三個執行緒。擁有處理器資源的只有 M0
而已。萬一使用者給輸入的時候,當時被中斷的執行緒剛好不是 M0
的話,不幸的是,它為了處理我們前前一小節加入的頻道通訊,而會觸發某些需要用到處理器資源的場景。也就是說,我們在先前處理上下文的時候,機關算盡,還挪用了 ethanol 不會用到的 gsignal
成員,但如果當初中斷的執行緒不對,仍然會遇到現在的問題。
這個其實頗為難解,這裡先用另外一個方法繞過去。概念很簡單:如果進來的是外部中斷,就先看看自己是不是 m0
,如果不是,就做上下文交換。由於這個外部中斷沒有處理,所以交換完之後回到中斷啟用模式的時候,這個外部中斷又會立刻觸發。直到被中斷的執行緒確實是 m0
,理論上就可以繞過這個問題。
m0
處理外部中斷...
case TIMER_INTERRUPT:
ethanol.SetTimer()
_g_.m.life -= 1
case EXTERNAL_INTERRUPT:
print("user input\n")
- fn := eisr
- fn(ctxt)
+ if _g_.m != &m0 {
+ _g_.m.life = 0
+ } else {
+ fn := eisr
+ fn(ctxt)
+ }
default:
...
default:
+ print("sepc: ", unsafe.Pointer(uintptr(ctxt.sepc)), "\n")
+ print("scause: ", unsafe.Pointer(uintptr(ctxt.scause)), "\n")
+ print("stval: ", unsafe.Pointer(uintptr(ctxt.stval)), "\n")
throw("unexpected exception")
重新編譯並執行!結果還是出現了錯誤,這個頁表錯誤(0xd
代表的是讀取時的頁表錯誤),是錯誤中的錯誤(panic in panic);根據錯誤訊息,問題是在更早的 fatal error: malloc during signal
錯誤。這個發生在 src/runtime/malloc.go
裡面的 mallocgc
函數,使用 GDB 可以觀察到這個回溯:
Breakpoint 2, runtime.mallocgc (size=88, typ=0xffffffc00009f740, needzero=true, ~r0=<optimized out>)
at /home/noner/FOSS/hoddarla/Hoddarla/go/src/runtime/malloc.go:971
971 throw("malloc during signal")
(gdb) bt
#0 runtime.mallocgc (size=88, typ=0xffffffc00009f740, needzero=true, ~r0=<optimized out>)
at /home/noner/FOSS/hoddarla/Hoddarla/go/src/runtime/malloc.go:971
#1 0xffffffc00000bf24 in runtime.newobject (typ=0xffffffc00009f740, ~r0=<optimized out>)
at /home/noner/FOSS/hoddarla/Hoddarla/go/src/runtime/malloc.go:1221
#2 0xffffffc000033920 in runtime.acquireSudog (~r0=<optimized out>) at /home/noner/FOSS/hoddarla/Hoddarla/go/src/runtime/proc.go:405
#3 0xffffffc000004a3c in runtime.chansend (c=0xffffffcf00050060, ep=0xffffffcf00009ea3, block=true, callerpc=<optimized out>,
~r0=<optimized out>) at /home/noner/FOSS/hoddarla/Hoddarla/go/src/runtime/chan.go:238
#4 0xffffffc0000047d0 in runtime.chansend1 (c=0xffffffcf00050060, elem=0xffffffcf00009ea3)
at /home/noner/FOSS/hoddarla/Hoddarla/go/src/runtime/chan.go:144
#5 0xffffffc00008ca1c in main.eisr (c=<optimized out>) at /home/noner/FOSS/hoddarla/Hoddarla/ethanol/main.go:76
#6 0xffffffc00002e554 in runtime.ethanol_trap1 (ctxt=0xffffffcf00009ee0)
at /home/noner/FOSS/hoddarla/Hoddarla/go/src/runtime/os_opensbi.go:317
#7 0xffffffc000059b2c in runtime.ethanol_trap () at /home/noner/FOSS/hoddarla/Hoddarla/go/src/runtime/sys_opensbi_riscv64.s:78
這個歷程是,eisr
試圖傳送 UART 當中的那個字元,而 Golang 幫我們把頻道操作轉換成 chansend*
系列呼叫。在 acquireSudog
裡面,符合了某些條件之後,它會試圖創建一個新的物件。而又因為我們現在處在 m0.gsignal
共常式裡面,就會觸發記憶體配置控制裡面的判斷,認定我們在訊號處理當中不該額外配置記憶體。
前後搜尋可以突破的線索,發現了這個(在 chansend1
函數中):
231 if !block {
232 unlock(&c.lock)
233 return false
234 }
235
236 // Block on the channel. Some receiver will complete our operation for us.
237 gp := getg()
238 mysg := acquireSudog()
這裡,阻塞與否的判斷顯然是個很重要的關鍵字。在正式進入 acquireSudog
之前,甚至還有一個控制區塊是,如果是非阻塞模式就會回傳的狀態。筆者先前其實並不是一個很有經驗的 Golang 使用者,所以其實在這之前都沒有考慮過要使用非阻塞式的頻道。
如果我們定性分析一下現在的狀況,會發現確實有道理。如果是非阻塞式頻道的傳輸端,假設頻道上限還未達到的情況,沒有道理需要當前共常式進入睡眠狀態。總之,將這個頻道改為非阻塞式試試。
ByteChannel
為非阻塞頻道 alld[1] = plic0
- os.UartChannel = make(chan byte)
+ os.UartChannel = make(chan byte, 1)
for _, d := range alld {
感謝 Golang 的簡潔語法,我們可以新增一個參數,使它的性質變為非阻塞。重新編譯之後,無奈,還是會再遇到問題:
Memory Base: 0x80000000
Memory Size: 0x20000000
hdla > asfatal error: malloc during signal
goroutine 0 [idle]:
main.eisr(0xffffffcf00009ee0)
/home/noner/FOSS/hoddarla/Hoddarla/ethanol/main.go:76 +0x84
...
還是一樣是 malloc during signal
!使用 GDB 觀察,發現
(gdb) bt
#0 runtime.mallocgc (size=88, typ=0xffffffc00009f740, needzero=true, ~r0=<optimized out>)
at /home/noner/FOSS/hoddarla/Hoddarla/go/src/runtime/malloc.go:971
#1 0xffffffc00000bf24 in runtime.newobject (typ=0xffffffc00009f740, ~r0=<optimized out>) at /home/noner/FOSS/hoddarla/Hoddarla/go/src/runtime/malloc.go:1221
#2 0xffffffc0000338f8 in runtime.acquireSudog (~r0=<optimized out>) at /home/noner/FOSS/hoddarla/Hoddarla/go/src/runtime/proc.go:405
#3 0xffffffc000004a3c in runtime.chansend (c=0xffffffcf00042070, ep=0xffffffcf00009ea3, block=true, callerpc=<optimized out>,
~r0=<optimized out>) at /home/noner/FOSS/hoddarla/Hoddarla/go/src/runtime/chan.go:238
#4 0xffffffc0000047d0 in runtime.chansend1 (c=0xffffffcf00042070, elem=0xffffffcf00009ea3)
at /home/noner/FOSS/hoddarla/Hoddarla/go/src/runtime/chan.go:144
#5 0xffffffc00008c9f4 in main.eisr (c=<optimized out>) at /home/noner/FOSS/hoddarla/Hoddarla/ethanol/main.go:76
...
特別值得玩味的是,chansend
函數的參數裡面,block
竟然為真。我們難道不是已經宣告成非阻塞頻道了嗎?而且筆者輸入的時候,還特地間隔較久的時間,就是不希望輸入太快打壞了僅有一個字元容量的頻道。
事實上,這可能表示接收端沒有成功接收這個頻道的訊息。共常式的排程不像執行緒的排程,在我們沒有非同步搶佔(asynchrous preemption)支援的情況下,如果有些共常式很自私的話,很有可能其它的共常式沒有辦法被排程回來。比方說我們前幾個小節加入的無窮迴圈。
正確的作法應該是讓那個無窮迴圈共常式有辦法讓出執行資源,也就是主動觸發執行緒內的共常式排程。這個不需要我們額外再設計什麼機制,只要套用原本就有的 runtime.Gosched
函數就可以了。
...
d.deviceInit()
}
go func() {
for {
+ runtime.Gosched()
}
}()
fmt.Printf("hdla > ")
...
然後再執行(老樣子,在 Hoddarla repo)
:
筆者稍早推上 github 的版本沒有包含到 patch,慚愧!再補上一張執行動圖。
如圖:
予焦啦!這就是這屆鐵人賽,Hoddarla 這個專案初次亮相的所有技術內容。在最後一章裡面,我們探索了 RISC-V 的外部中斷機制,然後專攻 UART 的輸入處理。今天,我們綜合前兩天的知識,也順手使用了 Golang 的介面功能與頻道同步機制,雖然過程中吃了不少苦頭,但也逐步分析並解決了。筆者很慶幸現在能夠達成這樣的階段性目標。
但當然,三十天的挑戰尚未結束。筆者相信今天以前,這系列的技術內容已經相當足夠,實際上也留下很多技術債,若是要進一步成熟化是不可忽視的項目。不過最後收尾的這幾天,且讓筆者挪用為附錄,分享一些與 Hoddarla 專案本身弱相關,但與 RISC-V 或是 Golang 或是作業系統主題相關的資訊。各位讀者,我們明日再會!