1. 程式人生 > >Golang原始碼學習:排程邏輯(二)main goroutine的建立

Golang原始碼學習:排程邏輯(二)main goroutine的建立

接上一篇繼續分析一下runtime.newproc方法。 ## 函式簽名 newproc函式的簽名為 newproc(siz int32, fn *funcval) siz是傳入的引數大小(不是個數);**fn對應的是函式,但並不是函式指標,funcval.fn才是真正指向函式程式碼的指標。** ``` // go/src/runtime/runtime2.go type funcval struct { fn uintptr // 真正指向函式程式碼的指標 } ``` ## 關鍵字go 在golang中編譯器會把類似 **go foo()** 編譯成呼叫 runtime.newproc 方法。 準備一段程式碼: ``` package main import ( "fmt" "time" ) func main() { go printAdd(3, 7) time.Sleep(time.Second) } func printAdd(a, b int) { fmt.Println(a + b) } ``` 開始除錯: 關於golang棧結構的分析可以參考 Golang原始碼學習:使用gdb除錯探究Golang函式呼叫棧結構 ``` root@xiamin:~/study# dlv debug test.go Type 'help' for list of commands. (dlv) b main.main Breakpoint 1 set at 0x4ada0f for main.main() ./test.go:8 (dlv) c > main.main() ./test.go:8 (hits goroutine(1):1 total:1) (PC: 0x4ada0f) 3: import ( 4: "fmt" 5: "time" 6: ) 7: => 8: func main() { 9: go printAdd(3, 7) 10: time.Sleep(time.Second) 11: } 12: 13: func printAdd(a, b int) { // 這裡執行幾次si,得到下面。 (dlv) disass TEXT main.main(SB) /root/study/test.go test.go:8 0x4ada00 64488b0c25f8ffffff mov rcx, qword ptr fs:[0xfffffff8] test.go:8 0x4ada09 483b6110 cmp rsp, qword ptr [rcx+0x10] test.go:8 0x4ada0d 764f jbe 0x4ada5e test.go:8 0x4ada0f* 4883ec28 sub rsp, 0x28 test.go:8 0x4ada13 48896c2420 mov qword ptr [rsp+0x20], rbp test.go:8 0x4ada18 488d6c2420 lea rbp, ptr [rsp+0x20] // 在main的棧幀中設定newproc的引數siz,16位元組 test.go:9 0x4ada1d c7042410000000 mov dword ptr [rsp], 0x10 // 計算printAdd函式對應的funcval結構體的地址放入rax test.go:9 0x4ada24 488d057d5e0300 lea rax, ptr [rip+0x35e7d] // 在main的棧幀中設定newproc的引數fn test.go:9 0x4ada2b 4889442408 mov qword ptr [rsp+0x8], rax // printAdd的引數a test.go:9 0x4ada30 48c744241003000000 mov qword ptr [rsp+0x10], 0x3 // printAdd的引數b test.go:9 0x4ada39 48c744241807000000 mov qword ptr [rsp+0x18], 0x7 // 呼叫 runtime.newproc => test.go:9 0x4ada42 e80902f9ff call $runtime.newproc test.go:10 0x4ada47 48c7042400ca9a3b mov qword ptr [rsp], 0x3b9aca00 test.go:10 0x4ada4f e86c4afaff call $time.Sleep test.go:11 0x4ada54 488b6c2420 mov rbp, qword ptr [rsp+0x20] test.go:11 0x4ada59 4883c428 add rsp, 0x28 test.go:11 0x4ada5d c3 ret test.go:8 0x4ada5e e88d47fbff call $runtime.morestack_noctxt :1 0x4ada63 eb9b jmp $main.main ``` 我們來驗證一下fn引數: ``` (dlv) regs ...... Rax = 0x00000000004e38a8 // 儲存的是 printAdd 對應的 runtime.funcval 地址。 ...... (dlv) p *(*runtime.funcval)(0x00000000004e38a8) runtime.funcval {fn: 4905584} // 4905584是十進位制,轉換成十六進位制是 0x4ada70。 (dlv) p &printAdd (*)(0x4ada70) // 函式指標與上面的 funcval.fn 相符。 ``` 此段僅用來分析go關鍵字的實現。與下面的 main goroutine無直接關聯。 ## main goroutine的建立 以下注釋的場景均為初始化時。 runtime·rt0_go 中呼叫 runtime.newproc 相關程式碼: ``` TEXT runtime·rt0_go(SB),NOSPLIT,$0 ...... // 呼叫runtime·newproc建立goroutine,指向函式為runtime·main MOVQ $runtime·mainPC(SB), AX // runtime·mainPC就是runtime·main PUSHQ AX // newproc的第二個引數fn,也就是goroutine要執行的函式。 PUSHQ $0 // newproc的第一個引數siz,表示要傳入runtime·main中引數的大小,此處為0。 // 建立 main goroutine。非main goroutine也是此方法建立。 CALL runtime·newproc(SB) POPQ AX POPQ AX ...... DATA runtime·mainPC+0(SB)/8,$runtime·main(SB) GLOBL runtime·mainPC(SB),RODATA,$8 ``` ### runtime.newproc ``` func newproc(siz int32, fn *funcval) { // 獲取fn函式的引數起始地址,可參考上例中的printAdd,sys.PtrSize的值是8。 argp := add(unsafe.Pointer(&fn), sys.PtrSize) // 獲取一個g(m0.g0) gp := getg() // 呼叫者的pc,也就是執行完此函式返回呼叫者時的下一條指令地址,本例中是 POPQ AX pc := getcallerpc() systemstack(func() { newproc1(fn, argp, siz, gp, pc) }) } ``` ### runtime.newproc1 ``` func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) { _g_ := getg() // 當前g。g0 ...... acquirem() // 禁止搶佔 siz := narg siz = (siz + 7) &^ 7 // 使siz為8的整數倍。&^為雙目運算子,將運算子左邊資料相異的保留,相同位清零。 ...... _p_ := _g_.m.p.ptr() // 當前關聯的p。allp[0] newg := gfget(_p_) // 獲取一個g,下有分析。 if newg == nil { newg = malg(_StackMin) // 分配一個新g casgstatus(newg, _Gidle, _Gdead) // 更改狀態 allgadd(newg) // 加入到allgs切片中 } ...... // 調整newg的棧頂指標 totalSize := 4*sys.RegSize + uintptr(siz) + sys.MinFrameSize // extra space in case of reads slightly beyond frame totalSize += -totalSize & (sys.SpAlign - 1) // align to spAlign sp := newg.stack.hi - totalSize spArg := sp ...... if narg > 0 { memmove(unsafe.Pointer(spArg), argp, uintptr(narg)) // 將引數從呼叫newproc的函式棧幀中copy到新的g棧幀中。 ...... } // newg.sched儲存的是排程相關的資訊,排程器要將這些資訊裝載到cpu中才能執行goroutine。 memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched)) // 將newg.sched結構體清零 newg.sched.sp = sp // 棧頂 newg.stktopsp = sp // 此處只是暫時借用pc屬性儲存 runtime.goexit + 1 位置的地址。在gostartcallfn會用到。 newg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function newg.sched.g = guintptr(unsafe.Pointer(newg)) // 儲存newg指標 gostartcallfn(&newg.sched, fn) // 將函式與g關聯起來。下有分析。 ...... casgstatus(newg, _Gdead, _Grunnable) // 更改狀態 ...... runqput(_p_, newg, true) // 儲存到執行佇列中。 // 初始化時不會執行,mainStarted 在 runtime.main 中設定為 true if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted { wakep() } releasem(_g_.m) } ``` 總結一下初始化時newproc1做的工作: - 呼叫gfget獲取newg,如果為nil,呼叫malg分配一個,然後加入到全域性變數allgs中。 - 從呼叫newproc的函式棧幀中copy引數到newg棧幀中。 - 設定newg.sched屬性,呼叫gostartcallfn,將newg和函式關聯。 - 更改狀態為_Grunnable,儲存到p.runq中(p.runq長度是256,滿了會被拿出一些放在sched.runq中)。 概括講就是:獲取g->複製引數->設定排程屬性->放入佇列等排程。 下面來分析以下gfget、gostartcallfn。 ### runtime.gfget 整體邏輯為:在p.gFree為空,sched.gFree中不空時,從後者向前者最多轉移32個。然後從前者的頭部返回一個。如果沒有分配棧幀,就分配。 ``` func gfget(_p_ *p) *g { retry: // 如果p.gFree為空,但sched.gFree中不為空,則從其中最多獲取32個 if _p_.gFree.empty() && (!sched.gFree.stack.empty() || !sched.gFree.noStack.empty()) { lock(&sched.gFree.lock) // Move a batch of free Gs to the P. for _p_.gFree.n < 32 { // Prefer Gs with stacks. gp := sched.gFree.stack.pop() if gp == nil { gp = sched.gFree.noStack.pop() if gp == nil { break } } sched.gFree.n-- _p_.gFree.push(gp) _p_.gFree.n++ } unlock(&sched.gFree.lock) goto retry } gp := _p_.gFree.pop() // 從列表頭部獲取一個g if gp == nil { return nil } _p_.gFree.n-- if gp.stack.lo == 0 { // 沒有棧就分配棧 // Stack was deallocated in gfput. Allocate a new one. systemstack(func() { gp.stack = stackalloc(_FixedStack) }) gp.stackguard0 = gp.stack.lo + _StackGuard } else { ...... } return gp } ``` ### runtime.gostartcallfn ``` func gostartcallfn(gobuf *gobuf, fv *funcval) { var fn unsafe.Pointer // fn是真正指向函式的指標 if fv != nil { fn = unsafe.Pointer(fv.fn) } else { fn = unsafe.Pointer(funcPC(nilfunc)) } gostartcall(gobuf, fn, unsafe.Pointer(fv)) } ``` ### runtime.gostartcall gostartcall主要做了兩件事: - 將 fn 偽造成是被 goexit 呼叫的 - 將 buf.pc 賦值為真正的函式指標 ``` func gostartcall(buf *gobuf, fn, ctxt unsafe.Pointer) { sp := buf.sp if sys.RegSize >
sys.PtrSize { sp -= sys.PtrSize *(*uintptr)(unsafe.Pointer(sp)) = 0 } sp -= sys.PtrSize // 為返回地址預留空間 // buf.pc 儲存的是 funcPC(goexit) + sys.PCQuantum // 將其儲存到返回地址是為了偽造成 fn 是被 goexit 呼叫的,在 fn 執行完後返回 goexit執行,做一些清理工作。 *(*uintptr)(unsafe.Pointer(sp)) = buf.pc buf.sp = sp // 重新賦值 buf.pc = uintptr(fn) // 賦值為函式指標 buf.ctxt = ct