main的啟動過程
main的啟動過程
參考連結
重點應解答以下問題
- go程式的大致啟動過程?
- 一個go程式預設到底是幾個goroutines?分別承擔什麼角色?
- go程式入口是什麼?中間大致需要有哪些準備?
- 涉及到的重點檔案或重點function?分別實現了什麼功能?
- 進階:gorutine 的"一生"
1. go 的出生
- 環境:
- 作業系統:CentOS Linux release 7.8.2003 (Core)
- go版本:go version go1.13.11 linux/amd64
- gdb版本:GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-119.el7
1.1 示例
-
編寫 main.go
package main import ( "fmt" ) func main() { fmt.Println("hello,world") }
-
編譯
go build -gcflags "-N -l" -o hello src/main.go
-gcflags"-N -l"
是為了關閉編譯器優化和函式內聯,防止後面在設定斷點的時候找不到相對應的程式碼位置。得到了可執行檔案 main。
1.2 gdb 除錯
1.2.1 info files
-
執行 info files
-
Entry point: 0x454d30
檔案執行的記憶體入口地址,由其值可看到入口在 .text (401000 - 48ce19) 段中。
-
.text
程式碼段(codesegment/textsegment)通常是指用來存放程式執行程式碼的一塊記憶體區域。這部分割槽域的大小在程式執行前就已經確定,並且記憶體區域通常屬於只讀,某些架構也允許程式碼段為可寫,即允許修改程式。在程式碼段中,也有可能包含一些只讀的常數變數,例如字串常量等。 -
.rodata
存放字串和#define定義的常量 -
.data
資料段(datasegment)通常是指用來存放程式中已初始化的全域性變數的一塊記憶體區域。資料段屬於靜態記憶體分配。 -
.bss
BSS段(bsssegment)通常是指用來存放程式中未初始化的全域性變數的一塊記憶體區域。BSS是英文BlockStarted by Symbol的簡稱。BSS段屬於靜態記憶體分配。…
-
1.2.2 新增斷點
-
設定斷點
b _rt0_amd64_linux b _rt0_amd64 b runtime.check b runtime.args b runtime.osinit b runtime.schedinit b runtime.newproc b runtime.mstart b main.main b runtime.exit
-
列印斷點設定資訊如下
1.3 列印斷點
Breakpoint 1:_rt0_amd64_linux
-
檢視 Breakpoint 1 資訊
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-ahr3x9pw-1608406838359)(main%E5%90%AF%E5%8A%A8%E8%BF%87%E7%A8%8B.assets/image-20201118005003864.png)]
由 Address 一列可以看到, Breakpoint 1 就是我們的程式入 口地址,其檔案為
src/runtime/rt0_linux_amd64.s
。由此可見,go程式啟動不是直接進入main.main
函式,而是需要進行命令列處理,執行時初始化等工作,這些工作基本由go提供的 runtime 執行,之後才會進入使用者邏輯。go的runtime 包下面有各種不同名稱的程式入口檔案,支援各種作業系統和架構,筆者機器為x86_64,因此入口檔案是rt0_linux_amd64.s。
-
執行 Breakpoint 1
JMP _rt0_amd64(SB)
進入_rt0_amd64
, 即Breakpoint 2。
Breakpoint 2:_rt0_amd64
_rt0_amd64
在檔案asm_amd64.s
內,這裡 LEAQ 是計算記憶體地址,然後把記憶體地址本身放進暫存器裡,也就是把 argv 的地址放到了SI
暫存器中。我們可以看一下程式碼檔案 asm_amd64.s
看註釋可知,_rt0_amd64 是大多數amd64系統的常用啟動程式碼。 這是核心中普通 -buildmode = exe 程式的程式入口點。 堆疊儲存引數數量argv和C風格的argv。然後呼叫 runtime.rt0_go
。
runtime.rt0_go
做了什麼呢?繼續往下看該檔案,runtime.rt0_go
也在asm_amd64.s
檔案中,它其實已經大概展示了一個go程式的一生。
- 依次呼叫了 Breakpoint 3-6 的幾個函式來完成初始化和執行時啟動等一系列工作。
- 呼叫 Breakpoint 7的
runtime.newproc
新建gorutine,並繫結我們所編寫的main - 呼叫 Breakpoint 8 的
runtime.mstart
來獲取 m ,執行我們的main函式,即Breakpoint9的 main_main。 - 最終在
main_main
執行完後返回runtime.main
通過exit(0)呼叫runtime.exit
正常結束程式,runtime.abort(SB)
是程式異常時呼叫退出。(mstart should never return)
Breakpoint 3:runtime.check
runtime.rt0_go
中,首先是呼叫runtime·check(SB)
,該函式實際是go程式碼了,主要是 CPU 相關的特性標誌位檢查的程式碼,以及對一些型別大小進行檢查等。
Breakpoint 4:runtime.args
然後是runtime·args(SB)
,作用就是將引數(argc 和 argv )儲存到靜態變數中。其實執行到runtime.sysargs
就知道 c 和 v 其實就是argc和argv 了。
Breakpoint 5:runtime·osinit
接下來是runtime·osinit(SB)
,比較容易看出來,這個函式初始化了 CPU 邏輯核數和記憶體頁大小。
Breakpoint 6:runtime.schedinit
接下來是runtime·schedinit(SB)
,顧名思義,它幹了一大堆初始化排程器的事,包括:初始化命令列引數、環境變數、gc、棧空間、記憶體管理、P 例項、HASH演算法等,幾乎所有執行環境的初始化都在這裡執行了。
這裡我們具體看一下schedinit的程式碼:
// The bootstrap sequence is:
//
// call osinit
// call schedinit
// make & queue new G
// call runtime·mstart
//
// The new G calls runtime·main.
func schedinit() {
// raceinit must be the first call to race detector.
// In particular, it must be done before mallocinit below calls racemapshadow.
_g_ := getg()
if raceenabled {
_g_.racectx, raceprocctx0 = raceinit() // 初始化競態檢測器
}
// 最大系統執行緒數量限制,這也說明了go是多執行緒模型。具體檢視 runtime/debug.SetMaxThreads
sched.maxmcount = 10000 //sched結構體位於rutime2.go中
// 初始棧跟蹤
tracebackinit()
// 模組資料校驗
moduledataverify()
// 初始化棧空間
stackinit()
// 初始化記憶體分配器
mallocinit()
// 初始化 m
mcommoninit(_g_.m)
// 初始化cpu:cpu個數;cpu特性
cpuinit()
// 初始化AES hash演算法
alginit()
// 模組初始化
// src\runtime\symtab.go
// 當一個module被動態連結器首次載入時,.init_array被呼叫。
// 在一個Go程式宣告週期內,有兩種場景會發生module的載入。
// 1.以-buildmode=shared 模式編譯,程式初始化會載入。
// 2.-buildmode=plugin模式編譯,程式執行時載入。
// 約束:1.modulesinit一次只能有一個goroutine呼叫。
modulesinit()
// 使用 map 建立型別符號表
typelinksinit()
// itab初始化
itabsinit()
msigsave(_g_.m)
initSigmask = _g_.m.sigmask
goargs() // 處理命令列引數
goenvs() // 獲取環境變數
parsedebugvars() // 處理 GODEBUG?GOTRACEBACK 除錯相關的環境變數設定
gcinit() //垃圾回收初始化
sched.lastpoll = uint64(nanotime())
//設定最大p數量上限值, 如果設定了環境變數,"GOMAXPROCS",則設定為環境變數設定值。
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
procs = n
}
if procresize(procs) != nil { // 調整p的數量
throw("unknown runnable goroutine during bootstrap")
}
...
}
Breakpoint 7:runtime.newproc
實際上,我們使用關鍵字開啟執行緒的時候入口就是
runtime.newproc
,即go func(){}實際上newproc(func(){})的呼叫。fn就是我們所編寫的使用者程式碼
runtime·newproc(SB)
則使用系統棧(即systemstack)新建一個 goroutine。systemstack會切換到系統棧,然後呼叫傳入函式,具體如下:
- g0是執行緒繫結的。
- 如果是g0,或者gsignal goroutine,則直接呼叫呼叫傳入的函式。
- 否則,先切換到g0棧,再執行傳入函式。
- 執行完畢,切換回撥用者棧。
我們一步步執行看一下newproc1具體工作(這裡省去一些if判斷):
// Create a new g running fn with narg bytes of arguments starting
// at argp. callerpc is the address of the go statement that created
// this. The new g is put on the queue of g's waiting to run.
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
_g_ := getg()
_p_ := _g_.m.p.ptr()
// 從本地gFree獲取一個g物件,若gFree為空,全域性allgs非空,則從allgs獲取32個
// 到gFree並返回一個;若全域性為空,則返回nil(go啟動過程返回nil)
newg := gfget(_p_)
if newg == nil {
newg = malg(_StackMin) // 新建 gorutine
casgstatus(newg, _Gidle, _Gdead) // newg狀態: _Gidle -> _Gdead
allgadd(newg) // 新增到全域性 allgs []*g
}
totalSize := 4*sys.RegSize + uintptr(siz) + sys.MinFrameSize
totalSize += -totalSize & (sys.SpAlign - 1)
sp := newg.stack.hi - totalSize // 棧頂
spArg := sp
memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
newg.sched.sp = sp
newg.stktopsp = sp
newg.sched.pc = funcPC(goexit) + sys.PCQuantum
newg.sched.g = guintptr(unsafe.Pointer(newg))
gostartcallfn(&newg.sched, fn) // 準備goroutine執行環節,將gobuf.pc設定為fn。
newg.gopc = callerpc // 儲存呼叫者go語句地址
newg.ancestors = saveAncestors(callergp) // 儲存呼叫者的g的地址
newg.startpc = fn.fn
newg.gcscanvalid = false
casgstatus(newg, _Gdead, _Grunnable) // newg狀態: _Gdead -> _Grunnable
runqput(_p_, newg, true) // 將新g放入本地待執行佇列。p.runq
// 有空閒p && 沒有m處於自旋狀態 && main已啟動, 則喚醒(mainStarted在runtime.main執行後置為true)
if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted {
wakep()
// wakep 邏輯:
// 若無空閒p,即sched.pidle為空,則什麼也不做。
// 若有空閒p:
// 無空閒m,則新建一個m,將m與p關聯,新建一個系統執行緒,執行goroutine函式。
// 有空閒m,則喚醒該m執行緒
}
}
Breakpoint 8:runtime.mstart
runtime.mstart
新建並啟動M,開始排程goroutine,到此,彙編引導全部完成,剩下的由golang實現。對於啟動過程來說,第一個被排程的goroutine就是runtime.main()
協程本程了,函式呼叫鏈如下:
runtime.mstart -> runtime.mstart1 -> schedule() -> execute() -> gogo() -> runtime.main()
這裡我們不深究golang協程排程器如何實現,只關心runtime.main做了什麼。具體如下總結就是限定棧大小、將主goroutine鎖定到主OS執行緒上、啟動gc相關的協程、執行所有系統庫相關包內和使用者自定義的init函式,然後就進入使用者main函式
。
runtime.main程式碼如下:
// The main goroutine.
func main() {
g := getg()
// 棧大小上限: 1 GB on 64-bit, 250 MB on 32-bit.
if sys.PtrSize == 8 {
maxstacksize = 1000000000
} else {
maxstacksize = 250000000
}
// 允許 newproc 開始新的 M.
mainStarted = true
// 將主goroutine鎖定到主OS執行緒上
lockOSThread()
// 執行runtime包內所有init函式
doInit(&runtime_inittask)
// 啟動了bgsweep goroutine和scavenger goroutine,負責gc
// bgsweep 負責垃圾回收
// scavenger 負責mhead(堆記憶體)的回收
gcenable()
// 執行使用者程式碼的init函式
doInit(&main_inittask)
needUnlock = false
unlockOSThread()
// 執行main_main,即我們編寫的main函式
fn := main_main
fn() // 執行main_main,即執行到了Breakpoint 9
// 呼叫runtime.exit,退出程式
exit(0)
}
Breakpoint 9:main.main
現在,正式進入我們寫的main函式了,main所在gorutine獲得cpu執行許可權,PID為31678,至此main啟動完成
Breakpoint 10:runtime.exit
最終,main執行結束,返回runtime.mstart
,在runtime.mstart
呼叫runtime.exit結束程式;
2. 即問即答
過程走完了,有點亂,通過下面幾個問題我們來整理一下…
2.1 go程式的大致啟動過程
答:如我們除錯過程所示,斷點的設定即代表了go啟動的大致過程。
2.2 一個go程式預設到底是幾個goroutines?分別承擔什麼角色?
答:預設5個執行緒,角色如下:
main:使用者主協程
forcegchelper:監控計時器觸發垃圾回收
bgsweep: 負責垃圾回收的併發執行
scavenger: 負責mhead(堆記憶體)的回收(真正執行記憶體釋放操作?)
finalizer: 專門執行最終附加到物件的所有終結器(Finalizer)
可以逐步除錯
runtime.main
程式碼,通過info goroutines
和goroutine [gid] bt
檢視協程資訊
2.3 涉及到的重點檔案或重點function?分別實現了什麼功能?
function | 位置 | 功能 |
---|---|---|
_rt0_amd64_linux | runtime.rt0_linux_amd64.s | 入口檔案 |
_rt0_amd64 | runtime.asm_amd64.s | amd64系統的常用啟動程式碼,go程式的一生都展示在這個function裡了 |
func check() | runtime.runtime1.go | CPU 相關的特性標誌位檢查的程式碼,以及對一些型別大小進行檢查等(比如我們不同字長機器上每種型別的位元組大小的確定就是在here了,簡單理解為適配機器) |
func args(c int32, v **byte) | runtime.runtime1.go | 將引數(argc 和 argv )儲存到靜態變數中 |
func osinit() | runtime.os_linux.go | 初始化了 CPU 邏輯核數和記憶體頁大小 |
func schedinit() | runtime.proc.go | 初始化排程器的事,包括:初始化命令列引數、環境變數、gc、棧空間、記憶體管理、P 例項、HASH演算法等 |
func newproc(siz int32, fn *funcval) | runtime.proc.go | 新建協程(即執行程式碼go func() 的入口,fn 為該協程的函式地址) |
func mstart() | runtime.proc.go | mstart是新M的入點(每個M有一個g0進行排程等工作) |
runtime.main | runtime.proc.go | 限定棧大小、將主goroutine鎖定到主OS執行緒上、啟動gc相關協程、執行系統庫相關包和使用者自定義的init函式、執行使用者main函式 |
2.4 進階:gorutine 的"一生"
未完待續…
相關文章
- Angular的啟動過程Angular
- Nginx的啟動過程Nginx
- Oracle的啟動過程Oracle
- iOS main()執行前的過程 + weak 置 nil的過程iOSAI
- Windows 啟動過程Windows
- app的啟動過程(三)APP
- App 啟動過程(含 Activity 啟動過程) | 安卓 offer 收割基APP安卓
- 根Activity元件的啟動過程元件
- Android Service的啟動過程Android
- Android Activity的啟動過程Android
- Oracle啟動的三個過程Oracle
- iOS App 的完整啟動過程iOSAPP
- oracle的內部啟動過程Oracle
- Linux的啟動過程(轉)Linux
- Service啟動過程分析
- Activity啟動過程分析
- linux啟動過程Linux
- 【LINUX】啟動過程Linux
- iOS App啟動過程iOSAPP
- Windows啟動過程(MBR引導過程分析)Windows
- 一張圖弄清Activity的啟動過程
- 走近原始碼:Redis的啟動過程原始碼Redis
- Cypress 本身啟動過程的除錯除錯
- Slackware的啟動(init)過程(轉)
- Oracle-解析啟動的全過程Oracle
- Linux的啟動過程介紹Linux
- 作業系統啟動的過程作業系統
- Android App啟動過程AndroidAPP
- 計算機啟動過程計算機
- Liferay 啟動過程分析
- Spring Boot 啟動過程Spring Boot
- HDFS啟動過程+安全模式模式
- Eureka Server啟動過程分析Server
- Linux 啟動過程分析Linux
- Spring啟動過程(一)Spring
- ORACLE啟動過程淺析Oracle
- ORACLE啟動過程簡析Oracle
- 資料庫啟動過程資料庫