Golang原始碼學習:排程邏輯(二)main goroutine的建立
阿新 • • 發佈:2020-05-25
接上一篇繼續分析一下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