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
	<autogenerated>: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 = ctxt
}

相關文章