整個程序啟動是從_rt0_amd64_darwin開始的,然后JMP到main,接著到_rt0_amd64。前面只有一點(diǎn)點(diǎn)匯編代碼,做的事情就是通過參數(shù)argc和argv等,確定棧的位置,得到寄存器。下面將從_rt0_amd64開始分析。
這里首先會設(shè)置好m->g0的棧,將當(dāng)前的SP設(shè)置為stackbase,將SP往下大約64K的地方設(shè)置為stackguard。然后會獲取處理器信息,放在全局變量runtime·cpuid_ecx和runtime·cpuid_edx中。接著,設(shè)置本地線程存儲。本地線程存儲是依賴于平臺實(shí)現(xiàn)的,比如說這臺機(jī)器上是調(diào)用操作系統(tǒng)函數(shù)thread_fast_set_cthread_self。設(shè)置本地線程存儲之后還會立即測試一下,寫入一個值再讀出來看是否正常。
這里解釋一下本地線程存儲。比如說每個goroutine都有自己的控制信息,這些信息是存放在一個結(jié)構(gòu)體G中。假設(shè)我們有一個全局變量g是結(jié)構(gòu)體G的指針,我們希望只有唯一的全局變量g,而不是g0,g1,g2...但是我們又希望不同goroutine去訪問這個全局變量g得到的并不是同一個東西,它們得到的是相對自己線程的結(jié)構(gòu)體G,這種情況下就需要本地線程存儲。g確實(shí)是一個全局變量,卻在不同線程有多份不同的副本。每個goroutine去訪問g時,都是對應(yīng)到自己線程的這一份副本。
設(shè)置好本地線程存儲之后,就可以為每個goroutine和machine設(shè)置寄存器了。這樣設(shè)置好了之后,每次調(diào)用get_tls(r),就會將當(dāng)前的goroutine的g的地址放到寄存器r中。你可以在源代碼中看到一些類似這樣的匯編:
get_tls(CX)
MOVQ g(CX), AX //get_tls(CX)之后,g(CX)得到的就是當(dāng)前的goroutine的g
不同的goroutine調(diào)用get_tls
,得到的g是本地的結(jié)構(gòu)體G的,結(jié)構(gòu)體中記錄goroutine的相關(guān)信息。
接下來的事情就非常直白,可以直接上代碼:
CLD // convention is D is always left cleared
CALL runtime·check(SB) //檢測像int8,int16,float等是否是預(yù)期的大小,檢測cas操作是否正常
MOVL 16(SP), AX // copy argc
MOVL AX, 0(SP)
MOVQ 24(SP), AX // copy argv
MOVQ AX, 8(SP)
CALL runtime·args(SB) //將argc,argv設(shè)置到static全局變量中了
CALL runtime·osinit(SB) //osinit做的事情就是設(shè)置runtime.ncpu,不同平臺實(shí)現(xiàn)方式不一樣
CALL runtime·hashinit(SB) //使用讀/dev/urandom的方式從內(nèi)核獲得隨機(jī)數(shù)種子
CALL runtime·schedinit(SB) //內(nèi)存管理初始化,根據(jù)GOMAXPROCS設(shè)置使用的procs等等
proc.c中有一段注釋,也說明了bootstrap的順序:
// The bootstrap sequence is:
//
// call osinit
// call schedinit
// make & queue new G
// call runtime·mstart
//
// The new G calls runtime·main.
先調(diào)用osinit,再調(diào)用schedinit,創(chuàng)建就緒隊(duì)列并新建一個G,接著就是mstart。這幾個函數(shù)都不太復(fù)雜。
讓我們看一下runtime.schedinit函數(shù)。該函數(shù)其實(shí)是包裝了一下其它模塊的初始化函數(shù)。有調(diào)用mallocinit,mcommoninit分別對內(nèi)存管理模塊初始化,對當(dāng)前的結(jié)構(gòu)體M初始化。
接著調(diào)用runtime.goargs和runtime.goenvs,將程序的main函數(shù)參數(shù)argc和argv等復(fù)制到了os.Args中。
也是在這個函數(shù)中,根據(jù)環(huán)境變量GOMAXPROCS決定可用物理線程數(shù)目的:
procs = 1;
p = runtime·getenv("GOMAXPROCS");
if(p != nil && (n = runtime·atoi(p)) > 0) {
if(n > MaxGomaxprocs)
n = MaxGomaxprocs;
procs = n;
}
回到前面的匯編代碼繼續(xù)看:
// 新建一個G,當(dāng)它運(yùn)行時會調(diào)用main.main
PUSHQ $runtime·main·f(SB) // entry
PUSHQ $0 // arg size
CALL runtime·newproc(SB)
POPQ AX
POPQ AX
// start this M
CALL runtime·mstart(SB)
還記得前面章節(jié)講的go關(guān)鍵字的調(diào)用協(xié)議么?先將參數(shù)進(jìn)棧,再被調(diào)函數(shù)指針和參數(shù)字節(jié)數(shù)進(jìn)棧,接著調(diào)用runtime.newproc函數(shù)。所以這里其實(shí)就是新開個goroutine執(zhí)行runtime.main。
runtime.newproc會把runtime.main放到就緒線程隊(duì)列里面。本線程繼續(xù)執(zhí)行runtime.mstart,m意思是machine。runtime.mstart會調(diào)用到調(diào)度函數(shù)schedule
schedule函數(shù)絕不返回,它會根據(jù)當(dāng)前線程隊(duì)列中線程狀態(tài)挑選一個來運(yùn)行。由于當(dāng)前只有這一個goroutine,它會被調(diào)度,然后就到了runtime.main函數(shù)中來,runtime.main會調(diào)用用戶的main函數(shù),即main.main從此進(jìn)入用戶代碼。前面已經(jīng)寫過helloworld了,用gdb調(diào)試,一步一步的跟蹤觀察這個過程。
更多建議: