本文是《Go語言調度器源代碼情景分析》系列的第13篇,也是第二章的第3小節。函數
上一節咱們分析了調度器的初始化,這一節咱們來看程序中的第一個goroutine是如何建立的。ui
接上一節,schedinit完成調度系統初始化後,返回到rt0_go函數中開始調用newproc() 建立一個新的goroutine用於執行mainPC所對應的runtime·main函數,看下面的代碼:this
runtime/asm_amd64.s : 197spa
# create a new goroutine to start program MOVQ $runtime·mainPC(SB), AX# entry,mainPC是runtime.main # newproc的第二個參數入棧,也就是新的goroutine須要執行的函數 PUSHQ AX # AX = &funcval{runtime·main}, # newproc的第一個參數入棧,該參數表示runtime.main函數須要的參數大小,由於runtime.main沒有參數,因此這裏是0 PUSHQ $0 CALL runtime·newproc(SB) # 建立main goroutine POPQ AX POPQ AX # start this M CALL runtime·mstart(SB) # 主線程進入調度循環,運行剛剛建立的goroutine # 上面的mstart永遠不該該返回的,若是返回了,必定是代碼邏輯有問題,直接abort CALL runtime·abort(SB)// mstart should never return RET DATA runtime·mainPC+0(SB)/8,$runtime·main(SB) GLOB Lruntime·mainPC(SB),RODATA,$8
在後面的分析過程當中咱們會看到這個runtime.main最終會調用咱們寫的main.main函數,在分析runtime·main以前咱們先把重點放在newproc這個函數上。線程
newproc函數用於建立新的goroutine,它有兩個參數,先說第二個參數fn,新建立出來的goroutine將從fn這個函數開始執行,而這個fn函數可能也會有參數,newproc的第一個參數正是fn函數的參數以字節爲單位的大小。好比有以下go代碼片斷:3d
func start(a, b, c int64) { ...... } func main() { go start(1, 2, 3) }
編譯器在編譯上面的go語句時,就會把其替換爲對newproc函數的調用,編譯後的代碼邏輯上等同於下面的僞代碼指針
func main() { push 0x3 push 0x2 push 0x1 runtime.newproc(24, start) }
編譯器編譯時首先會用幾條指令把start函數須要用到的3個參數壓棧,而後調用newproc函數。由於start函數的3個int64類型的參數共佔24個字節,因此傳遞給newproc的第一個參數是24,表示start函數須要24字節大小的參數。code
那爲何須要傳遞fn函數的參數大小給newproc函數呢?緣由就在於newproc函數將建立一個新的goroutine來執行fn函數,而這個新建立的goroutine與當前這個goroutine會使用不一樣的棧,所以就須要在建立goroutine的時候把fn須要用到的參數先從當前goroutine的棧上拷貝到新的goroutine的棧上以後才能讓其開始執行,而newproc函數自己並不知道須要拷貝多少數據到新建立的goroutine的棧上去,因此須要用參數的方式指定拷貝多少數據。對象
瞭解完這些背景知識以後,下面咱們開始分析newproc的代碼。newproc函數是對newproc1的一個包裝,這裏最重要的準備工做有兩個,一個是獲取fn函數第一個參數的地址(代碼中的argp),另外一個是使用systemstack函數切換到g0棧,固然,對於咱們這個初始化場景來講如今原本就在g0棧,因此不須要切換,然而這個函數是通用的,在用戶的goroutine中也會建立goroutine,這時就須要進行棧的切換。blog
runtime/proc.go : 3232
// Create a new g running fn with siz bytes of arguments. // Put it on the queue of g's waiting to run. // The compiler turns a go statement into a call to this. // Cannot split the stack because it assumes that the arguments // are available sequentially after &fn; they would not be // copied if a stack split occurred. //go:nosplit func newproc(siz int32, fn *funcval) { //函數調用參數入棧順序是從右向左,並且棧是從高地址向低地址增加的 //注意:argp指向fn函數的第一個參數,而不是newproc函數的參數 //參數fn在棧上的地址+8的位置存放的是fn函數的第一個參數 argp := add(unsafe.Pointer(&fn), sys.PtrSize) gp:= getg() //獲取正在運行的g,初始化時是m0.g0 //getcallerpc()返回一個地址,也就是調用newproc時由call指令壓棧的函數返回地址, //對於咱們如今這個場景來講,pc就是CALLruntime·newproc(SB)指令後面的POPQ AX這條指令的地址 pc := getcallerpc() //systemstack的做用是切換到g0棧執行做爲參數的函數 //咱們這個場景如今自己就在g0棧,所以什麼也不作,直接調用做爲參數的函數 systemstack(func() { newproc1(fn, (*uint8)(argp), siz, gp, pc) }) }
newproc1函數的第一個參數fn是新建立的goroutine須要執行的函數,注意這個fn的類型是funcval結構體類型,其定義以下:
type funcval struct{ fn uintptr // variable-size, fn-specific data here }
newproc1的第二個參數argp是fn函數的第一個參數的地址,第三個參數是fn函數的參數以字節爲單位的大小,後面兩個參數咱們不用關心。這裏須要注意的是,newproc1是在g0的棧上執行的。該函數很長也很重要,因此咱們分段來看。
runtime/proc.go : 3248
// 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) { //由於已經切換到g0棧,因此不管什麼場景都有 _g_ = g0,固然這個g0是指當前工做線程的g0 //對於咱們這個場景來講,當前工做線程是主線程,因此這裏的g0 = m0.g0 _g_ := getg() ...... _p_ := _g_.m.p.ptr() //初始化時_p_ = g0.m.p,從前面的分析能夠知道其實就是allp[0] newg := gfget(_p_) //從p的本地緩衝裏獲取一個沒有使用的g,初始化時沒有,返回nil if newg == nil { //new一個g結構體對象,而後從堆上爲其分配棧,並設置g的stack成員和兩個stackgard成員 newg = malg(_StackMin) casgstatus(newg, _Gidle, _Gdead) //初始化g的狀態爲_Gdead //放入全局變量allgs切片中 allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack. } ...... //調整g的棧頂置針,無需關注 totalSize := 4*sys.RegSize+uintptr(siz) +sys.MinFrameSize// extra space in case of reads slightly beyond frame totalSize += -totalSize&(sys.SpAlign-1) // align to spAlign sp := newg.stack.hi-totalSize spArg := sp ...... if narg > 0 { //把參數從執行newproc函數的棧(初始化時是g0棧)拷貝到新g的棧 memmove(unsafe.Pointer(spArg), unsafe.Pointer(argp), uintptr(narg)) // ...... }
這段代碼主要從堆上分配一個g結構體對象併爲這個newg分配一個大小爲2048字節的棧,並設置好newg的stack成員,而後把newg須要執行的函數的參數從執行newproc函數的棧(初始化時是g0棧)拷貝到newg的棧,完成這些事情以後newg的狀態以下圖所示:
咱們能夠看到,通過前面的代碼以後,程序中多了一個咱們稱之爲newg的g結構體對象,該對象也已經得到了從堆上分配而來的2k大小的棧空間,newg的stack.hi和stack.lo分別指向了其棧空間的起止位置。
接下來咱們繼續分析newproc1函數。
runtime/proc.go : 3314
//把newg.sched結構體成員的全部成員設置爲0 memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched)) //設置newg的sched成員,調度器須要依靠這些字段才能把goroutine調度到CPU上運行。 newg.sched.sp = sp //newg的棧頂 newg.stktopsp = sp //newg.sched.pc表示當newg被調度起來運行時從這個地址開始執行指令 //把pc設置成了goexit這個函數偏移1(sys.PCQuantum等於1)的位置, //至於爲何要這麼作須要等到分析完gostartcallfn函數才知道 newg.sched.pc = funcPC(goexit) + sys.PCQuantum// +PCQuantum so that previous instruction is in same function newg.sched.g = guintptr(unsafe.Pointer(newg)) gostartcallfn(&newg.sched, fn)//調整sched成員和newg的棧
這段代碼首先對newg的sched成員進行了初始化,該成員包含了調度器代碼在調度goroutine到CPU運行時所必須的一些信息,其中sched的sp成員表示newg被調度起來運行時應該使用的棧的棧頂,sched的pc成員表示當newg被調度起來運行時從這個地址開始執行指令,然而從上面的代碼能夠看到,new.sched.pc被設置成了goexit函數的第二條指令的地址而不是fn.fn,這是爲何呢?要回答這個問題,必須深刻到gostartcallfn函數中作進一步分析。
// adjust Gobuf as if it executed a call to fn // and then did an immediate gosave. func gostartcallfn(gobuf *gobuf, fv *funcval) { var fn unsafe.Pointer if fv != nil { fn = unsafe.Pointer(fv.fn) //fn: gorotine的入口地址,初始化時對應的是runtime.main } else { fn = unsafe.Pointer(funcPC(nilfunc)) } gostartcall(gobuf, fn, unsafe.Pointer(fv)) }
gostartcallfn首先從參數fv中提取出函數地址(初始化時是runtime.main),而後繼續調用gostartcall函數。
// adjust Gobuf as if it executed a call to fn with context ctxt // and then did an immediate gosave. func gostartcall(buf *gobuf, fn, ctxt unsafe.Pointer) { sp := buf.sp//newg的棧頂,目前newg棧上只有fn函數的參數,sp指向的是fn的第一參數 if sys.RegSize > sys.PtrSize { sp -= sys.PtrSize *(*uintptr)(unsafe.Pointer(sp)) = 0 } sp -= sys.PtrSize//爲返回地址預留空間, //這裏在假裝fn是被goexit函數調用的,使得fn執行完後返回到goexit繼續執行,從而完成清理工做 *(*uintptr)(unsafe.Pointer(sp)) = buf.pc//在棧上放入goexit+1的地址 buf.sp = sp//從新設置newg的棧頂寄存器 //這裏才真正讓newg的ip寄存器指向fn函數,注意,這裏只是在設置newg的一些信息,newg還未執行, //等到newg被調度起來運行時,調度器會把buf.pc放入cpu的IP寄存器, //從而使newg得以在cpu上真正的運行起來 buf.pc = uintptr(fn) buf.ctxt = ctxt }
gostartcall函數的主要做用有兩個:
調整newg的棧空間,把goexit函數的第二條指令的地址入棧,僞形成goexit函數調用了fn,從而使fn執行完成後執行ret指令時返回到goexit繼續執行完成最後的清理工做;
從新設置newg.buf.pc 爲須要執行的函數的地址,即fn,咱們這個場景爲runtime.main函數的地址。
調整完成newg的棧和sched成員以後,返回到newproc1函數,咱們繼續往下看,
newg.gopc = callerpc //主要用於traceback newg.ancestors = saveAncestors(callergp) //設置newg的startpc爲fn.fn,該成員主要用於函數調用棧的traceback和棧收縮 //newg真正從哪裏開始執行並不依賴於這個成員,而是sched.pc newg.startpc = fn.fn ...... //設置g的狀態爲_Grunnable,表示這個g表明的goroutine能夠運行了 casgstatus(newg, _Gdead, _Grunnable) ...... //把newg放入_p_的運行隊列,初始化的時候必定是p的本地運行隊列,其它時候可能由於本地隊列滿了而放入全局隊列 runqput(_p_, newg, true) ...... }
newproc1函數最後這點代碼比較直觀,首先設置了幾個與調度無關的成員變量,而後修改newg的狀態爲_Grunnable並把其放入了運行隊列,到此程序中第一個真正意義上的goroutine已經建立完成。
這時newg也就是main goroutine的狀態以下圖所示:
這個圖看起來比較複雜,由於表示指針的箭頭實在是太多了,這裏對其稍做一下解釋。
首先,main goroutine對應的newg結構體對象的sched成員已經完成了初始化,圖中只顯示了pc和sp成員,pc成員指向了runtime.main函數的第一條指令,sp成員指向了newg的棧頂內存單元,該內存單元保存了runtime.main函數執行完成以後的返回地址,也就是runtime.goexit函數的第二條指令,預期runtime.main函數執行完返回以後就會去執行runtime.exit函數的CALL runtime.goexit1(SB)這條指令;
其次,newg已經放入與當前主線程綁定的p結構體對象的本地運行隊列,由於它是第一個真正意義上的goroutine,尚未其它goroutine,因此它被放在了本地運行隊列的頭部;
最後,newg的m成員爲nil,由於它尚未被調度起來運行,也就沒有跟任何m進行綁定。
這一節咱們分析了程序中第一個goroutine也就是main goroutine的建立,下一節咱們繼續分析它是怎麼被主工做線程調度到CPU上去執行的。