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
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函式。
快速導覽: