本节是以 Golang 上游 6a79f358069195e1cddb821e81fab956d9a0c7d1 为基准做的实验
予焦啦!昨日我们观察了 Golang 执行绪(以下以 M
代称)在被系统呼叫创建真实的执行绪之前後,於 Golang 执行期分别做了哪些事情。今日我们就来试着将执行绪与基本的排程直接实作进来。
昨天的两个范例里面,首先 freebsd/arm64 系统组合的 thr_new
我们不好参考。它太方便了,能够让新的执行绪在回到使用者空间的时候直接从指定的函数开始。我们(opensbi/riscv64)无论如何不可能做到这件事情。
那麽,linux/riscv64 系统组合的 clone
又如何?在组合语言实作的 clone
包装函数之中,父执行绪在系统呼叫之前,会帮忙将重要的资讯先行塞到即将被创造的子执行绪的堆叠之中。虽然 clone
的服务没有那麽周到,但至少堆叠是设定好的,所以回到使用者空间之後,也立刻就能够使用堆叠当中的内容。
笔者这里的判断是,我们就让 newosproc
没有什麽真正的作用吧。反正没有任何底层的系统能够提供我们这种服务。可是,Golang 本身在 newm
以降的过程当中,早已将新的 M
设立完毕,并将之置放到全执行绪列表的 allm
里面了。
但是,执行绪间的并行(concurrency)仍然应该要设法解决,否则整个 Golang 执行期就只会是 m0
的千里独行。是的,所以笔者打算正式来实作上下文交换了。
目前的 ethanol_trap
只处理一种中断,也就是前一章安置的时间中断。而且若是读者诸君仔细观察不难发现,现在的写法非常粗暴:
// func ethanol_trap()
TEXT runtime·ethanol_trap(SB),NOSPLIT|TOPFRAME,$0
...
CSRRW CSR_SCAUSE, ZERO, T6
MOV T6, 0xF8(SP)
CSRRW CSR_SEPC, ZERO, T6
MOV T6, 0x100(SP)
CSRRW CSR_STVAL, ZERO, T6
MOV T6, 0x108(SP)
// TODO: setup g for the handling
CALL runtime∕ethanol·SetTimer(SB)
// TODO: reset sscratch for user space
MOV 0x100(SP), T6
CSRRW CSR_SEPC, T6, ZERO
...
在进入後储存上下文的阶段,与离开前回复上下文的阶段两者之间,只有一个呼叫,用来设定下一次的计时器中断。以两个层面来说,这麽做是不够的:
stvec
的陷阱处理并非只有计时器中断。还有其他种类的中断或是例外状况,理论上也都会进入这里。ethanol 核心总是会渐渐增加功能,所以这里也至少该引入针对 scause
判断的逻辑才是。以下,就先设法将这两件事情都实作为能够满足的形式吧。
newosproc
虽说是清空,但考量到 linux/riscv64 的 clone
包装函数(wrapper function)里面有一个叙述,使用 linux 核心的 gettid
系统呼叫,取得作业系统核心内的执行绪 ID,以赋值与 m.procid
。这里我们也没有相对应的系统服务可用,但若要简单赋予 Golang 执行绪之间可区分的 ID,笔者这里先直接将之令为 mp
本身:
func newosproc(mp *m) {
- for {
- var i, j int
- i = 16
...
+ // Do nothing.
+ // mp is already registered in allm, so we can switch to it
+ // later anyway.
+ mp.procid = uint64(uintptr(unsafe.Pointer(mp)))
- panic("newosproc: not implemented")
}
若是执行这个简单的修改版,则会遭遇新的错误:
fatal error: nanotime returning zero
goroutine 1 [running, locked to thread]:
runtime.throw({0xffffffc0000612e7, 0x17})
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/panic.go:965 +0x60 fp=0xffffffcf040267a8 sp=0xffffffcf04026780 pc=0xffffffc00002ec70
runtime.main()
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/proc.go:196 +0x11c fp=0xffffffcf040267d8 sp=0xffffffcf040267a8 pc=0xffffffc000030d44
runtime.goexit()
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/asm_riscv64.s:494 +0x4 fp=0xffffffcf040267d8 sp=0xffffffcf040267d8 pc=0xffffffc00005100c
这肇因於先前在打通工具链与相依性的时候,将许多函数都直接以空函数的形式引入进来。nanotime1
就是其中之一。要正规的解决这个 API 的话,需要再解析 timer 装置,并解析其频率,再使用虚拟状态暂存器 TIME
来计算得到真正的奈秒数才对。
为何说
TIME
是虚拟的状态暂存器?因爲真正的时间资讯来自mtime
,但作业系统模式已经无法存取机器模式的控制或状态暂存器了;上周实作SetTimer
函数时,我们曾提到这是mtime
的别名。
但这里笔者打算先暴力地将之绕过。首先,移除 src/runtime/os_opensbi.go
内的 nanotime1
函数(由於 walltime
函数也很类似,就一并移除)
+func nanotime1() int64
-func nanotime1() int64 {
- return 0
-}
-
+func walltime() (sec int64, nsec int32)
-func walltime() (sec int64, nsec int32) {
- return
-}
然後在 src/runtime/sys_opensbi_riscv64.s
之中引入:
+
+TEXT runtime·nanotime1(SB),NOSPLIT,$0
+ CSRRS CSR_TIME, X0, T6
+ MOV T6, ret+0(FP)
+ RET
+
+TEXT runtime·walltime(SB),NOSPLIT,$0
+ CSRRS CSR_TIME, X0, T6
+ MOV T6, ret+0(FP)
+ RET
linux/riscv64 系统组合里面,两者确实是共用
clock_gettime
这个系统呼叫。nanotime1
使用的是CLOCK_MONOTONIC
单调计时,而walltime
使用的是CLOCK_REALTIME
真实时间计时。
还是遇到了新的问题,
fatal error: stoplockedm: not runnable
runtime stack:
runtime.throw({0xffffffc00009858a, 0x19})
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/panic.go:965 +0x60
runtime.stoplockedm()
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/proc.go:2600 +0x2c0
runtime.schedule()
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/proc.go:3292 +0x40
runtime.park_m(0xffffffcf040001a0)
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/proc.go:3509 +0x198
runtime.mcall()
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/asm_riscv64.s:279 +0x48
...
但笔者决定先忽略这个问题。我们可以先将触发这个问题的地方卡住,就像先前我们以无穷回圈卡住 newosproc
一样。然後,由於目前为止已经至少有两个 M
的存在,我们还是可以实作出排程,来检验我们是否能够观测到 m0
与新的 M
的上下文成功交换。
虽然上面省略了完整的错误回溯,但问题点发生在稍早、我们已经用暴力方式打通的 nanotime1
的使用之後,发生在 src/runtime/proc.go
呼叫 gcenable
函数之内。所以我们可以先以无穷回圈卡住流程如下:
diff --git a/src/runtime/proc.go b/src/runtime/proc.go
index 197441dfa7..6605b0516b 100644
--- a/src/runtime/proc.go
+++ b/src/runtime/proc.go
@@ -211,6 +211,8 @@ func main() {
}
}()
+ for GOOS == "opensbi" {
+ }
gcenable()
main_init_done = make(chan bool)
这个卡住的回圈千万要记得加上这个作业系统为
opensbi
的条件,否则编工具链时,也会让工具链的那些执行档卡在这里。
这个小节按时间顺序带出 ethanol 核心在上下文交换的设计,也就是依序回答下列问题:从 M
遭到计时器中断起算,如何判断执行绪是否该进行上下文交换了?若是,则该如何进行?欲交接的下一个执行绪若是新生成的执行绪,该如何另外处理?
如前述,我们先将陷阱处理的部分做得正式一点:
...
MOV T6, 0x108(SP)
- // TODO: setup g for the handling
- CALL runtime∕ethanol·SetTimer(SB)
+ MOV (g_m)(g), g
+ MOV (m_gsignal)(g), g
+ MOV SP, -0x08(SP)
+ ADD $-0x10, SP, SP
+ CALL runtime·ethanol_trap1(SB)
+ ADD $0x10, SP, SP
// TODO: reset sscratch for user space
...
移除掉直接设置计时器的部分,然後把先前欠的共常式(g
)设置做成原先打算的(g.m.gsignal
)。这麽一来,後续的呼叫都会以 gsignal
共常式的身份继续执行。
又,接下来我们挪动堆叠,将原先的堆叠指标当作一个参数传入。这里是遵守一般的 Golang 函数 ABI,所以第一个函数在传入时的堆叠的下一个位置(8(SP)
),并在返回时将堆叠指标回归。
这里我们呼叫了 ethanol_trap1
,实作在 src/runtime/os_opensbi.go
当中:
func ethanol_trap1(ctxt *rv64ctxt) {
_g_ := getg()
print("ethanol_trap1 ", _g_.m, "\n")
if (ctxt.scause&uint64(0x8000000000000000))>>63 != 0 {
// interrupt
ctxt.scause &= 0xF
switch ctxt.scause {
case TIMER_INTERRUPT:
ethanol.SetTimer()
_g_.m.life -= 1
default:
throw("cannot handle other traps now")
}
} else {
// exception
switch ctxt.scause {
default:
throw("unexpected exception")
}
}
if _g_.m.life == 0 {
_g_.m.life = 3
mswitch()
}
...
这个函数的传入参数其实只是前一段堆叠的内容,但是为了在 Golang 里面解析,我们另外宣告了一个结构体(rv64ctxt
)来存取。其中包含所有通用暂存器与我们所关注的三个状态暂存器,对应到稍早 ethanol_trap
当中的顺序。此处略过该结构的宣告。
根据先前所储存的 scause
状态暂存器,先做最有效位元(most significant bit)的判断。当这个位元是 1 时,表示进入陷阱处理的原因是中断;反之,则为例外事件。
所以这里我们仅在计时器中断时才重新设置。除此之外都先以 throw
的错误卡住。
这里有一个新增的 life
属性,它代表的意思是,每个执行绪的生命周期中能够遭到计时器中断的次数。这个属性新增在作业系统定义的 mOS
当中,且初值目前设定为 3。当时间到了之後,原执行绪先恢复生命值,然後进行上下文交换,呼叫 mswitch
。
mswitch
以 Golang 实作,
func mswitch1(prev, next *m)
func mswitch() {
_g_ := getg()
var next *m
for mp := allm; mp != nil; mp = mp.alllink {
if mp == _g_.m {
next = _g_.m.alllink
break
}
}
if next == nil {
next = allm
}
mswitch1(_g_.m, next)
}
这里采用的主要逻辑与 newm
时看到的类似,我们也是遍历所有执行绪,找到当前的执行绪在列表中的位置之後,再设置下一个执行绪 next
。之後牵涉到上下文处理,因此我们又得回到组合语言部分。
读者是否会疑问,为什麽这里手续繁多,却不怕被计时器中断又打进来?这是因为,在中断而进入到陷阱处理之时,系统已经改变了状态。具体来说,
sstatus
控制暂存器中的先前中断状态(SPIE
,Supervisor Previous Interrupt Enable)位元会储存这次中断之前的中断启用位元(SIE
,Supervisor Interrupt Enable)的状态,并将当前SIE
设置为 0。
另一个可能的疑问是,为什麽在陷阱处理向量前後已经储存与回复过上下文,而这里还要另外处理?因为前者是正常的流程被中断或是例外强迫进到陷阱处理向量,所以需要保存状态;後者则是,在陷阱处理向量之後又已经走了一段路,因为排程的因素而与其他执行绪交换上下文,日後交换回来之後、离开陷阱向量之前,或许也还有其他事情要由作业系统执行。所以这两者无法互相取代。
mswitch1
定义在 src/runtime/sys_opensbi_riscv64.s
之中:
+// func mswitch1(prev, next *m)
+TEXT runtime·mswitch1(SB),NOSPLIT|NOFRAME,$0-16
+ MOV prev+0(FP), A0
+ MOV next+8(FP), A1
+ 储存部分,省略
+ MOVB (m_mOS+mOS_mstarted)(A1), A2
+ BEQ A2, ZERO, new
+ JMP restore
+new:
+ MOV $1, A2
+ MOVB A2, (m_mOS+mOS_mstarted)(A1)
+ MOV $3, A2
+ MOVB A2, (m_mOS+mOS_life)(A1)
+ MOV (m_g0)(A1), g
+ MOV (g_stack+stack_hi)(g), SP
+ MOV $runtime·mstart(SB), A0
+ CSRRW CSR_SEPC, A0, ZERO
+ SRET
+restore:
+ 回复部分,省略
+ RET
主要的逻辑是,判断 next
执行绪是否已经开始。这里我们也是透过一个 mOS
结构中的 mstarted
布林变数来记录这个状态。若已开始,则可以直接着手回复 next
执行绪的上下文。
若否,表示这是个全新的执行绪。设置完 mstarted
与 life
这些由 opensbi/riscv 系统组合赋予的属性(分别是设置为真,以及三格的生命值)之後,设置 next
执行绪的共常式 g
(mstart
函数的後续要求 g0
共常式,所以这里如此设置。)与堆叠所代表的暂存器。这是在模拟类似 clone
的效果。
最後,就是设置新生执行绪的起始函数,返回到 mstart
去。注意这里是透过 SRET
指令回到被中断前的状态,而非再返回陷阱向量去回复上下文。若是後者的话,会变成回复 prev
执行绪的状态,产生逻辑错误。
以下的范例已更新到 Hoddarla repo。
试着运行看看吧!则会看到
...
ethanol_trap1 0xffffffcf0002a000
ethanol_trap1 0xffffffcf0002a000
ethanol_trap1 0xffffffcf0002a000
ethanol_trap1 0xffffffc0000ac200
ethanol_trap1 0xffffffc0000ac200
ethanol_trap1 0xffffffc0000ac200
ethanol_trap1 0xffffffcf0002a000
ethanol_trap1 0xffffffcf0002a000
ethanol_trap1 0xffffffcf0002a000
ethanol_trap1 0xffffffc0000ac200
ethanol_trap1 0xffffffc0000ac200
ethanol_trap1 0xffffffc0000ac200
ethanol_trap1 0xffffffcf0002a000
ethanol_trap1 0xffffffcf0002a000
ethanol_trap1 0xffffffcf0002a000
ethanol_trap1 0xffffffc0000ac200
ethanol_trap1 0xffffffc0000ac200
ethanol_trap1 0xffffffc0000ac200
...
的输出结果,确实可以观察到三次计时器中断交换一次的状态。结果相当符合预期!
本节实验不包含在上传的新增之中。有兴趣的读者需搭配简单的程序码修改:一行组语里面的一个字元,以及
Makefile
里面修改一行指令,总共两行不到 20 个字元。
也许超过一秒一次的计时器中断还是太不真实了。我们可以回到 src/runtime/ethanol/trap.s
之中修改 SetTimer
函数,
TEXT runtime∕ethanol·SetTimer(SB), NOSPLIT|NOFRAME, $0-0
CSRRS CSR_TIME, ZERO, A0
- ADD $0x1000000, A0, A0
+ ADD $0x100000, A0, A0
当然,这个频率,比照一般的作业系统使用习惯,也还是太低,不过现在当然还没什麽关系。再来,笔者也想再展示一个功能。先前我们在断章当中设定了环境变数,但一直没有去使用它们。现在,由於 Golang 的系统监控执行绪(sysmon
)已经启动,所以我们其实可以传入一些它认得的环境变数,使它改变一点点行为。具体来说,我们修改 Makefile
中 run
项目的传入参数,
- -append "ethanol arg1 arg2 env1=1 env2=abc" \
+ -append "ethanol arg1 arg2 GODEBUG=schedtrace=1" \
如此一来就可以看到其他的除错讯息输出,如下:
ethanol_trap1 0xffffffc0000ac200
ethanol_trap1 0xffffffc0000ac200
ethanol_trap1 0xffffffc0000ac200
SCHED 0ms: gomaxprocs=1 idleprocs=0 threads=2 spinningthreads=0 idlethreads=0 runqueue=0 [0]
SCHED 0ms: gomaxprocs=1 idleprocs=0 threads=2 spinningthreads=0 idlethreads=0 runqueue=0 [0]
ethanol_trap1 0xffffffcf0002c000
SCHED 1ms: gomaxprocs=1 idleprocs=0 threads=2 spinningthreads=0 idlethreads=0 runqueue=0 [0]
SCHED 2ms: gomaxprocs=1 idleprocs=0 threads=2 spinningthreads=0 idlethreads=0 runqueue=0 [0]
ethanol_trap1 0xffffffcf0002c000
SCHED 2ms: gomaxprocs=1 idleprocs=0 threads=2 spinningthreads=0 idlethreads=0 runqueue=0 [0]
SCHED 3ms: gomaxprocs=1 idleprocs=0 threads=2 spinningthreads=0 idlethreads=0 runqueue=0 [0]
ethanol_trap1 0xffffffcf0002c000
ethanol_trap1 0xffffffc0000ac200
ethanol_trap1 0xffffffc0000ac200
ethanol_trap1 0xffffffc0000ac200
SCHED 6ms: gomaxprocs=1 idleprocs=0 threads=2 spinningthreads=0 idlethreads=0 runqueue=0 [0]
SCHED 7ms: gomaxprocs=1 idleprocs=0 threads=2 spinningthreads=0 idlethreads=0 runqueue=0 [0]
ethanol_trap1 0xffffffcf0002c000
...
然而,一瞬间之後,却会发生疯狂的错误输出如下:
...
runtime.dopanic_m(0xffffffcf040004e0, 0xffffffc000030738, 0xffffffcf0403be08)
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/panic.go:1168 +0x2e4
runtime.fatalthrow.func1()
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/panic.go:1020 +0x5c
runtime.fatalthrow()
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/panic.go:1017 +0x4c
runtime.throw({0xffffffc000066608, 0x24})
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/panic.go:965 +0x60
runtime.newstack()
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/stack.go:955 +0xd48
runtime.morestack()
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/asm_riscv64.s:201 +0x70
fatal error: unlock of unlocked lock
runtime stack:
runtime.throw({0xffffffc0000650e0, 0x17})
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/panic.go:965 +0x60
runtime.unlock2(0xffffffc0000daa90)
/home/noner/FOSS/hoddarla/ithome/go/src/runtime/lock_opensbi.go:53 +0x90
...
予焦啦!今天也算是走了颇长的一段路!我们以 Golang 的 M
物件充当执行绪的单位,试图启用一套基本的排程机制。对於新生成的 M
物件,我们主要关注它使用的共常式与堆叠,针对性地初始化;其余的 M
物件,我们则是简单的储存与回复。
乍看之下一切都运行得很好,但稍微让执行状态复杂一点点之後,立刻出现大量的 unlock of unlocked lock
错误。显然还需要继续努力。各位读者,我们明日再会!
<<: [DAY 17]Discord server串接webhook
>>: actions 就是 Vuex 里「共用的 method」
简易呼叫(Simple Call) 当我们直接执行函式时,就是所谓的简易呼叫(Simple Call...
问世间Docker为何物,直教DotCloud以死相许,所以Docker到底是虾毁?不仅让当时云端巨...
Event 事件是什麽? Event 介面表示一个在 DOM 物件上所发生的事件。 一个事件可以是由...
Golang 切片(Slice) 我看很多中文的教学都是翻切片,我也不知道是不是正确的说法,总之也附...
和牛涮最近很常出现在朋友的ig画面中 前阵子找时间到忠孝店品嚐 在价位方面 考量炙烧和牛寿司有数量限...