前文
導(dǎo)讀
源碼分析
- rt0_go
- check
- args
- osinit
- schedinit
- newproc
- mstart
- runtime.main
環(huán)境
阿里云centOS7
go1.18
demo代碼main.go
package main
import "fmt"
func main(){
var a int = 1
fmt.Println(a)
}
實驗
1.編譯成可執(zhí)行文件(就算是利用dlv attach main.go命令也是要先編譯生成臨時文件后運(yùn)行的)
go build main.go
2.readelf查看程序入口--查找elf頭的entry point
readelf -h ./main
//Entry point address: 0x45bfc0
- dlv進(jìn)入 0x45bfc0地址(按你機(jī)器上的來)
dlv exec ./main
// (dlv)表示在dlv程序中執(zhí)行的指令
(dlv) l
( dlv) b *入口地址
(dlv) c
我們執(zhí)行" l "發(fā)現(xiàn)debug程序停留的位置實際和 給入口打斷點(diǎn)的地方一樣!4.進(jìn)入runtime/rt0_linxu_amd64.s/_rt0_amd64
//si表示執(zhí)行一行匯編指令
(dlv) si
5.進(jìn)入rt0_go
(dlv) si
rt0_go是一個很長的函數(shù)娜遵,但是我們可以通過提示直接定位到源碼位置位于runtime/asm_amd64.s文件中,直接閱讀源碼(ctrl+z掛起dlv)
cd $GOROOT/src/runtime
vim xxxxxx.s
細(xì)扒一下里面的邏輯
// copy arguments forward on an even stack
//將DI和SI壓入AX和BX寄存器
MOVQ DI, AX // argc
MOVQ SI, BX // argv
//棧頂向下移動40位(給函數(shù)棧騰出空間)
SUBQ $(5*8), SP // 3args 2auto
//15二進(jìn)制是1111,取反是0000
//相當(dāng)于把低地址4位全變?yōu)?,
//這么做的目的實際是為了內(nèi)存對齊
ANDQ $~15, SP
//argc,argv參數(shù)壓入棧,分別相對SP偏移24和32
MOVQ AX, 24(SP)
MOVQ BX, 32(SP)
// create istack out of the given (operating system) stack.
// _cgo_init may update stackguard.
//給g0分配椪两幔空間
MOVQ $runtime·g0(SB), DI
LEAQ (-64*1024+104)(SP), BX
MOVQ BX, g_stackguard0(DI)
MOVQ BX, g_stackguard1(DI)
MOVQ BX, (g_stack+stack_lo)(DI)
MOVQ SP, (g_stack+stack_hi)(DI)
上面的這些字段都是g的結(jié)構(gòu)體成員(runtime2.go)
type g struct {
stack stack
stackguarg0
stackguarg1
sched gobuf //gorutine被調(diào)走時部分信息包括sp會被保存于此
m *m
}
type stack struct {
lo uintptr
hi uintptr
}
所以實際是開辟g0函數(shù)棧,分配空間和資源(創(chuàng)建g0)
前一部分根據(jù)備注我們可以知道是確定入口參數(shù)和CPU處理信息,直到后面發(fā)現(xiàn)都是跳到ok代碼段執(zhí)行,那么直接尋找ok位置
這里再復(fù)習(xí)下m結(jié)構(gòu)
type m struct {
g0 *g
tls [tlsSlots]uintptr //線程本地私有全局變量
}
這段代碼解釋了g0和m0的相互綁定
//創(chuàng)建m0
//初始化m的tls字段用于之后綁定g0
//DI = &m0.tls
LEAQ runtime·m0+m_tls(SB), DI
CALL runtime·settls(SB)
// store through it, to make sure it works
get_tls(BX)
MOVQ $0x123, g(BX)
MOVQ runtime·m0+m_tls(SB), AX
CMPQ AX, $0x123
JEQ 2(PC)
//abort用于退出牢硅,本地線程存儲不能工作則退出
CALL runtime·abort(SB)
ok:
// set the per-goroutine and per-mach "registers"
/ /設(shè)置per-goroutine和per-mach寄存器
//綁定m0和g0的關(guān)系
get_tls(BX) //獲取fs段的基地址到bx,bx=m0.tls[0]
//lea是取址指令芝雪,把g0地址放到cx,cx=&g0
LEAQ runtime·g0(SB), CX
MOVQ CX, g(BX) //m0.tls[0] = &g0
LEAQ runtime·m0(SB), AX //m0地址給ax减余,ax=&m0
//將m0和g0通過指針進(jìn)行相互關(guān)聯(lián)
// save m->g0 = g0
//之前cx= &g0,ax=&m0,現(xiàn)在 m0.g0 = &g0
MOVQ CX, m_g0(AX) //給m0的g0字段綁定g
// save m0 to g0->m
// g0.m = &m0
MOVQ AX, g_m(CX)
至此,m0和g0創(chuàng)建并相互關(guān)聯(lián)惩系,他們是第一個線程和協(xié)程(實際對于g就算分配椕ト螅空間)趴泌,有了主線程我們就可以工作調(diào)度G啦
#接上文
CLD
//運(yùn)行時類型檢查
CALL runtime·check(SB)
....
//系統(tǒng)參數(shù)獲取,調(diào)用GO函數(shù)args
CALL runtime·args(SB)
// 相關(guān)常量初始化
CALL runtime·osinit(SB)
// 程序調(diào)度相關(guān)常量初始化
CALL runtime·schedinit(SB)
#程序馬上開始運(yùn)行
// create a new goroutine to start program
//mainPC方法(也就是runtime·main函數(shù)吆玖,是一個全局變量)壓入AX寄存器复局,
//函數(shù)是在棧上運(yùn)行的要先將地址參數(shù)壓入棧
MOVQ $runtime·mainPC(SB), AX // entry
PUSHQ AX
//調(diào)用 newproc 函數(shù)創(chuàng)建一個新的g
CALL runtime·newproc(SB)
POPQ AX
// start this M
//// 啟動這個 M.mstart 主線程
CALL runtime·mstart(SB)
// M.mstart 應(yīng)該永不返回
CALL runtime·abort(SB) // mstart should never return
RET
關(guān)于mainPC,編譯文件里是這樣描述的
// mainPC is a function value for runtime.main, to be passed to newproc.
// The reference to runtime.main is made via ABIInternal, since the
// actual function (not the ABI0 wrapper) is needed by newproc.
DATA runtime·mainPC+0(SB)/8,$runtime·main<ABIInternal>(SB)
GLOBL runtime·mainPC(SB),RODATA,$8
mainPC是runtime.main的函數(shù)值(mainPC=fn(){runtime.main})晤柄,要傳遞給newproc,對 runtime.main 的引用是通過 ABIInternal 進(jìn)行的擦剑,因為 newproc 需要實際的函數(shù)(不是 ABI0 包裝器)----即編譯器負(fù)責(zé)生成了 main 函數(shù)的入口地址,runtime.mainPC 在數(shù)據(jù)段中被定義為 runtime.main 保存主 goroutine 入口地址實驗小結(jié)
go程序不是從main.main(main包中的main函數(shù))開始執(zhí)行芥颈,也不是runtime.main抓于,而是從rt0_adm64啟動,之后調(diào)用rt0_go浇借,進(jìn)行check類型檢查捉撮,args參數(shù)傳遞,osinit系統(tǒng)基本參數(shù)設(shè)置妇垢,schedinit初始化調(diào)度器巾遭,創(chuàng)建一個新的gorutine和主線程M,調(diào)度器開始循環(huán)調(diào)度(第一個G和第一個M)
查看函數(shù)
check
rutime·check是一個GO語言的函數(shù)闯估,我們在上面打斷點(diǎn)(包名.方法名)
退出vim灼舍,喚醒掛起的dlv程序fg
重新運(yùn)行程序
(dlv) r
(dlv) b runtime.check
(dlv) c
(dlv) si //不斷重復(fù)si直到進(jìn)入check
根據(jù)上面提示我們可以直到check的位置是在runtime1.go文件中具體代碼不貼了,反正是一堆ifelse判斷檢查
args
在同文件下
func args(c int32, v **byte) {
argc = c
argv = v
//看到sys前綴就知道是調(diào)用了系統(tǒng)調(diào)用
sysargs(c, v)
}
其中argc和argv是在全局中定義的
var (
argc int32
argv **byte
)
osinit
同理利用dlv找到函數(shù)位置os_linux.go
//runtime/os_dragonfly.go
func osinit() {
// 獲取CPU核數(shù)
ncpu = getncpu()
if physPageSize == 0 {
physPageSize = getPageSize()
}
}
schedinit
dlv找到位于proc.go涨薪,太多懶得貼骑素,咱總結(jié)下偽代碼
#加鎖
_g_:=getg() // getg() *g 是獲取g,初始化獲取g0
#設(shè)置最大線程數(shù)量
#初始化棧和內(nèi)存
mcommoninit(_g_.m, -1) //看下文分析分配id和加入全局鏈表
#繼續(xù)初始化
//初始化p的個數(shù)刚夺,有多少個核就創(chuàng)建多少個P
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
procs = n
}
//procresize是初始化p
//這也是一個挺重要的函數(shù)献丑,在這里m和P被關(guān)聯(lián)末捣,總結(jié)在下面
if procresize(procs) != nil {
throw("unknown runnable goroutine during bootstrap")
}
mcomoninit也只貼個大概
func mcommoninit(mp *m, id int64) {
_g_ := getg() //初始化g0
lock(&sched.lock)
//給m分配id
if id >= 0 {
mp.id = id
} else {
mp.id = mReserveID()
}
#線程創(chuàng)建數(shù)量檢查
//把m放到全局鏈表 m.alllink(alllink是m字段) = alm
//那如果有m1,m2呢 m0.alllink指向m1.alllink再指向,鏈表嘛
mp.alllink = allm
#解鎖
procresize偽代碼(太長啦)
#初始化全局變量allp= make([]*p, nprocs)
# 循環(huán)初始化nprocs個p結(jié)構(gòu)體存放在allp中
# m和allp綁定创橄,以m0為例 m0.p =allp[0],allp[0].m = m0
#把除了allp之外的所有p放入全局變量sched的pidle空閑隊列
至此我們已經(jīng)把第一個m,g,p都創(chuàng)建且互相綁定啦
newproc
位于proc.go箩做,調(diào)用newproc創(chuàng)建了第二個G(main gorutine),這部分代碼在GMP模型的1.3中分析過
func newproc(fn *funcval) { //funcval是只包含一個地址指針的結(jié)構(gòu)體
gp := getg() //獲取當(dāng)前G指針
pc := getcallerpc() //獲取寄存器PC內(nèi)容
systemstack(func() {//創(chuàng)建新G
//真正負(fù)責(zé)初始化G
newg := newproc1(fn, gp, pc)
_p_ := getg().m.p.ptr()
//runqput之前提到過
//先嘗試進(jìn)入runnext妥畏,再本地queue邦邦,最后全局queue
runqput(_p_, newg, true)
if mainStarted {
wakep()
}
})
}
初始化時newproc的參數(shù)fn就是runtime.main函數(shù)之前在rt0.go已經(jīng)被壓入棧“MOVQ $runtime·mainPC(SB), AX ”醉蚁,“ PUSHQ AX”燃辖,newproc創(chuàng)建main gorutine綁定runtime.main,G被runqput函數(shù)放入runnext準(zhǔn)備開始循環(huán)調(diào)度
- 每個gorutine都有自己的椡鳎空間黔龟,newproc會創(chuàng)建新g來執(zhí)行fn函數(shù),在新的gorutine上執(zhí)行指令要用新的gorutine棧
mstart
其實mstart在第5章談M的創(chuàng)建的時候已經(jīng)見過了确沸,M的創(chuàng)建newm也會調(diào)用mstart,只不過現(xiàn)在這個m是為執(zhí)行runtime的main函數(shù)的
//mstart 是 new Ms 的入口點(diǎn)俘陷。它是用匯編編寫的罗捎,使用 ABI0,標(biāo)記為 TOPFRAME拉盾,并調(diào)用 mstart0桨菜。
func mstart()
func mstart0() {
_g_ := getg()
osStack := _g_.stack.lo == 0
if osStack {
//從系統(tǒng)堆棧初始化堆棧邊界。 Cgo 可能在 stack.hi 中保留了堆棧大小捉偏。 minit 可能會更新堆棧邊界倒得。注意:這些界限可能不是很準(zhǔn)確。我們將 hi 設(shè)置為 &size夭禽,但它上面還有一些東西霞掺。 1024 應(yīng)該可以彌補(bǔ)這一點(diǎn),但有點(diǎn)武斷讹躯。
size := _g_.stack.hi
if size == 0 {
size = 8192 * sys.StackGuardMultiplier
}
_g_.stack.hi = uintptr(noescape(unsafe.Pointer(&size)))
_g_.stack.lo = _g_.stack.hi - size + 1024
}
//初始化堆棧保護(hù)菩彬,以便我們可以開始調(diào)用常規(guī)
// Go code.
_g_.stackguard0 = _g_.stack.lo + _StackGuard
// 這是 g0,所以我們也可以調(diào)用 go:systemstack 函數(shù)來檢查 stackguard1潮梯。
_g_.stackguard1 = _g_.stackguard0
mstart1()
// Exit this thread.
if mStackIsSystemAllocated() {
// Windows骗灶、Solaris、illumos秉馏、Darwin耙旦、AIX 和Plan 9 總是system-allocate stack,但是在mstart 之前放在_g_.stack 中萝究,所以上面的邏輯還沒有設(shè)置osStack免都。
osStack = true
}
mexit(osStack)//幫助退出線程的
}
func mstart1() {
_g_ := getg()
if _g_ != _g_.m.g0 { // 判斷是不是g0
throw("bad runtime·mstart")
}
_g_.sched.g = guintptr(unsafe.Pointer(_g_))
_g_.sched.pc = getcallerpc() // 保存pc锉罐、sp信息到g0
_g_.sched.sp = getcallersp()
asminit() // asm初始化
minit() // m初始化
// Install signal handlers; after minit so that minit can
// prepare the thread to be able to handle the signals.
if _g_.m == &m0 {
//該部分僅在m0上運(yùn)行
mstartm0() // 啟動m0的signal handler
}
if fn := _g_.m.mstartfn; fn != nil {
fn()
}
if _g_.m != &m0 { // 如果不是m0
acquirep(_g_.m.nextp.ptr())
_g_.m.nextp = 0
}
schedule() // 進(jìn)入調(diào)度。這個函數(shù)會阻塞琴昆,最終會執(zhí)行main函數(shù)
}
func schedule() {
_g_ := getg()
...
execute(gp, inheritTime) // 在這里會執(zhí)行runtime.main
}
mstart0調(diào)用mstart1氓鄙,進(jìn)行minit()(繼續(xù)m的初始化),mstartm0(啟動m0的信號控制),schedule()進(jìn)入調(diào)度业舍,阻塞函數(shù))runtime.main
不用看就能猜到是調(diào)用用戶寫的main函數(shù)
// The main goroutine.
func main() {
g := getg()
...
// 執(zhí)行棧最大限制:1GB(64位系統(tǒng))或者 250MB(32位系統(tǒng))
if sys.PtrSize == 8 {
maxstacksize = 1000000000
} else {
maxstacksize = 250000000
}
...
// 啟動系統(tǒng)后臺監(jiān)控(定期垃圾回收抖拦、搶占調(diào)度等等)
systemstack(func() {
newm(sysmon, nil)
})
...
// 讓goroute獨(dú)占當(dāng)前線程,
// runtime.lockOSThread的用法詳見http://xiaorui.cc/archives/5320
lockOSThread()
...
// runtime包內(nèi)部的init函數(shù)執(zhí)行
runtime_init() // must be before defer
// Defer unlock so that runtime.Goexit during init does the unlock too.
needUnlock := true
defer func() {
if needUnlock {
unlockOSThread()
}
}()
// 啟動GC
gcenable()
...
// 用戶包的init執(zhí)行
main_init()
...
needUnlock = false
unlockOSThread()
...
// 執(zhí)行用戶的main主函數(shù)
main_main()
...
// 退出
exit(0)
for {
var x *int32
*x = 0
}
}
總結(jié)
參考
1.GO基礎(chǔ)啟動流程
2.GO程序的啟動和runtime初始化
3.go程序啟動過程
4.go夜讀schedule分析