原創文章,歡迎轉載,轉載請註明出處,謝謝。
0. 前言
皇天不負有心人,終於我們到了執行 main goroutine 環節了。讓我們走起來,看看一個 goroutine 到底是怎麼執行的。
1. 執行 goroutine
稍微回顧下前面的內容,第一講 Go 程式初始化,介紹了 Go 程式是怎麼進入到 runtime 的,隨之揭開 runtime 的面紗。第二講,介紹了排程器的初始化,要執行 goroutine 排程器是必不可少的,只有排程器準備就緒才能開始工作。第三講,介紹了 main goroutine 是如何建立出來的,只有建立一個 goroutine 才能開始執行,否則執行程式碼無從談起。這一講,我們繼續介紹如何執行 main goroutine。
我們知道 main goroutine 此時處於 _Grunnable 狀態,要使得 main goroutine 處於 _Grunning 狀態,還需要將它和 P 繫結。畢竟 P 是負責排程任務給執行緒處理的,只有和 P 繫結執行緒才能處理相應的 goroutine。
1.1 繫結 P
回到程式碼 newproc
:
func newproc(fn *funcval) {
gp := getg()
pc := getcallerpc()
systemstack(func() {
newg := newproc1(fn, gp, pc) // 建立 newg,這裡是 main goroutine
pp := getg().m.p.ptr() // 獲取當前工作執行緒繫結的 P,這裡是 g0.m.p = allp[0]
runqput(pp, newg, true) // 繫結 allp[0] 和 main goroutine
if mainStarted { // mainStarted 還未啟動,這裡是 false
wakep()
}
})
}
進入 runqput
函式檢視 main goroutine 是怎麼和 allp[0]
繫結的:
// runqput tries to put g on the local runnable queue.
// If next is false, runqput adds g to the tail of the runnable queue.
// If next is true, runqput puts g in the pp.runnext slot.
// If the run queue is full, runnext puts g on the global queue.
// Executed only by the owner P.
func runqput(pp *p, gp *g, next bool) {
...
if next {
retryNext:
oldnext := pp.runnext // 從 P 的 runnext 獲取下一個將要執行的 goroutine,這裡 pp.runnext = nil
if !pp.runnext.cas(oldnext, guintptr(unsafe.Pointer(gp))) { // 將 P 的 runnext 更新為 gp,這裡的 gp 是 main goroutine
goto retryNext
}
if oldnext == 0 { // 如果 P 原來要執行的 goroutine 是 nil,則直接返回,這裡建立的是 main goroutine 將直接返回
return
}
gp = oldnext.ptr() // 如果不為 nil,表示是一個將要執行的 goroutine。後續對這個被趕走的 goroutine 進行處理
}
retry:
h := atomic.LoadAcq(&pp.runqhead)
t := pp.runqtail
if t-h < uint32(len(pp.runq)) { // P 的隊尾和隊頭指向本地執行佇列 runq,如果當前佇列長度小於 runq 則將趕走的 goroutine 新增到隊尾
pp.runq[t%uint32(len(pp.runq))].set(gp)
atomic.StoreRel(&pp.runqtail, t+1)
return
}
if runqputslow(pp, gp, h, t) { // 如果當前 P 的佇列長度等於不小於 runq,表示本地佇列滿了,將趕走的 goroutine 新增到全域性佇列中
return
}
goto retry
}
runqput
函式繫結 P 和 goroutine,同時處理 P 中的本地執行佇列。基本流程在註釋中已經介紹的比較清楚了。
這裡我們繫結的是 main goroutine,直接繫結到 P 的 runnext 成員即可。不過對於 runqput
的整體處理來說,還需要在介紹一下 runqputslow
函式:
// Put g and a batch of work from local runnable queue on global queue.
// Executed only by the owner P.
func runqputslow(pp *p, gp *g, h, t uint32) bool {
var batch [len(pp.runq)/2 + 1]*g // 定義 batch,長度是 P.runq 的一半。batch 用來裝 g
// First, grab a batch from local queue.
n := t - h
n = n / 2
if n != uint32(len(pp.runq)/2) {
throw("runqputslow: queue is not full")
}
for i := uint32(0); i < n; i++ {
batch[i] = pp.runq[(h+i)%uint32(len(pp.runq))].ptr() // 從 P 的 runq 中拿出一半的 g 到 batch 中
}
if !atomic.CasRel(&pp.runqhead, h, h+n) { // cas-release, commits consume // 更新 P 的 runqhead 的指向,它指向的是本地佇列的頭
return false
}
batch[n] = gp // 將趕走的 goroutine 放到 batch 尾
if randomizeScheduler { // 如果是隨機排程的話,這裡還要打亂 batch 中 g 的順序以保證隨機性
for i := uint32(1); i <= n; i++ {
j := fastrandn(i + 1)
batch[i], batch[j] = batch[j], batch[i]
}
}
// Link the goroutines.
for i := uint32(0); i < n; i++ {
batch[i].schedlink.set(batch[i+1]) // batch 中 goroutine 的 schedlink 按順序指向其它 goroutine,構造一個連結串列
}
var q gQueue // gQueue 是一個包含頭和尾的指標,將頭和尾指標分別指向 batch 的頭 batch[0] 和尾 batch[n]
q.head.set(batch[0])
q.tail.set(batch[n])
// Now put the batch on global queue.
lock(&sched.lock) // 操作全域性變數 sched,為 sched 加鎖
globrunqputbatch(&q, int32(n+1)) // globrunqputbatch 將 q 指向的 batch 傳給全域性變數 sched
unlock(&sched.lock) // 解鎖
return true
}
func globrunqputbatch(batch *gQueue, n int32) {
assertLockHeld(&sched.lock)
sched.runq.pushBackAll(*batch) // 這裡將 sched.runq 指向 batch
sched.runqsize += n // sched 的 runqsize 加 n,n 表示新新增進 sched.runq 的 goroutine
*batch = gQueue{}
}
如果 P 的本地佇列已滿,則在 runqputslow
中拿出本地佇列的一半 goroutine 放到 sched.runq 全域性佇列中。這裡本地佇列是固定長度,容量有限,用陣列來表示佇列。而全域性佇列長度是不固定的,用連結串列來表示全域性佇列。
我們可以畫出示意圖如下圖,注意示意圖只是加深理解,和我們這裡執行 main goroutine 的流程沒關係:
1.2 執行 main goroutine
P 和 main goroutine 繫結之後,理論上已經可以執行 main goroutine 了。繼續看程式碼執行的什麼:
> runtime.rt0_go() /usr/local/go/src/runtime/asm_amd64.s:358 (PC: 0x45434a)
Warning: debugging optimized function
353: PUSHQ AX
354: CALL runtime·newproc(SB)
355: POPQ AX
356:
357: // start this M
=> 358: CALL runtime·mstart(SB) // 呼叫 mstart 意味著當前執行緒開始工作了;mstart 是一個永不返回的函式
359:
360: CALL runtime·abort(SB) // mstart should never return
361: RET
362:
向下執行:
(dlv) si
> runtime.mstart() /usr/local/go/src/runtime/asm_amd64.s:394 (PC: 0x4543c0)
Warning: debugging optimized function
TEXT runtime.mstart(SB) /usr/local/go/src/runtime/asm_amd64.s
=> asm_amd64.s:394 0x4543c0 e87b290000 call $runtime.mstart0
asm_amd64.s:395 0x4543c5 c3 ret
呼叫 runtime.mstart0
:
func mstart0() {
gp := getg() // gp = g0
...
mstart1()
...
}
呼叫 mstart1
:
func mstart1() {
gp := getg() // gp = g0
// 儲存執行緒執行的棧,當執行緒進入 schedule 函式就不會返回,這意味著執行緒執行的棧是可複用的
gp.sched.g = guintptr(unsafe.Pointer(gp))
gp.sched.pc = getcallerpc()
gp.sched.sp = getcallersp()
...
if fn := gp.m.mstartfn; fn != nil { // 執行 main goroutine,fn == nil
fn()
}
...
schedule() // 執行緒進入 schedule 排程迴圈,該迴圈是永不返回的
}
進入 schedule
:
func schedule() {
mp := getg().m // mp = m0
...
top:
pp := mp.p.ptr() // pp = allp[0]
pp.preempt = false
// 執行緒有兩種狀態,自旋和非自旋。自旋表示執行緒沒有工作,在找工作階段。非自旋表示執行緒正在工作
// 這裡如果執行緒自旋,但是執行緒繫結的 P 本地佇列有 goroutine 則報異常
if mp.spinning && (pp.runnext != 0 || pp.runqhead != pp.runqtail) {
throw("schedule: spinning with local work")
}
// blocks until work is available
gp, inheritTime, tryWakeP := findRunnable() // 找一個處於 _Grunnable 狀態的 goroutine 出來
...
execute(gp, inheritTime) // 執行該 goroutine,這裡執行的是 main goroutine
}
schedule
中的重點是 findRunaable
函式,進入該函式:
// Finds a runnable goroutine to execute.
// Tries to steal from other P's, get g from local or global queue, poll network.
// tryWakeP indicates that the returned goroutine is not normal (GC worker, trace
// reader) so the caller should try to wake a P.
func findRunnable() (gp *g, inheritTime, tryWakeP bool) {
mp := getg().m // mp = m0
top:
pp := mp.p.ptr() // pp = allp[0] = p0
...
// Check the global runnable queue once in a while to ensure fairness.
// Otherwise two goroutines can completely occupy the local runqueue
// by constantly respawning each other.
// 官方的註釋對這一段邏輯已經解釋的很詳細了,我們就跳過了,偷個懶
if pp.schedtick%61 == 0 && sched.runqsize > 0 {
lock(&sched.lock)
gp := globrunqget(pp, 1)
unlock(&sched.lock)
if gp != nil {
return gp, false, false
}
}
// local runq
// 從 P 的本地佇列找 goroutine
if gp, inheritTime := runqget(pp); gp != nil {
return gp, inheritTime, false
}
...
}
findRunnable
中首先為了公平,每呼叫 schedule 函式 61 次就要從全域性可執行佇列中獲取 goroutine,防止全域性佇列中的 goroutine 被“餓死”。接著從 P 的本地佇列中獲取 goroutine,這裡執行的是 main goroutine 將從 P 的本地佇列中獲取 goroutine。檢視 runqget
:
func runqget(pp *p) (gp *g, inheritTime bool) {
// If there's a runnext, it's the next G to run.
next := pp.runnext
// If the runnext is non-0 and the CAS fails, it could only have been stolen by another P,
// because other Ps can race to set runnext to 0, but only the current P can set it to non-0.
// Hence, there's no need to retry this CAS if it fails.
if next != 0 && pp.runnext.cas(next, 0) {
return next.ptr(), true
}
for {
h := atomic.LoadAcq(&pp.runqhead) // load-acquire, synchronize with other consumers
t := pp.runqtail
if t == h {
return nil, false
}
gp := pp.runq[h%uint32(len(pp.runq))].ptr()
if atomic.CasRel(&pp.runqhead, h, h+1) { // cas-release, commits consume
return gp, false
}
}
}
註釋已經比較詳細了,首先拿到 P 的 runnext 作為要執行的 goroutine。如果拿到的 goroutine 不是空,則重置 runnext,並且返回拿到的 goroutine。如果拿到的 goroutine 是空的,則從本地佇列中拿 goroutine。
透過 findRunnable
我們拿到可執行的 main goroutine。接著呼叫 execute
執行 main goroutine。
進入 execute
:
func execute(gp *g, inheritTime bool) {
mp := getg().m // mp = m0
mp.curg = gp // mp.curg = g1
gp.m = mp // gp.m = m0
casgstatus(gp, _Grunnable, _Grunning) // 更新 goroutine 的狀態為 _Grunning
gp.waitsince = 0
gp.preempt = false
gp.stackguard0 = gp.stack.lo + stackGuard
if !inheritTime {
mp.p.ptr().schedtick++
}
...
gogo(&gp.sched)
}
在 execute
中將執行緒和 gouroutine 關聯起來,更新 goroutine 的狀態,然後呼叫 gogo 完成從 g0 棧到 gp 棧的切換,gogo 是用匯編編寫的,原因如下:
gogo 函式也是透過組合語言編寫的,這裡之所以需要使用匯編,是因為 goroutine 的排程涉及不同執行流之間的切換。
前面我們在討論作業系統切換執行緒時已經看到過,執行流的切換從本質上來說就是 CPU 暫存器以及函式呼叫棧的切換,然而不管是 go 還是 c 這種高階語言都無法精確控制 CPU 暫存器,因而高階語言在這裡也就無能為力了,只能依靠彙編指令來達成目的。
進入 gogo
,gogo
傳入的是 goroutine 的 sched 結構:
TEXT runtime·gogo(SB), NOSPLIT, $0-8
MOVQ buf+0(FP), BX // gobuf
MOVQ gobuf_g(BX), DX // gobuf 的 g 賦給 DX
MOVQ 0(DX), CX // make sure g != nil
JMP gogo<>(SB) // 跳轉到私有函式 gogo<>
TEXT gogo<>(SB), NOSPLIT, $0
get_tls(CX) // 獲取當前執行緒 tls 中的 goroutine
MOVQ DX, g(CX)
MOVQ DX, R14 // set the g register
MOVQ gobuf_sp(BX), SP // restore SP
MOVQ gobuf_ret(BX), AX // AX = gobuf.ret
MOVQ gobuf_ctxt(BX), DX // DX = gobuf.ctxt
MOVQ gobuf_bp(BX), BP // BP = gobuf.bp
MOVQ $0, gobuf_sp(BX) // clear to help garbage collector
MOVQ $0, gobuf_ret(BX)
MOVQ $0, gobuf_ctxt(BX)
MOVQ $0, gobuf_bp(BX)
MOVQ gobuf_pc(BX), BX // BX = gobuf.pc
JMP BX // 跳轉到 gobuf.pc
在 gogo<>
中完成 g0 到 gp 棧的切換:MOVQ gobuf_sp(BX), SP
,並且跳轉到 gobuf.pc 執行。我們看 gobuf.pc 要執行的指令地址是什麼:
asm_amd64.s:421 0x45363a 488b5b08 mov rbx, qword ptr [rbx+0x8]
=> asm_amd64.s:422 0x45363e ffe3 jmp rbx
(dlv) regs
Rbx = 0x000000000042ee80
執行 JMP BX
跳轉到 0x000000000042ee80
:
(dlv) si
> runtime.main() /usr/local/go/src/runtime/proc.go:144 (PC: 0x42ee80)
Warning: debugging optimized function
TEXT runtime.main(SB) /usr/local/go/src/runtime/proc.go
=> proc.go:144 0x42ee80 4c8d6424e8 lea r12, ptr [rsp-0x18]
終於我們揭開了它的神秘面紗,這個指令指向的是 runtime.main
函式的第一條彙編指令。也就是說,跳轉到了 runtime.main
,這個函式會呼叫我們 main 包下的 main 函式。檢視 runtime.main
函式:
// The main goroutine.
func main() {
mp := getg().m // mp = m0
if goarch.PtrSize == 8 {
maxstacksize = 1000000000 // 擴棧,棧的最大空間是 1GB
} else {
maxstacksize = 250000000
}
...
// Allow newproc to start new Ms.
mainStarted = true
if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
systemstack(func() {
newm(sysmon, nil, -1) // 開啟監控執行緒,這個執行緒很重要,我們後續會講,這裡先放著,讓 sysmon 飛一會兒
})
}
...
// make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
fn := main_main // 這裡的 main_main 連結的是 main 包中的 main 函式
fn() // 執行 main.main
...
runExitHooks(0)
exit(0) // 執行完 main.main 之後呼叫 exit 退出執行緒
for {
var x *int32
*x = 0
}
}
runtime.main
是在 main goroutine 棧中執行的。在函式中呼叫 main.main 執行我們寫的使用者程式碼:
(dlv) n
266: fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
=> 267: fn()
(dlv) s
> main.main() ./hello.go:3 (PC: 0x45766a)
Warning: debugging optimized function
1: package main
2:
=> 3: func main() {
4: println("Hello World")
5: }
main.main
執行完之後執行緒呼叫 exit(0)
退出程式。
2. 小結
至此我們的 main goroutine 就執行完了,花了四講才算走通了一個 main goroutine,真不容易呀。當然,關於 Go runtime 排程器的故事還沒結束,下一講我們繼續。