1. 程式人生 > >Golang-bootstrap分析

Golang-bootstrap分析

這篇部落格主要分析golang程式的載入程式啟動流程。

1. 環境

要分析runtime相關內部機制,首先從系統啟動開始。首先準備分析環境:golang、OS、gdb
在這裡插入圖片描述

2. 載入程式巨集觀流程

在go程式碼裡面,使用者邏輯從main.main()開始,那麼runtime如何啟動?怎麼初始化?初始化做了哪些工作呢?

這裡我們從函式執行的起點開始分析。我們先編寫一個最簡單的go程式碼:

package main

import "fmt"

func main() {
	fmt.Println("hello,golang")
}

這是最簡單的一個原始碼版本了,然後生成可執行檔案,使用GDB動態檢視即可。

$go build -o test2
$gdb test2

先用go build編譯可執行檔案,然後用GDB命令gdb test2進入除錯分析原始碼介面。

使用gdb命令可以獲取到系統的Entry Point然後找到函式入口。發現是在

(gdb) info symbol 0x1051fd0
_rt0_amd64_darwin in section .text

在rt0_darwin_amd64.s裡面:

TEXT _rt0_amd64_darwin(SB),NOSPLIT,$-8
	JMP	_rt0_amd64(SB)

然後在:asm_amd64.s裡面發現了_rt0_amd64程式碼段,如下:

TEXT _rt0_amd64(SB),NOSPLIT,$-8
	MOVQ	0(SP), DI	// argc
	LEAQ	8(SP), SI	// argv
	JMP	runtime·rt0_go(SB)

最後呼叫了runtime.runtime·rt0_go,系統初始化主要邏輯也是在這個地方(已刪減不重要邏輯);
具體的原始碼見 asm_amd64.s

TEXT runtime·rt0_go(SB),NOSPLIT,$0
	......
	CALL	runtime·args(SB)
	CALL	runtime·osinit(SB)
	CALL	runtime·schedinit(SB)

	// create a new goroutine to start program
	MOVQ	$runtime·mainPC(SB), AX		// entry
	PUSHQ	AX
	PUSHQ	$0			// arg size
	CALL	runtime·newproc(SB)
	POPQ	AX
	POPQ	AX

	// start this M
	CALL	runtime·mstart(SB)

	CALL	runtime·abort(SB)	// mstart should never return
	RET

	// Prevent dead-code elimination of debugCallV1, which is
	// intended to be called by debuggers.
	MOVQ	$runtime·debugCallV1(SB), AX
	RET

在完成命令列初始化、OS初始化、排程器初始化之後。使用newproc建立一個goroutine放入待執行佇列。然後mstart讓主執行緒進入任務排程模式,從佇列提出main goroutine並執行。

bootstrap overview
bootstrap

3. 初始化流程

初始化的流程裡面,命令列初始化和OS初始化與排程器的機制關係不大,這裡就不做主要講解,這裡主要關心排程器的初始化,初始化函式schedinit()在runtime/proc.go 這個原始碼裡面。

3.1 schedule init

初始化的程式碼在proc.go裡面的schedule_init函式,原始碼如下(只保留核心流程程式碼):

// The bootstrap sequence is:
//
//	call osinit
//	call schedinit
//	make & queue new G
//	call runtime·mstart
//
// The new G calls runtime·main.
func schedinit() {
	// M最大數量限制
	sched.maxmcount = 10000
	
	// 記憶體相關初始化
	stackinit()
	mallocinit()
	// m相關初始化
	mcommoninit(_g_.m)
	// 命令引數和環境初始化
	goargs()
	goenvs()
	// GC 初始化
	gcinit()
	// 設定GOMAXPROCS
	procs := ncpu
	if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
		procs = n
	}
	if procresize(procs) != nil {
		throw("unknown runnable goroutine during bootstrap")
	}
}

排程器的初始化主要相關內容上面註釋都描述的比較清楚了,主要是初始化棧空間分配器, GC, 按cpu核心數量或GOMAXPROCS的值生成P等。

特別要主要的是生成P的操作在procresize函式裡面,如果在runtime需要更改P的數量也是呼叫這個函式。這個函式主要邏輯如下原始碼:

// Returns list of Ps with local work, they need to be scheduled by the caller.
func procresize(nprocs int32) *p {
	// Grow allp if necessary.
	if nprocs > int32(len(allp)) {
		// Synchronize with retake, which could be running
		// concurrently since it doesn't run on a P.
		lock(&allpLock)
		if nprocs <= int32(cap(allp)) {
			allp = allp[:nprocs]
		} else {
			nallp := make([]*p, nprocs)
			// Copy everything up to allp's cap so we
			// never lose old allocated Ps.
			copy(nallp, allp[:cap(allp)])
			allp = nallp
		}
		unlock(&allpLock)
	}

	// initialize new P's
	for i := int32(0); i < nprocs; i++ {
		pp := allp[i]
		if pp == nil {
			pp = new(p)
			pp.id = i
			pp.status = _Pgcstop
			pp.sudogcache = pp.sudogbuf[:0]
			for i := range pp.deferpool {
				pp.deferpool[i] = pp.deferpoolbuf[i][:0]
			}
			pp.wbBuf.reset()
			atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
		}
		if pp.mcache == nil {
			if old == 0 && i == 0 {
				if getg().m.mcache == nil {
					throw("missing mcache?")
				}
				pp.mcache = getg().m.mcache // bootstrap
			} else {
				pp.mcache = allocmcache()
			}
		}
	}

	// free unused P's
	for i := nprocs; i < old; i++ {
		p := allp[i]
		// move all runnable goroutines to the global queue
		for p.runqhead != p.runqtail {
			// pop from tail of local queue
			p.runqtail--
			gp := p.runq[p.runqtail%uint32(len(p.runq))].ptr()
			// push onto head of global queue
			globrunqputhead(gp)
		}
		if p.runnext != 0 {
			globrunqputhead(p.runnext.ptr())
			p.runnext = 0
		}
		// if there's a background worker, make it runnable and put
		// it on the global queue so it can clean itself up
		if gp := p.gcBgMarkWorker.ptr(); gp != nil {
			casgstatus(gp, _Gwaiting, _Grunnable)
			if trace.enabled {
				traceGoUnpark(gp, 0)
			}
			globrunqput(gp)
			// This assignment doesn't race because the
			// world is stopped.
			p.gcBgMarkWorker.set(nil)
		}
		// Flush p's write barrier buffer.
		if gcphase != _GCoff {
			wbBufFlush1(p)
			p.gcw.dispose()
		}
		for i := range p.sudogbuf {
			p.sudogbuf[i] = nil
		}
		p.sudogcache = p.sudogbuf[:0]
		for i := range p.deferpool {
			for j := range p.deferpoolbuf[i] {
				p.deferpoolbuf[i][j] = nil
			}
			p.deferpool[i] = p.deferpoolbuf[i][:0]
		}
		freemcache(p.mcache)
		p.mcache = nil
		gfpurge(p)
		traceProcFree(p)
		if raceenabled {
			raceprocdestroy(p.racectx)
			p.racectx = 0
		}
		p.gcAssistTime = 0
		p.status = _Pdead
		// can't free P itself because it can be referenced by an M in syscall
	}

	// Trim allp.
	if int32(len(allp)) != nprocs {
		lock(&allpLock)
		allp = allp[:nprocs]
		unlock(&allpLock)
	}

	_g_ := getg()
	if _g_.m.p != 0 && _g_.m.p.ptr().id < nprocs {
		// continue to use the current P
		_g_.m.p.ptr().status = _Prunning
	} else {
		// release the current P and acquire allp[0]
		if _g_.m.p != 0 {
			_g_.m.p.ptr().m = 0
		}
		_g_.m.p = 0
		_g_.m.mcache = nil
		p := allp[0]
		p.m = 0
		p.status = _Pidle
		acquirep(p)
	}
	var runnablePs *p
	for i := nprocs - 1; i >= 0; i-- {
		p := allp[i]
		if _g_.m.p.ptr() == p {
			continue
		}
		p.status = _Pidle
		if runqempty(p) {
			pidleput(p)
		} else {
			p.m.set(mget())
			p.link.set(runnablePs)
			runnablePs = p
		}
	}
	stealOrder.reset(uint32(nprocs))
	var int32p *int32 = &gomaxprocs // make compiler check that gomaxprocs is an int32
	atomic.Store((*uint32)(unsafe.Pointer(int32p)), uint32(nprocs))
	return runnablePs
}

主要邏輯如下:

  • 如果新的GOMAXPROCS的值大於已有的全域性變數裡面P的數量,那麼需要擴容
    • 加鎖allpLock然後重新生成新的GOMAXPROCS數量的P陣列,並將原來已有的P陣列copy過來,重新構造P陣列之後釋放allpLock鎖
  • 初始化並new新的P(GOMAXPROCS數量大於old P數量)
  • 如果GOMAXPROCS小於old P數量,那麼舊的P需要free掉,free時候涉及到一些P持有相關資源的釋放和轉移
    • move all runnable goroutines to the global queue(contain local queue and p.runnext)
    • if there’s a background worker, make it runnable and put it on the global queue so it can clean itself up
    • release related cache
    • mark P state Pdead
  • 重新賦值runtime.allp
  • 如果當前g的P仍然有效,那麼繼續使用當前P,如果當前g的P已經被release,那麼選擇runtime.allp[0]作為當前g的新P。
  • 更新runtime.allp的狀態機為Pidle

當我們更新GOMAXPROCS數量時候,排程器會被加上全域性鎖,也會出發Stop the World。所以除了系統啟動時候呼叫外,不建議在其餘地方觸發這個函式,十分影響效能。

3.2 runtime.main

完成核心初始化之後建立並執行main goroutine,其實也就是runtime.main函式。

有一個概念需要區分清楚,前面描述的初始化屬於核心層面的初始化,還有一種初始化屬於使用者層面邏輯初始化,包括runtime包、標準庫、使用者、第三方包初始化函式。邏輯層面的初始化可能關係到很多的同步關係,程式碼依賴等等。

函式的核心程式碼如下:proc.go#main

// The main goroutine.
func main() {
	// Max stack size is 1 GB on 64-bit, 250 MB on 32-bit.
	if sys.PtrSize == 8 {
		maxstacksize = 1000000000
	} else {
		maxstacksize = 250000000
	}

	// Allow newproc to start new Ms.
	mainStarted = true
	
	// 啟動後臺監控執行緒
	if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
		systemstack(func() {
			newm(sysmon, nil)
		})
	}
	// runtime包裡面的初始化函式
	runtime_init() // must be before defer
	
	// Record when the world started. Must be after runtime_init
	// because nanotime on some platforms depends on startNano.
	runtimeInitTime = nanotime()
	//啟動GC
	gcenable()
	//執行使用者、標準庫,三方包裡面的初始化函式
	fn := main_init // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
	fn()
	// 如果是庫方式就不執行使用者入口函式
	if isarchive || islibrary {
		// A program compiled with -buildmode=c-archive or c-shared
		// has a main, but it is not executed.
		return
	}
	//執行使用者入口函式
	fn = main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
	fn()
	// 退出程序
	exit(0)
	for {
		var x *int32
		*x = 0
	}
}

有幾個需要注意的點:
1)goroutine的棧最大值是1G(64位系統)
2)會啟動一個後臺監控執行緒,這個監控執行緒也是完成搶佔式排程的地方
3)目標是runtime.init、main.init、以及main.main這個入口函式

runtime.init函式和main.init由編譯器編譯生成,負責呼叫所有的初始化函式。

runtime.init函式僅僅負責runtime包,
main.init函式負責使用者、標準庫和第三方所有init函式,
所有初始化都在main goroutine中完成
全部初始化完成再執行main.main函式。

快速導覽:
在這裡插入圖片描述