1. 程式人生 > >Golang原始碼學習:排程邏輯(三)工作執行緒的執行流程與排程迴圈

Golang原始碼學習:排程邏輯(三)工作執行緒的執行流程與排程迴圈

本文內容主要分為三部分: 1. main goroutine 的排程執行 2. 非 main goroutine 的退出流程 3. 工作執行緒的執行流程與排程迴圈。 ## main goroutine 的排程執行 runtime·rt0_go中在呼叫完runtime.newproc建立main goroutine後,就呼叫了runtime.mstart。讓我們來分析一下這個函式。 ### mstart mstart沒什麼太多工作,然後就呼叫了mstart1。 ``` func mstart() { _g_ := getg() // 在啟動階段,_g_.stack早就完成了初始化,所以osStack是false,下面被省略的也不會執行。 osStack := _g_.stack.lo == 0 ...... _g_.stackguard0 = _g_.stack.lo + _StackGuard _g_.stackguard1 = _g_.stackguard0 mstart1() ...... mexit(osStack) } ``` ### mstart1 - 呼叫save儲存g0的狀態 - 處理訊號相關 - 呼叫 schedule 開始排程 ``` func mstart1() { _g_ := getg() if _g_ != _g_.m.g0 { throw("bad runtime·mstart") } save(getcallerpc(), getcallersp()) // 儲存呼叫mstart1的函式(mstart)的 pc 和 sp。 asminit() // 空函式 minit() // 訊號相關 if _g_.m == &m0 { // 初始化時會執行這裡,也是訊號相關 mstartm0() } if fn := _g_.m.mstartfn; fn != nil { // 初始化時 fn = nil,不會執行這裡 fn() } if _g_.m != &m0 { // 不是m0的話,沒有p。繫結一個p acquirep(_g_.m.nextp.ptr()) _g_.m.nextp = 0 } schedule() } ``` ### save(pc, sp uintptr) 儲存排程資訊 儲存當前g(初始化時為g0)的狀態到sched欄位中。 ``` func save(pc, sp uintptr) { _g_ := getg() _g_.sched.pc = pc _g_.sched.sp = sp _g_.sched.lr = 0 _g_.sched.ret = 0 _g_.sched.g = guintptr(unsafe.Pointer(_g_)) if _g_.sched.ctxt != nil { badctxt() } } ``` ### schedule 開始排程 呼叫globrunqget、runqget、findrunnable獲取一個可執行的g ``` func schedule() { _g_ := getg() // g0 ...... var gp *g // 初始化時,經過下面一系列查詢,會找到main goroutine,因為目前為止整個執行時只有這一個g(除了g0)。 var inheritTime bool ...... if gp == nil { // 該p上每進行61次就從全域性佇列中獲取一個g if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 { lock(&sched.lock) gp = globrunqget(_g_.m.p.ptr(), 1) unlock(&sched.lock) } } if gp == nil { // 從p的runq中獲取一個g gp, inheritTime = runqget(_g_.m.p.ptr()) // We can see gp != nil here even if the M is spinning, // if checkTimers added a local goroutine via goready. } if gp == nil { // 尋找可執行的g,會嘗試從本地,全域性執行對列獲取,如果沒有,從其他p那裡偷取。 gp, inheritTime = findrunnable() // blocks until work is available } ...... execute(gp, inheritTime) } ``` ### execute:安排g在當前m上執行 - 被排程的 g 與 m 相互繫結 - 更改g的狀態為 _Grunning - 呼叫 gogo 切換到被排程的g上 ``` func execute(gp *g, inheritTime bool) { _g_ := getg() // g0 _g_.m.curg = gp // 與下面一行是 gp 和 m 相互繫結。gp 其實就是 main goroutine gp.m = _g_.m casgstatus(gp, _Grunnable, _Grunning) // 更改狀態 gp.waitsince = 0 gp.preempt = false gp.stackguard0 = gp.stack.lo + _StackGuard if !inheritTime { _g_.m.p.ptr().schedtick++ } ...... gogo(&gp.sched) } ``` ### gogo(buf *gobuf) 在本方法下面的講解中將使用newg代指被排程的g。 gogo函式是用匯編實現的。其作用是:載入newg的上下文,跳轉到gobuf.pc指向的函式。 ``` // go/src/runtime/asm_amd64.s TEXT runtime·gogo(SB), NOSPLIT, $16-8 MOVQ buf+0(FP), BX // bx = &gp.sched MOVQ gobuf_g(BX), DX // dx = gp.sched.g ,也就是儲存的 newg 指標 MOVQ 0(DX), CX // make sure g != nil get_tls(CX) MOVQ DX, g(CX) // newg指標設定到tls MOVQ gobuf_sp(BX), SP // 下面四條是載入上下文到cpu暫存器。 MOVQ gobuf_ret(BX), AX MOVQ gobuf_ctxt(BX), DX MOVQ gobuf_bp(BX), BP MOVQ $0, gobuf_sp(BX) // 下面四條是清零,減少gc的工作量。 MOVQ $0, gobuf_ret(BX) MOVQ $0, gobuf_ctxt(BX) MOVQ $0, gobuf_bp(BX) MOVQ gobuf_pc(BX), BX // gobuf.pc 儲存的是要執行的函式指標,初始化時此函式為runtime.main JMP BX // 跳轉到要執行的函式 ``` ### runtime.main:main函式的執行 在上面gogo執行最後的JMP指令,其實就是跳轉到了runtime.main。 ``` func main() { g := getg() // 獲取當前g,已經不是g0了,我們暫且稱為maing if sys.PtrSize == 8 { // 64位系統,棧最大為1GB maxstacksize = 1000000000 } else { maxstacksize = 250000000 } mainStarted = true // 啟動監控程序,搶佔排程就是在這裡實現的 if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon systemstack(func() { newm(sysmon, nil) }) } ...... doInit(&runtime_inittask) // 呼叫runtime的初始化函式 ...... runtimeInitTime = nanotime() // 記錄世界開始時間 gcenable() // 開啟gc ...... doInit(&main_inittask) // 呼叫main的初始化函式 ...... fn := main_main // 呼叫main.main,也就是我們經常寫hello world的main。 fn() ...... exit(0) // 退出 } ``` runtime.main主要做了以下的工作: - 啟動監控程序。 - 呼叫runtime的初始化函式。 - 開啟gc。 - 呼叫main的初始化函式。 - 呼叫main.main,執行完後退出。 ## 非 main goroutine 的退出流程 首先明確一點,無論是main goroutine還是非main goroutine的都是呼叫newproc建立的,所以在排程上基本是一致的。 之前的文章中說過,在gostartcall函式中,會將goroutine要執行的函式fn偽造成是被goexit呼叫的。但是,當fn是runtime.main的時候是沒有用的,因為在runtime.main末尾會呼叫exit(0)退出程式。所以,這隻對非main goroutine起作用。讓我們簡單驗證一下。 先給出一個簡單的例子: ``` package main import "fmt" func main() { ch := make(chan int) go foo(ch) fmt.Println(<-ch) } func foo(ch chan int) { ch <- 1 } ``` dlv除錯一波: ``` root@xiamin:~/study# dlv debug foo.go (dlv) b main.foo // 打個斷點 Breakpoint 1 set at 0x4ad86f for main.foo() ./foo.go:11 (dlv) c >
main.foo() ./foo.go:11 (hits goroutine(6):1 total:1) (PC: 0x4ad86f) 6: ch := make(chan int) 7: go foo(ch) 8: fmt.Println(<-ch) 9: } 10: => 11: func foo(ch chan int) { 12: ch <- 1 13: } (dlv) bt // 可以看到呼叫棧中確實存在goexit 0 0x00000000004ad86f in main.foo at ./foo.go:11 1 0x0000000000463df1 in runtime.goexit at /root/go/src/runtime/asm_amd64.s:1373 // 此處執行三次 s,得到以下結果,確實是回到了goexit。 > runtime.goexit() /root/go/src/runtime/asm_amd64.s:1374 (PC: 0x463df1) 1370: // The top-most function running on a goroutine 1371: // returns to goexit+PCQuantum. 1372: TEXT runtime·goexit(SB),NOSPLIT,$0-0 1373: BYTE $0x90 // NOP =>
1374: CALL runtime·goexit1(SB) // does not return 1375: // traceback from goexit1 must hit code range of goexit 1376: BYTE $0x90 // NOP ``` 我們暫且將關聯foo的g稱之為foog,接下來我們看一下它的退出流程。 ### goexit ``` TEXT runtime·goexit(SB),NOSPLIT,$0-0 BYTE $0x90 // NOP CALL runtime·goexit1(SB) // does not return // traceback from goexit1 must hit code range of goexit BYTE $0x90 // NOP ``` ### goexit1 ``` func goexit1() { if raceenabled { racegoend() } if trace.enabled { traceGoEnd() } mcall(goexit0) } ``` goexit和goexit1沒什麼可說的,看一下mcall ### mcall(fn func(*g)) mcall的引數是個函式fn,而fn有個引數是*g,此處fn是goexit0。 mcall是由彙編編寫的: ``` TEXT runtime·mcall(SB), NOSPLIT, $0-8 MOVQ fn+0(FP), DI // 此處 di 儲存的是 funcval 結構體指標,funcval.fn 指向的是 goexit0。 get_tls(CX) MOVQ g(CX), AX // 此處 ax 中儲存的是foog // 儲存foog的上下文 MOVQ 0(SP), BX // caller's PC。mcall的返回地址,此處就是 goexit1 呼叫 mcall 時的pc MOVQ BX, (g_sched+gobuf_pc)(AX) // foog.sched.pc = caller's PC LEAQ fn+0(FP), BX // caller's SP。 MOVQ BX, (g_sched+gobuf_sp)(AX) // foog.sched.sp = caller's SP MOVQ AX, (g_sched+gobuf_g)(AX) // foog.sched.g = foog MOVQ BP, (g_sched+gobuf_bp)(AX) // foog.sched.bp = bp // 切換到m.g0和它的棧,呼叫fn。 MOVQ g(CX), BX // 此處 bx 中儲存的是foog MOVQ g_m(BX), BX // bx = foog.m MOVQ m_g0(BX), SI // si = m.g0 CMPQ SI, AX // if g == m->
g0 call badmcall JNE 3(PC) // 上面的結果不相等就跳轉到下面第三行。 MOVQ $runtime·badmcall(SB), AX JMP AX MOVQ SI, g(CX) // g = m->g0。m.g0設定到tls MOVQ (g_sched+gobuf_sp)(SI), SP // sp = m->g0->sched.sp。設定g0棧. PUSHQ AX // fn的引數壓棧,ax = foog MOVQ DI, DX MOVQ 0(DI), DI // 讀取 funcval 結構的第一個成員,也就是 funcval.fn,此處是goexit0。 CALL DI // 呼叫 goexit0(foog)。 POPQ AX MOVQ $runtime·badmcall2(SB), AX JMP AX RET ``` 在此場景下,mcall做了以下工作:儲存foog的上下文。切換到g0及其棧,呼叫傳入的方法,並將foog作為引數。 可以看到mcall與gogo的作用正好相反: - gogo實現了從g0切換到某個goroutine,執行關聯函式。 - mcall實現了儲存某個goroutine,切換到g0及其棧,並呼叫fn函式,其引數就是被儲存的goroutine指標。 ### goexit0 ``` func goexit0(gp *g) { _g_ := getg() // g0 casgstatus(gp, _Grunning, _Gdead) // 更改gp狀態為_Gdead if isSystemGoroutine(gp, false) { atomic.Xadd(&sched.ngsys, -1) } // 下面的一段就是清零gp的屬性 gp.m = nil locked := gp.lockedm != 0 gp.lockedm = 0 _g_.m.lockedg = 0 gp.preemptStop = false gp.paniconfault = false gp._defer = nil // should be true already but just in case. gp._panic = nil // non-nil for Goexit during panic. points at stack-allocated data. gp.writebuf = nil gp.waitreason = 0 gp.param = nil gp.labels = nil gp.timer = nil ...... dropg() // 解綁gp與當前m。_g_.m.curg.m = nil ; _g_.m.curg = nil 。 ...... gfput(_g_.m.p.ptr(), gp) // 放入空閒列表。如果本地佇列太多,會轉移一部分到全域性佇列。 ...... schedule() // 重新排程 } ``` goexit0做了以下工作: - 將gp屬性清零與m解綁 - gfput 放入空閒列表 - schedule 重新排程 ## 工作執行緒的執行流程與排程迴圈 以下給出一個工作執行緒的執行流程簡圖: ![](https://img2020.cnblogs.com/blog/365378/202004/365378-20200413143150726-1515963048.png) 可以看到工作執行緒的執行是從mstart開始的。schedule->......->goexit0->schedule形成了一個排程迴圈。 高度概括一下執行流程與排程迴圈: - **mstart**:主要是設定g0.stackguard0,g0.stackguard1。 - **mstart1**:呼叫save儲存callerpc和callerpc到g0.sched。然後呼叫schedule開始排程迴圈。 - **schedule**:獲得一個可執行的g。下面用gp代指。 - **execute**(gp *g, inheritTime bool):繫結gp與當前m,狀態改為_Grunning。 - **gogo**(buf *gobuf):載入gp的上下文,跳轉到buf.pc指向的函式。 - **執行buf.pc指向函式**。 - **goexit->goexit1**:呼叫mcall(goexit0)。 - **mcall**(fn func(*g)):儲存當前g(也就是gp)的上下文;切換到g0及其棧,呼叫fn,引數為gp。 - **goexit0**(gp *g):清零gp的屬性,狀態_Grunning改為_Gdead;dropg解綁m和gp;gfput放入佇列;**schedule**重新