前一篇文章大致介紹了Go語言調(diào)度的各個方面至耻,這篇文章通過介紹源碼來進一步了解調(diào)度的一些過程悉稠。源碼是基于最新的Go 1.12疮装。
Go的編譯方式是靜態(tài)編譯托酸,把runtime本身直接編譯到了最終的可執(zhí)行文件里。
入口是系統(tǒng)和平臺架構(gòu)對應(yīng)的rt0_[OS]_[arch].s
(runtime文件夾下)不跟,這是一段匯編代碼颓帝,做一些初始化工作,例如初始化g,新建一個線程等购城,然后會調(diào)用runtime.rt0_go(runtime/asm_[arch].s中)吕座。
runtime.rt0_go會繼續(xù)檢查cpu信息,設(shè)置好程序運行標(biāo)志瘪板,tls(thread local storage)初始化等吴趴,設(shè)置g0與m0的相互引用,然后調(diào)用runtime.args侮攀、runtime.osinit(os_[arch].go)锣枝、runtime.schedinit(proc.go),在runtime.schedinit會調(diào)用stackinit()兰英, mallocinit()等初始化棧撇叁,內(nèi)存分配器等等。接下來調(diào)用runtime.newproc(proc.go)創(chuàng)建新的goroutine用于執(zhí)行runtime.main進而綁定用戶寫的main方法畦贸。runtime.mstart(proc.go)啟動m0開始goroutine的調(diào)度(也就是執(zhí)行main函數(shù)的線程就是m0陨闹?)。
// 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()
}
sched.maxmcount = 10000
tracebackinit()
moduledataverify()
stackinit()
mallocinit()
mcommoninit(_g_.m)
cpuinit() // must run before alginit
alginit() // maps must not be used before this call
modulesinit() // provides activeModules
typelinksinit() // uses maps, activeModules
itabsinit() // uses activeModules
有些文章會提到m0
和g0
家制。上文提到的匯編中新建的第一個線程就是m0正林,它在全局變量中, 無需再heap上分配颤殴,是一個脫離go本身內(nèi)存分配機制的存在觅廓。而m0中的g0也是全局變量,上面提到的runtime.rt0_go中設(shè)置了很多g0的各個成員變量涵但。但同時每個之后創(chuàng)建的m也都有自己的g0杈绸,負(fù)責(zé)調(diào)度而不是執(zhí)行用戶程序里面的函數(shù)。
runtime.main
上文講到創(chuàng)建的goroutine會執(zhí)行runtime.main進而執(zhí)行main.main從而開啟用戶寫的程序部分的運行矮瘟。
這個函數(shù)在proc.go中:
// The main goroutine.
func main() {
g := getg()
// Racectx of m0->g0 is used only as the parent of the main goroutine.
// It must not be used for anything else.
g.m.g0.racectx = 0
// Max stack size is 1 GB on 64-bit, 250 MB on 32-bit.
// Using decimal instead of binary GB and MB because
// they look nicer in the stack overflow failure message.
if sys.PtrSize == 8 {
maxstacksize = 1000000000
} else {
maxstacksize = 250000000
}
// Allow newproc to start new Ms.
mainStarted = true
這個函數(shù)會標(biāo)記mainStarted從而表示newproc能創(chuàng)建新的M了瞳脓,創(chuàng)建新的M來啟動sysmon函數(shù)(gc相關(guān),g搶占調(diào)度相關(guān))澈侠,調(diào)用runtime_init劫侧,gcenable等,如果是作為c的類庫編譯哨啃,這時就退出了烧栋。作為go程序,就繼續(xù)執(zhí)行main.main函數(shù)拳球,這就是用戶自己定義的程序了审姓。等用戶寫的程序執(zhí)行完,如果發(fā)生了panic則等待panic處理祝峻,最后exit(0)退出魔吐。
runtime.newproc (G的創(chuàng)建)
runtime.newproc函數(shù)本身比較簡單扎筒,傳入兩個參數(shù),其中siz是funcval+額外參數(shù)的長度酬姆,fn是指向函數(shù)機器代碼的指針嗜桌。過程只是獲取參數(shù)的起始地址和調(diào)用段返回地址的pc寄存器。然后通過systemstack調(diào)用newproc1來實現(xiàn)G的創(chuàng)建和入隊轴踱。
func newproc(siz int32, fn *funcval) {
argp := add(unsafe.Pointer(&fn), sys.PtrSize)
gp := getg()
pc := getcallerpc()
systemstack(func() {
newproc1(fn, (*uint8)(argp), siz, gp, pc)
})
}
systemstack
會切換當(dāng)前的g到g0(每個m里專門用于調(diào)度的g)症脂,然后調(diào)用newproc1谚赎。
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
_g_ := getg()
if fn == nil {
_g_.m.throwing = -1 // do not dump full stacks
throw("go of nil func value")
}
_g_.m.locks++ // disable preemption because it can be holding p in a local var
siz := narg
siz = (siz + 7) &^ 7
...
runtime.newproc1
做的事情大概包括:
- 獲取當(dāng)前的G(也就是G0)淫僻,并使綁定的M不可搶占,獲取M對應(yīng)的P
- 獲群健(或新建)一個G:
- 通過gfget從P的gfree鏈表里獲取G
- 獲取不到則調(diào)用malg分配一個G雳灵,初始棧2K,設(shè)置G的狀態(tài)為_Gdead闸盔,這樣gc不會掃描這個G悯辙。然后把G放入全局的G隊列里
- 參數(shù)和返回地址復(fù)制到G的棧上
- 設(shè)置G的調(diào)度信息(sched)
- 設(shè)置G的狀態(tài)為_Grunnable
- 調(diào)用runqput把G放入隊列等待運行:
- 嘗試把G放到P的runnext
- 嘗試把G放到P的runq(本地運行隊列)
- 如果P的runq滿了則調(diào)用runqputslow把G放入全局隊列sched中(本地隊列的一半G放入,而不是一次放一個)
- 檢查:如果無自旋的M但是有空閑的P迎吵,則喚醒或新建一個M躲撰。這本身跟創(chuàng)建G已經(jīng)無關(guān)了,主要是保證有足夠的M來運行G击费。
- 喚醒或新建M通過wakeup函數(shù)
- 釋放不可搶占狀態(tài)
runtime.mstart (M對G的執(zhí)行)
M調(diào)用的的函數(shù)拢蛋。m0在初始化后調(diào)用,其他m在線程啟動時調(diào)用蔫巩。
函數(shù)在proc.go中谆棱,處理大致如下:
- 調(diào)用getg獲取當(dāng)前的G,會得到g0
- 如果g未分配椩沧校空間垃瞧,從系統(tǒng)棧空間分配
- 調(diào)用mstart1
- 檢查坪郭,g不是g0就報錯
- 調(diào)用save保存當(dāng)前狀態(tài)个从,以后每次調(diào)度從這個棧地址開始
- 執(zhí)行asminit,minit歪沃,設(shè)置當(dāng)前線程可以接收的信號
- 調(diào)用schedule函數(shù)嗦锐,開始調(diào)度。
-
schedule
是調(diào)度的核心- 獲取g绸罗,檢查是否lock意推,是則報錯
- 如果m被某個g鎖住(locked to a g)珊蟀,則等待那個g能執(zhí)行
- 如果是cgo菊值,也報錯
- 然后才進入主要的循環(huán):
- 如果gc需要stw(stop the world)外驱,那么用stopm休眠當(dāng)前的m
- m的p指定了需要在安全點運行的函數(shù),就運行它
- 獲取特定的幾種g腻窒,一旦獲取到昵宇,就跳過獲取階段了:
- 有trace(參考go tool trace)相關(guān)的g,執(zhí)行
- gc標(biāo)記階段儿子,有待運行的gc worker(也是一個g)瓦哎,執(zhí)行
- 每61次調(diào)度,從全局g隊列中獲取g柔逼。主要是為公平起見蒋譬,防止全局g一直不執(zhí)行
- 從p本地獲取,調(diào)用runqget
- 沒有獲取到愉适,則調(diào)用findrunnable獲取
- 檢查gc的stw犯助,安全點運行函數(shù)
- 有finalizer相關(guān)的g,運行
- 從q的本地隊列中取维咸, runqget
- 從全局隊列中取剂买,globrunqget,需要鎖
- 用netpoll獲取可運行的g(見下面netpoll相關(guān)說明)癌蓖,這一步非必須瞬哼,可以跳過
- 還是沒獲取到的話, 檢查有沒有其他p有g(shù)(查看npidle)租副;檢查自旋的M和忙碌的P的數(shù)量(為啥代碼里乘以2坐慰?),如果M多則當(dāng)前M可以停了附井;設(shè)置當(dāng)前M為自旋狀態(tài)讨越,然后隨機從其他p偷一半g過來(work steal算法)
- 上面的異常分支或者最終沒有偷到g,都會導(dǎo)致m進入休眠(findrunnable的stop部分)永毅,休眠步驟是:
- 如果在gc標(biāo)記把跨,看有沒有g(shù)c worker,運行沼死。有trace相關(guān)着逐,也要處理
- gc需要stw,或者p有安全運行點函數(shù)意蛀,重新跳到findrunnable的開始執(zhí)行
- 再次檢查全局隊列是否有G耸别,有則獲取并返回
- 釋放P,P的狀態(tài)變?yōu)開Pidle县钥。P被添加到空閑列表
- 讓M離開自旋狀態(tài)秀姐,然后再次找所有P的本地隊列,GC worker等若贮,找到就跳到findrunnable頂部重新執(zhí)行
- 最終獲取不到G省有,則休眠當(dāng)前的M痒留,調(diào)用的是stopm
- 如果之后被喚醒,跳到findrunnable頂部重新執(zhí)行
- 繼續(xù)執(zhí)行則表示找到了帶運行的G
- 如果M在自旋蠢沿,讓M離開自旋狀態(tài)伸头,resetspinning
- 如果找到的G要求回到指定的M運行(lockedm != 0,例如runtime.main)
- 調(diào)用startlockedm把G和P交給那個M舷蟀,自己進入休眠
- 自己從休眠中醒過來的時候恤磷,跳到schedule的主循環(huán)頭部,執(zhí)行
- 調(diào)用execute函數(shù)執(zhí)行G(這塊我寫簡單點野宜,因為主要是G本身的設(shè)置)
- 獲取當(dāng)前G扫步,設(shè)置狀態(tài)從Grunnable到Grunning
- 增加對應(yīng)的P中記錄的調(diào)用次數(shù)(為了61倍數(shù)次的時候從全局隊列取)
- 對應(yīng)g和m
- 調(diào)用gogo(匯編)函數(shù),這個函數(shù)根據(jù)g.sched中保存的狀態(tài)恢復(fù)各個寄存器中的值并開始(對應(yīng)g剛創(chuàng)建)或繼續(xù)(對應(yīng)g中斷之后又執(zhí)行)運行g(shù)速缨。設(shè)置寄存器的狀態(tài)锌妻,然后函數(shù)執(zhí)行完返回的時候調(diào)用goexit(因為newproc1中設(shè)置了返回為goexit)代乃。
- goexit本身的調(diào)用鏈?zhǔn)牵篻oexit(匯編)-> goexit1(proc.go)-> mcall(匯編)-> goexit0(proc.go)旬牲。而mcall會保存運行狀態(tài)到g.sched,然后切換到g0搁吓,再調(diào)用goexit0原茅。
- goexit0會把G的狀態(tài)從Grunning設(shè)置為Gdead,清理G的各個成員堕仔,解除M和G的關(guān)系并把G放到P的自由列表(GFree)中方便下次復(fù)用擂橘,最后調(diào)用schedule函數(shù),讓M繼續(xù)運行其他待運行的G
M的小結(jié)
上面的過程摩骨,是最基本的創(chuàng)建G和創(chuàng)建M的過程通贞。其中可以看到M的創(chuàng)建或喚醒主要包含在3個地方:
- runtime.newproc1的最后,入隊G之后恼五,如果無自旋轉(zhuǎn)的M但有空閑的P昌罩,則喚醒或創(chuàng)建一個M(wakep)
- M獲取到G,離開自旋狀態(tài)的時候(在schedule中)灾馒,如果當(dāng)前無自旋的M但有空閑的P茎用,就喚醒或創(chuàng)建一個M(wakep)
- M取不到待執(zhí)行的G的時候,離開自旋狀態(tài)準(zhǔn)備休眠時(在findrunnable的stop部分)睬罗,再次檢查有沒有可運行的G轨功,有則重新進入findrunnable(從而再次進入自旋狀態(tài))
- channel喚醒G的時候,無自旋M有空閑P容达,則喚醒或創(chuàng)建M
wakep函數(shù)也位于proc.go中:
func wakep() {
// be conservative about spinning threads
if !atomic.Cas(&sched.nmspinning, 0, 1) {
return
}
startm(nil, true)
}
- 原子交換nmspinning為1古涧,保證多個線程執(zhí)行wakep只有一個成功
- 調(diào)用startm:
- 從空閑列表獲取P,沒有則結(jié)束
- 從空閑列表獲取M(mget)花盐,沒有則調(diào)用newm創(chuàng)建羡滑。newm調(diào)用allocm創(chuàng)建M圆米,會包含g0,然后調(diào)用newm1進而調(diào)用newosproc創(chuàng)建線程(天書般的代碼)
- 調(diào)用notewakeup喚醒線程
G的小結(jié)
上面說了G從創(chuàng)建啄栓,到退出的過程娄帖。然而實際執(zhí)行的時候, 并不是這樣“一帆風(fēng)順”的昙楚。有很多情況會導(dǎo)致G在執(zhí)行過程中“中斷”近速。下面會大致介紹這些情況,但并不具體展開(因為代碼實在太多堪旧,每個都可以單獨形成一篇文章了)削葱。
搶占
每個M并不是執(zhí)行一個G到完成再執(zhí)行下一個,而是可能發(fā)生搶占淳梦。但是又不像操作系統(tǒng)的線程有時間片的概念析砸。搶占由sysmon(runtime.main里面創(chuàng)建的)觸發(fā),調(diào)用的是retake函數(shù)爆袍,這里不再詳細(xì)按代碼說明首繁,只說個大概:
- 對于每個P,如果P在系統(tǒng)調(diào)用Psyscall且超過一次sysmon循環(huán)陨囊,搶占這個P弦疮,解除M和P的關(guān)系(handoffp)
- 對于每個P,如果P在運行Prunning蜘醋,且超過一次sysmon循環(huán)且G的運行時間超過了一定值胁塞,搶占這個P,設(shè)置g.stackguard0為stackPreempt压语。這個值會在G調(diào)用函數(shù)的時候觸發(fā)morestack啸罢,然后經(jīng)過一系列復(fù)雜的檢查,再調(diào)用gopreempt_m完成搶占胎食。
gopreempt_m調(diào)用goschedImpl:
- 設(shè)置G從Grunning到Grunnable
- 解綁G和M
- 把G放到全局隊列
- 調(diào)用schedule函數(shù)扰才,讓M繼續(xù)執(zhí)行
搶占可以保證一個G不會長時間運行導(dǎo)致其他G餓死。前提是這個G要調(diào)用函數(shù)斥季,因為搶占在調(diào)用函數(shù)的時候才能檢測出來训桶。
channel
channel收發(fā)時可能會“阻塞”,導(dǎo)致G從Grunning變成Gwaiting酣倾,并與M解綁舵揭,M繼續(xù)調(diào)用schedule函數(shù)。
網(wǎng)絡(luò)調(diào)用
為了效率躁锡,go的網(wǎng)絡(luò)調(diào)用采用了異步方式epoll或kqueue等午绳,當(dāng)網(wǎng)絡(luò)調(diào)用讀寫數(shù)據(jù)的時候,G也可能被“阻塞”映之,從而被調(diào)度拦焚。
補充說明
上面介紹代碼的時候蜡坊,提到了G,M赎败,P使用中用到的很多屬性秕衙,這些定義在runtime2.go中。
type g struct {
// Stack parameters.
// stack describes the actual stack memory: [stack.lo, stack.hi).
// stackguard0 is the stack pointer compared in the Go stack growth prologue.
// It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
// stackguard1 is the stack pointer compared in the C stack growth prologue.
// It is stack.lo+StackGuard on g0 and gsignal stacks.
// It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).
stack stack // offset known to runtime/cgo
stackguard0 uintptr // offset known to liblink
stackguard1 uintptr // offset known to liblin
...
}
type m struct {
g0 *g // goroutine with scheduling stack
morebuf gobuf // gobuf arg to morestack
divmod uint32 // div/mod denominator for arm - known to liblin
...
}
type p struct {
lock mutex
id int32
status uint32 // one of pidle/prunning/...
link puintpt
...
}
參考:
- https://github.com/JerryZhou/golang-doc/blob/master/Golang-Internals/Part-6.Bootstrapping.and.Memory.Allocator.Initialization.md
- https://studygolang.com/articles/11627
- http://cbsheng.github.io/posts/%E6%8E%A2%E7%B4%A2goroutine%E7%9A%84%E5%88%9B%E5%BB%BA/
- https://making.pusher.com/go-tool-trace/
- https://tonybai.com/2017/06/23/an-intro-about-goroutine-scheduler/