Golang源碼學習:調度邏輯(三)工做線程的執行流程與調度循環

本文內容主要分爲三部分:函數

  1. main goroutine 的調度運行
  2. 非 main goroutine 的退出流程
  3. 工做線程的執行流程與調度循環。

main goroutine 的調度運行

runtime·rt0_go中在調用完runtime.newproc建立main goroutine後,就調用了runtime.mstart。讓咱們來分析一下這個函數。ui

mstart

mstart沒什麼太多工做,而後就調用了mstart1。atom

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獲取一個可執行的gdebug

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。3d

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主要作了如下的工做:code

  • 啓動監控進程。
  • 調用runtime的初始化函數。
  • 開啓gc。
  • 調用main的初始化函數。
  • 調用main.main,執行完後退出。

非 main goroutine 的退出流程

首先明確一點,不管是main goroutine仍是非main goroutine的都是調用newproc建立的,因此在調度上基本是一致的。blog

以前的文章中說過,在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 從新調度

工做線程的執行流程與調度循環

如下給出一個工做線程的執行流程簡圖:

能夠看到工做線程的執行是從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從新調度。
相關文章
相關標籤/搜索