本人的源碼是基於go 1.9.7 版本的哦!
緊接着之前寫的 【我的區塊鏈之路】- golang源碼分析之select的底層實現 和 【我的區塊鏈之路】- golang源碼分析之channel的底層實現 我們這一次需要對go的調度器做一番剖析。
go的調度器只要實現在 runtime 包中,路徑爲: ./src/runtime/proc.go 文件中。
我們都知道go的強大是因爲可以起很多 goroutine 也即是我們所說的協程。那麼協程和線程有什麼聯繫呢?協程又是如何調度的呢?
在逼逼這些東西之前,我們先了解下,go語言其實是在操作系統提供的內核線程之上搭建了一個特有得 【兩級線程】模型。下面再說兩級線程模型前,有三個必知的核心元素。(G、M、P)
G:Goroutine的縮寫,一個G代表了對一段需要被執行的Go語言代碼的封裝
M:Machine的縮寫,一個M代表了一個內核線程
P:Processor的縮寫,一個P代表了M所需的上下文環境
簡單的來說,一個G的執行需要M和P的支持。一個M在與一個P關聯之後形成了一個有效的G運行環境【內核線程 + 上下文環境】。每個P都會包含一個可運行的G的隊列 (runq )。
好了下面我們來具體的看看 G、M、P
M (machine):
M是machine的頭文字, 在當前版本的golang中等同於系統線程.
M可以運行兩種代碼:
- go代碼, 即goroutine, M運行go代碼需要一個P
- 原生代碼, 例如阻塞的syscall, M運行原生代碼不需要P
M會從運行隊列中取出G, 然後運行G, 如果G運行完畢或者進入休眠狀態, 則從運行隊列中取出下一個G運行, 週而復始。
有時候G需要調用一些無法避免阻塞的原生代碼, 這時M會釋放持有的P並進入阻塞狀態, 其他M會取得這個P並繼續運行隊列中的G.
go需要保證有足夠的M可以運行G, 不讓CPU閒着, 也需要保證M的數量不能過多。通常創建一個M的原因是由於沒有足夠的M來關聯P並運行其中可運行的G。而且運行時系統執行系統監控的時候,或者GC的時候也會創建M。
M的結構體定義:(在 ./src/runtime/runtime2.go 文件中)
// M 結構體
type m struct {
/*
1. 所有調用棧的Goroutine,這是一個比較特殊的Goroutine。
2. 普通的Goroutine棧是在Heap分配的可增長的stack,而g0的stack是M對應的線程棧。
3. 所有調度相關代碼,會先切換到該Goroutine的棧再執行。
*/
g0 *g // goroutine with scheduling stack
morebuf gobuf // gobuf arg to morestack
divmod uint32 // div/mod denominator for arm - known to liblink
// Fields not known to debuggers.
procid uint64 // for debuggers, but offset not hard-coded
gsignal *g // signal-handling g
goSigStack gsignalStack // Go-allocated signal handling stack
sigmask sigset // storage for saved signal mask
tls [6]uintptr // thread-local storage (for x86 extern register)
mstartfn func() //
curg *g // M 正在運行的結構體G
caughtsig guintptr // goroutine running during fatal signal
p puintptr // attached p for executing go code (nil if not executing go code)
nextp puintptr
id int32
mallocing int32
throwing int32
preemptoff string // if != "", keep curg running on this m
locks int32
softfloat int32
dying int32
profilehz int32
helpgc int32
spinning bool // m is out of work and is actively looking for work
blocked bool // m is blocked on a note
inwb bool // m is executing a write barrier
newSigstack bool // minit on C thread called sigaltstack
printlock int8
incgo bool // m is executing a cgo call
fastrand uint32
ncgocall uint64 // number of cgo calls in total
ncgo int32 // number of cgo calls currently in progress
cgoCallersUse uint32 // if non-zero, cgoCallers in use temporarily
cgoCallers *cgoCallers // cgo traceback if crashing in cgo call
park note
alllink *m // on allm
schedlink muintptr
mcache *mcache
lockedg *g // 表示與當前M鎖定那個g
createstack [32]uintptr // stack that created this thread.
freglo [16]uint32 // d[i] lsb and f[i]
freghi [16]uint32 // d[i] msb and f[i+16]
fflag uint32 // floating point compare flags
locked uint32 // tracking for lockosthread
nextwaitm uintptr // next m waiting for lock
needextram bool
traceback uint8
waitunlockf unsafe.Pointer // todo go func(*g, unsafe.pointer) bool
waitlock unsafe.Pointer
waittraceev byte
waittraceskip int
startingtrace bool
syscalltick uint32
thread uintptr // thread handle
// these are here because they are too large to be on the stack
// of low-level NOSPLIT functions.
libcall libcall
libcallpc uintptr // for cpu profiler
libcallsp uintptr
libcallg guintptr
syscall libcall // stores syscall parameters on windows
mOS
}
M的字段衆多,其中最重要的爲下面四個:
g0: Go運行時系統在啓動之初創建的,用於執行一些運行時任務。
mstartfn:表示M的起始函數。其實就是我們 go 語句攜帶的那個函數啦。
curg:存放當前正在運行的G的指針。
p:指向當前與M關聯的那個P。
nextp:用於暫存於當前M有潛在關聯的P。 (預聯)當M重新啓動時,即用預聯的這個P做關聯啦
spinning:表示當前M是否正在尋找G。在尋找過程中M處於自旋狀態。
lockedg:表示與當前M鎖定的那個G。運行時系統會把 一個M 和一個G鎖定,一旦鎖定就只能雙方相互作用,不接受第三者。
M並沒有像G和P一樣的狀態標記, 但可以認爲一個M有以下的狀態:
- 自旋中(spinning): M正在從運行隊列獲取G, 這時候M會擁有一個P
- 執行go代碼中: M正在執行go代碼, 這時候M會擁有一個P
- 執行原生代碼中: M正在執行原生代碼或者阻塞的syscall, 這時M並不擁有P
- 休眠中: M發現無待運行的G時會進入休眠, 並添加到空閒M鏈表中, 這時M並不擁有P
自旋中(spinning)這個狀態非常重要, 是否需要喚醒或者創建新的M取決於當前自旋中的M的數量。
M在被創建之初會被加入到全局的M列表 【runtime.allm】 。接着,M的起始函數(mstartfn)和準備關聯的P(p)都會被設置。最後,運行時系統會爲M專門創建一個新的內核線程並與之關聯。這時候這個新的M就爲執行G做好了準備。其中起始函數(mstartfn)僅當運行時系統要用此M執行系統監控或者垃圾回收等任務的時候纔會被設置。全局M列表的作用是運行時系統在需要的時候會通過它獲取到所有的M的信息,同時防止M被gc。
在新的M被創建後回西安做一番初始化工作。其中包括了對自身所持的棧空間以及信號做處理的初始化。在上述初始化完成後 mstartfn 函數就會被執行 (如果存在的話)。【注意】:如果mstartfn 代表的是系統監控任務的話,那麼該M會一直在執行mstartfn 而不會有後續的流程。否則 mstartfn 執行完後,當前M將會與那個準備與之關聯的P完成關聯。至此,一個併發執行環境才真正完成。之後就是M開始尋找可運行的G並運行之。
運行時系統管轄的M會在GC任務執行的時候被停止,這時候系統會對M的屬性做某些必要的重置並把M放置入調度器的空閒M列表。【很重要】因爲在需要一個未被使用的M時,運行時系統會先去這個空閒列表獲取M。(只有都沒有的時候纔會創建M)
M本身是無狀態的。M是否有空閒僅以它是否存在於調度器的空閒M列表 【runtime.sched.midle】 中爲依據 (空閒列表不是那個全局列表哦)。
單個Go程序所使用的M的最大數量是可以被設置的。在我們使用命令運行Go程序時候,有一個引導程序先會被啓動的。在這個歌引導程序中會爲Go程序的運行簡歷必要的環境。引導程序對M的數量進行初始化設置,默認是 最大值 1W 【即是說,一個Go程序最多可以使用1W個M,即:理想狀態下,可以同時有1W個內核線程被同時運行】。使用 runtime/debug.SetMaxThreads() 函數設置。
P (process):
P是process的頭文字, 代表M運行G所需要的資源。
一些講解協程的文章把P理解爲cpu核心, 其實這是錯誤的.
雖然P的數量默認等於cpu核心數, 但可以通過環境變量GOMAXPROC
修改, 在實際運行時P跟cpu核心並無任何關聯。
P也可以理解爲控制go代碼的並行度的機制,
如果P的數量等於1, 代表當前最多隻能有一個線程(M)執行go代碼,
如果P的數量等於2, 代表當前最多隻能有兩個線程(M)執行go代碼.
執行原生代碼的線程數量不受P控制。
因爲同一時間只有一個線程(M)可以擁有P, P中的數據都是鎖自由(lock free)的, 讀寫這些數據的效率會非常的高。
P是使G能夠在M中運行的關鍵。Go運行時系統適當地讓P與不同的M建立或者斷開聯繫,以使得P中的那些可運行的G能夠在需要的時候及時獲得運行時機。
P的結構體定義:(在 ./src/runtime/runtime2.go 文件中)
type p struct {
lock mutex
id int32
status uint32 // one of pidle/prunning/...
link puintptr
schedtick uint32 // incremented on every scheduler call
syscalltick uint32 // incremented on every system call
sysmontick sysmontick // last tick observed by sysmon
m muintptr // back-link to associated m (nil if idle)
mcache *mcache
racectx uintptr
deferpool [5][]*_defer // pool of available defer structs of different sizes (see panic.go)
deferpoolbuf [5][32]*_defer
// Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen.
goidcache uint64
goidcacheend uint64
// Queue of runnable goroutines. Accessed without lock.
runqhead uint32
runqtail uint32
runq [256]guintptr
// runnext, if non-nil, is a runnable G that was ready'd by
// the current G and should be run next instead of what's in
// runq if there's time remaining in the running G's time
// slice. It will inherit the time left in the current time
// slice. If a set of goroutines is locked in a
// communicate-and-wait pattern, this schedules that set as a
// unit and eliminates the (potentially large) scheduling
// latency that otherwise arises from adding the ready'd
// goroutines to the end of the run queue.
runnext guintptr
// Available G's (status == Gdead)
gfree *g
gfreecnt int32
sudogcache []*sudog
sudogbuf [128]*sudog
tracebuf traceBufPtr
// traceSweep indicates the sweep events should be traced.
// This is used to defer the sweep start event until a span
// has actually been swept.
traceSweep bool
// traceSwept and traceReclaimed track the number of bytes
// swept and reclaimed by sweeping in the current sweep loop.
traceSwept, traceReclaimed uintptr
palloc persistentAlloc // per-P to avoid mutex
// Per-P GC state
gcAssistTime int64 // Nanoseconds in assistAlloc
gcBgMarkWorker guintptr
gcMarkWorkerMode gcMarkWorkerMode
// gcw is this P's GC work buffer cache. The work buffer is
// filled by write barriers, drained by mutator assists, and
// disposed on certain GC state transitions.
gcw gcWork
runSafePointFn uint32 // if 1, run sched.safePointFn at next safe point
pad [sys.CacheLineSize]byte
}
通過runtime.GOMAXPROCS函數我們可以改變單個Go程序可以間擁有的P的最大數量。
P的最大數量相當於是對可以被併發執行的用戶級的G的數量作出限制。
每一個P都必須關聯一個M才能使其中的G得以運行。
【注意】:運行時系統會將M與關聯的P分離開來。但是如果該P的可運行隊列中還有未運行的G,那麼運行時系統就會找到一個空的M (在調度器的空閒隊列中的M) 或者創建一個空的M,並與該P關聯起來(爲了運行G而做準備)。
runtime.GOMAXPROCS函數設置的只會影響P的數量,但是對M (內核線程)的數量不會影響,所以runtime.GOMAXPROCS 並不是控制線程數,只能說是影響上下文環境P的數目。
在Go程序開始運行時,會先由引導程序對M做了數量上的限制,及對P做了限制,P的數量默認爲1。所以我們無論在程序中使用go關鍵字啓用多少goroutine,它們都會被塞到一個P的可運行G隊列中。
在確認P的最大數量後,運行時系統會根據這個數值初始化全局的P列表 【runtime.allp】,類似全局M列表,其中包含了所有 運行時系統創建的所有P。隨後,運行時系統會把調度器的可運行G隊列【runtime.sched.runq】中的所有G均勻的放入全局的P列表中的各個P的可執行G隊列當中。到這裏爲止,運行時系統需要用到的所有P都準備就緒了。
類似M的空閒列表,調度器也存在一個P的空閒列表【runtime.sched.pidle】,當一個P不再與任何M關聯的時候,運行時系統就會把該P放入這個列表中,而一個空閒的P關聯了某個M之後會被從這個列表中取出【注意:就算一個P加入了空閒隊列,但是它的可運行G隊列不一定爲空】
和M不同P是有狀態的:(五種)
Pidle:當前P未和任何M關聯
Prunning:當前P正在和某個M關聯
Psyscall:當前P中的被運行的那個G正在進行系統調用
Pgcstop:運行時系統正在進行gc。(運行時系統在gc時會試圖把全局P列表中的P都處於此狀態)
Pdead:當前P已經不再被使用。(在調用runtime.GOMAXPROCS減少P的數量時,多餘的P就處於此狀態)
P的初始狀態就是爲Pgcstop,處於這個狀態很短暫,在初始化和填充P中的G隊列之後,運行時系統會將其狀態置爲Pidle並放入調度器的空閒P列表 (runtime.sched.pidle)中。其中的P會由調度器根據實際情況進行取用。下圖是P在各個狀態建的流轉情況:
從上圖,我們可以看出,除了Pdead之外的其他狀態的P都會在運行時系統欲進行GC是被指爲Pgcstop。在gc結束後狀態不會回覆到之前的狀態的,而是都統一直接轉到了Pidle 【這意味着,他們都需要被重新調度】。【注意】:除了Pgcstop 狀態的P,其他狀態的P都會在 調用runtime.GOMAXPROCS 函數去減少P數目時,被認爲是多餘的P而狀態轉爲Pdead,這時候其帶的可運行G的隊列中的G都會被轉移到 調度器的可運行G隊列中,它的自由G隊列 【gfree】也是一樣被移到調度器的自由列表 【runtime.sched.gfree】中。
【注意】:每個P中都有一個可運行G隊列及自由G隊列。自由G隊列包含了很多已經完成的G,隨着被運行完成的G的積攢到一定程度後,運行時系統會把其中的部分G轉移的調度器的自由G隊列 【runtime.sched.gfree】中。
【注意】:當我們每次用 go關鍵字 啓用一個G的時候,運行時系統都會先從P的自由G隊列獲取一個G來封裝我們提供的函數 (go 關鍵字後面的函數) ,如果發現P中的自由G過少時,會從調度器的自由G隊列中移一些G過來,只有連調度器的自由G列表都彈盡糧絕的時候,纔會去創建新的G。
G (goroutine):
G是goroutine的頭文字, goroutine可以解釋爲受管理的輕量線程, goroutine使用go
關鍵詞創建。
舉例來說, func main() { go other() }
, 這段代碼創建了兩個goroutine。
一個是main, 另一個是other, 【注意】:main本身也是一個goroutine。
goroutine的新建, 休眠, 恢復, 停止都受到go運行時的管理。
goroutine執行異步操作時會進入休眠狀態, 待操作完成後再恢復, 無需佔用系統線程。
goroutine新建或恢復時會添加到運行隊列, 等待M取出並運行。
G的結構體定義:(在 ./src/runtime/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 liblink
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
m *m // current m; offset known to arm liblink 當前運行G的M
sched gobuf // goroutine切換時,用於保存g的上下文
syscallsp uintptr // if status==Gsyscall, syscallsp = sched.sp to use during gc
syscallpc uintptr // if status==Gsyscall, syscallpc = sched.pc to use during gc
stktopsp uintptr // expected sp at top of stack, to check in traceback
param unsafe.Pointer // passed parameter on wakeup 用於傳遞參數,睡眠時其他goroutine可以設置param,喚醒時該goroutine可以獲取
atomicstatus uint32
stackLock uint32 // sigprof/scang lock; TODO: fold in to atomicstatus
goid int64 // goroutine的ID
waitsince int64 // approx time when the g become blocked g被阻塞的大體時間
waitreason string // if status==Gwaiting
schedlink guintptr
preempt bool // preemption signal, duplicates stackguard0 = stackpreempt
paniconfault bool // panic (instead of crash) on unexpected fault address
preemptscan bool // preempted g does scan for gc
gcscandone bool // g has scanned stack; protected by _Gscan bit in status
gcscanvalid bool // false at start of gc cycle, true if G has not run since last scan; TODO: remove?
throwsplit bool // must not split stack
raceignore int8 // ignore race detection events
sysblocktraced bool // StartTrace has emitted EvGoInSyscall about this goroutine
sysexitticks int64 // cputicks when syscall has returned (for tracing)
traceseq uint64 // trace event sequencer
tracelastp puintptr // last P emitted an event for this goroutine
lockedm *m // G被鎖定只在這個m上運行
sig uint32
writebuf []byte
sigcode0 uintptr
sigcode1 uintptr
sigpc uintptr
gopc uintptr // pc of go statement that created this goroutine
startpc uintptr // pc of goroutine function
racectx uintptr
waiting *sudog // sudog structures this g is waiting on (that have a valid elem ptr); in lock order
cgoCtxt []uintptr // cgo traceback context
labels unsafe.Pointer // profiler labels
timer *timer // cached timer for time.Sleep
// Per-G GC state
// gcAssistBytes is this G's GC assist credit in terms of
// bytes allocated. If this is positive, then the G has credit
// to allocate gcAssistBytes bytes without assisting. If this
// is negative, then the G must correct this by performing
// scan work. We track this in bytes to make it fast to update
// and check for debt in the malloc hot path. The assist ratio
// determines how this corresponds to scan work debt.
gcAssistBytes int64
}
// 用於保存G切換時上下文的緩存結構體
type gobuf struct {
// The offsets of sp, pc, and g are known to (hard-coded in) libmach.
//
// ctxt is unusual with respect to GC: it may be a
// heap-allocated funcval so write require a write barrier,
// but gobuf needs to be cleared from assembly. We take
// advantage of the fact that the only path that uses a
// non-nil ctxt is morestack. As a result, gogo is the only
// place where it may not already be nil, so gogo uses an
// explicit write barrier. Everywhere else that resets the
// gobuf asserts that ctxt is already nil.
sp uintptr // 當前的棧指針
pc uintptr // 計數器
g guintptr // g自身
ctxt unsafe.Pointer // this has to be a pointer so that gc scans it
ret sys.Uintreg
lr uintptr
bp uintptr // for GOEXPERIMENT=framepointer
}
下面我們來講講G。Go語言的編譯器會把我們編寫的go語句編程一個運行時系統的函數調用,並把go語句中那個函數及其參數都作爲參數傳遞給這個運行時系統函數中。
運行時系統在接到這樣一個調用後,會先檢查一下go函數及其參數的合法性,緊接着會試圖從本地P的自由G隊列中(或者調度器的自由G隊列)中獲取一個可用的自由G (P中有講述了),如果沒有則新創建一個G。類似M和P,G在運行時系統中也有全局的G列表【runtime.allg】,那些新建的G會先放到這個全局的G列表中,其列表的作用也是集中放置了當前運行時系統中給所有的G的指針。在用自由G封裝go的函數時,運行時系統都會對這個G做一次初始化。
初始化:包含了被關聯的go關鍵字後的函數及當前G的狀態機G的ID等等。在G被初始化完成後就會被放置到當前本地的P的可運行隊列中。只要時機成熟,調度器會立即盡心這個G的調度運行。
G的各種狀態:
Gidle:G被創建但還未完全被初始化。
Grunnable:當前G爲可運行的,正在等待被運行。
Grunning:當前G正在被運行。
Gsyscall:當前G正在被系統調用
Gwaiting:當前G正在因某個原因而等待
Gdead:當前G完成了運行
正在被初始化進行中的G是處於Grunnable狀態的。一個G真正被使用是在狀態爲Grunnable之後。G的生命週期及狀態變化如圖:
圖上有一步是事件到來,那麼G在運行過程中,是否等待某個事件以及等待什麼樣的事件?完全由起封裝的go關鍵字後的函數決定。(如:等待chan中的值、涉及網絡I/O、time.Timer、time.Sleep等等事件)
G退出系統調用,及其複雜:運行時系統先會嘗試直接運行當前G,僅當無法被運行時纔會轉成Grunnable並放置入調度器的自由G列表中。
最後,已經是Gdead狀態的G是可以被重新初始化並使用的。而對比進入Pdead狀態的P等待的命運只有被銷燬。處於Gdead的G會被放置到本地P或者調度器的自由G列表中。
至此,G、M、P的初步描述已經完畢,下面我們來看一看一些核心的隊列:
中文名 | 源碼的名稱 | 作用域 | 簡要說明 |
全局M列表 | runtime.allm | 運行時系統 | 存放所有M |
全局P列表 | runtime.allp | 運行時系統 | 存放所有P |
全局G列表 | runtime.allg | 運行時系統 | 存放所有G |
調度器中的空閒M列表 | runtime.sched.midle | 調度器 | 存放空閒M |
調度器中的空閒P列表 | runtime.sched.pidle | 調度器 | 存放空閒P |
調度器中的可運行G隊列 | runtime.sched.runq | 調度器 | 存放可運行G |
調度器中那個的自由G列表 | runtime.sched.gfree | 調度器 | 存放自由G |
P的可運行G隊列 | runq | 本地P | 存放當前P中的可運行G |
P中的自由G列表 | gfree | 本地P | 存放當前P中的自由G |
三個全局的列表主要爲了統計運行時系統的的所有G、M、P。我們主要關心剩下的這些容器,尤其是和G相關的四個。
在運行時系統創建的G都會被保存在全局的G列表中,值得注意的是:從Gsyscall轉出來的G,都會被放置到調度器的可運行G隊列中。而被運行時系統初始化的G會被放置到本地P的可運行列表中。從Gwaiting轉出來的G,除了因網絡I/O陷入等待的G之外,都會被放置到本地P的可運行G隊列中。轉成Gdead狀態的G會先被放置到本地P的自由G列表 (上面的描述可以知道這一點)。調度器中的與G、M、P相關的列表其實只是起了一個暫存的作用。
一句話概括三者關係:
- G需要綁定在M上才能運行;
- M需要綁定P才能運行;
下面我們看一看三者及內核調度實體【KSE】的關係:
綜上所述,一個G的執行需要M和P的支持。一個M在於一個P關聯之後就形成一個有效的G運行環境 【內核線程 + 上下文環境】。每個P都含有一個 可運行G的隊列【runq】。隊列中的G會被一次傳遞給本地P關聯的M並且獲得運行時機。
由上圖可以看出 M 與 KSE 總是 一對一 的。一個M能且僅能代表一個內核線程。
一個M的生命週期內,它會且僅會與一個KSE產生關聯。M與P以及P與G之間的關聯是多變的,總是會隨着實際調度的過程而改變。其中, M 與 P 總是一對一,P 與 G 總是 一對多, 而 一個 G 最終由 一個 M 來負責運行。
上述我們講的運行時系統其實就是我們下面要說的調度器。
我們再來回顧下G、M、P 中的主要成員:
G裏面比較重要的成員:
- stack: 當前g使用的棧空間, 有lo和hi兩個成員
- stackguard0: 檢查棧空間是否足夠的值, 低於這個值會擴張棧, 0是go代碼使用的
- stackguard1: 檢查棧空間是否足夠的值, 低於這個值會擴張棧, 1是原生代碼使用的
- m: 當前g對應的m
- sched: g的調度數據, 當g中斷時會保存當前的pc和rsp等值到這裏, 恢復運行時會使用這裏的值
- atomicstatus: g的當前狀態
- schedlink: 下一個g, 當g在鏈表結構中會使用
- preempt: g是否被搶佔中
- lockedm: g是否要求要回到這個M執行, 有的時候g中斷了恢復會要求使用原來的M執行
M裏面比較重要的成員:
- g0: 用於調度的特殊g, 調度和執行系統調用時會切換到這個g
- curg: 當前運行的g
- p: 當前擁有的P
- nextp: 喚醒M時, M會擁有這個P
- park: M休眠時使用的信號量, 喚醒M時會通過它喚醒
- schedlink: 下一個m, 當m在鏈表結構中會使用
- mcache: 分配內存時使用的本地分配器, 和p.mcache一樣(擁有P時會複製過來)
- lockedg: lockedm的對應值
P裏面比較重要的成員:
- status: p的當前狀態
- link: 下一個p, 當p在鏈表結構中會使用
- m: 擁有這個P的M
- mcache: 分配內存時使用的本地分配器
- runqhead: 本地運行隊列的出隊序號
- runqtail: 本地運行隊列的入隊序號
- runq: 本地運行隊列的數組, 可以保存256個G
- gfree: G的自由列表, 保存變爲_Gdead後可以複用的G實例
- gcBgMarkWorker: 後臺GC的worker函數, 如果它存在M會優先執行它
- gcw: GC的本地工作隊列, 詳細將在下一篇(GC篇)分析
調度器涉及到的結構體除了上面的G、M、P 之外,還有以下,比如全局的調度器:
type schedt struct {
// accessed atomically. keep at top to ensure alignment on 32-bit systems.
// 下面兩個變量需以原子訪問訪問。保持在 struct 頂部,確保其在 32 位系統上可以對齊
goidgen uint64
lastpoll uint64
lock mutex
// 當修改 nmidle,nmidlelocked,nmsys,nmfreed 這些數值時
// 需要記得調用 checkdead
midle muintptr // idle m's waiting for work 空閒的M 隊列。
nmidle int32 // number of idle m's waiting for work 當前等待工作的空閒 m 計數
nmidlelocked int32 // number of locked m's waiting for work 當前等待工作的被 lock 的 m 計數
mcount int32 // number of m's that have been created 已經創建的 m 數量
maxmcount int32 // maximum number of m's allowed (or die) 允許創建的最大的 m 數量
ngsys uint32 // number of system goroutines; updated atomically 系統 goroutine 的數量, 原子操作
pidle puintptr // idle p's 空閒的 p 隊列
npidle uint32
nmspinning uint32 // See "Worker thread parking/unparking" comment in proc.go.
// Global runnable queue.
// 全局的可運行 g 隊列
runqhead guintptr // 隊頭地址
runqtail guintptr // 隊尾地址
runqsize int32 // 隊列寬度
// Global cache of dead G's.
// dead G 的全局緩
gflock mutex
gfreeStack *g // 棧中自由g ?
gfreeNoStack *g // 堆中自由g ?
ngfree int32
// Central cache of sudog structs.
// sudog 結構的集中緩存
sudoglock mutex
sudogcache *sudog
// Central pool of available defer structs of different sizes.
// 不同大小的可用的 defer struct 的集中緩存池
deferlock mutex
deferpool [5]*_defer
gcwaiting uint32 // gc is waiting to run gc 等待運行狀態。 作爲gc任務被執行期間的輔助標記、停止計數和通知機制
stopwait int32
stopnote note
sysmonwait uint32 // 作爲 系統檢測任務被執行期間的停止計數和通知機制
sysmonnote note
// safepointFn should be called on each P at the next GC
// safepoint if p.runSafePointFn is set.
// 應在下一個GC上的每個P上調用safepointFn
// 如果設置了p.runSafePointFn,則爲safepoint。
safePointFn func(*p)
safePointWait int32
safePointNote note
profilehz int32 // cpu profiling rate CPU分析率
procresizetime int64 // nanotime() of last change to gomaxprocs 上次修改 gomaxprocs 的納秒時間
totaltime int64 // ∫gomaxprocs dt up to procresizetime
}
全局調度器,全局只有一個schedt類型的實例。
sudoG 結構體:
// sudog 代表在等待列表裏的 g,比如向 channel 發送/接收內容時
// 之所以需要 sudog 是因爲 g 和同步對象之間的關係是多對多的
// 一個 g 可能會在多個等待隊列中,所以一個 g 可能被打包爲多個 sudog
// 多個 g 也可以等待在同一個同步對象上
// 因此對於一個同步對象就會有很多 sudog 了
// sudog 是從一個特殊的池中進行分配的。用 acquireSudog 和 releaseSudog 來分配和釋放 sudog
type sudog struct {
// The following fields are protected by the hchan.lock of the
// channel this sudog is blocking on. shrinkstack depends on
// this for sudogs involved in channel ops.
g *g
selectdone *uint32 // CAS to 1 to win select race (may point to stack)
next *sudog
prev *sudog
elem unsafe.Pointer // data element (may point to stack)
// The following fields are never accessed concurrently.
// For channels, waitlink is only accessed by g.
// For semaphores, all fields (including the ones above)
// are only accessed when holding a semaRoot lock.
acquiretime int64
releasetime int64
ticket uint32
parent *sudog // semaRoot binary tree
waitlink *sudog // g.waiting list or semaRoot
waittail *sudog // semaRoot
c *hchan // channel
}
那麼goroutine的入口是怎麼樣的呢?首先,我們從goroutine是如何被創建的說起,創建goroutine的函數爲:newproc 函數 (在 ./src/runtime/proc.go 文件中),即:使用go命令創建goroutine時, go會把go命令編譯爲對runtime.newproc的調用。
// 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.
// 根據 參數 fn 和 siz 創建一個 g
// 並把它放置入 自由g隊列中等待喚醒
// 編譯器翻譯一個 go 表達式時會調用這個函數
// 無法拆分堆棧,因爲它假設參數在 &fn 之後順序可用; 如果發生堆棧拆分,則不會複製它們。
// 新建一個goroutine,
// 用fn + PtrSize 獲取第一個參數的地址,也就是argp
// 用siz - 8 獲取pc地址
//go:nosplit
func newproc(siz int32, fn *funcval) {
// add 是一個指針運算,跳過函數指針
// 把棧上的參數起始地址找到
argp := add(unsafe.Pointer(&fn), sys.PtrSize)
// getcallerpc返回的是 調用函數之後的那條程序指令的地址,
// 即callee函數返回時要執行的下一條指令的地址
pc := getcallerpc(unsafe.Pointer(&siz))
// 用g0的棧創建G對象
systemstack(func() {
newproc1(fn, (*uint8)(argp), siz, 0, pc)
})
}
// 結構體 funcval
// funcval 是一個變長結構,第一個成員是函數指針
// 所以上面的 add 是跳過這個 fn
type funcval struct {
fn uintptr
// variable-size, fn-specific data here 這裏的可變大小,特定於fn的數據
}
runtime.newproc函數中只做了三件事:
- 計算額外參數的地址 argp
- 獲取調用端的地址(返回地址) pc
- 使用systemstack調用 newproc1 函數
systemstack 會切換當前的 g 到 g0, 並且使用g0的棧空間, 然後調用傳入的函數, 再切換回原來的g和原來的棧空間。
切換到g0後會假裝返回地址是mstart, 這樣traceback的時候可以在mstart停止。
這裏傳給systemstack的是一個閉包, 調用時會把閉包的地址放到寄存器rdx, 具體可以參考上面對閉包的分析。
下面我們在主要來看看 newproc1 函數做了什麼:
// Create a new g running fn with narg bytes of arguments starting
// at argp and returning nret bytes of results. 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.
// 根據函數參數和函數地址,創建一個新的G,然後將這個G加入隊列等待運行
func newproc1(fn *funcval, argp *uint8, narg int32, nret int32, callerpc uintptr) *g {
// 先獲取 當前 g,其實這裏獲取到的是 g0
_g_ := getg()
// 判斷下 func 的實現是否爲空
if fn == nil {
_g_.m.throwing = -1 // do not dump full stacks
throw("go of nil func value")
}
// 設置g對應的m的locks++, 禁止搶佔
_g_.m.locks++ // disable preemption because it can be holding p in a local var 禁用搶佔,因爲它可以在本地var中保存p
siz := narg + nret
siz = (siz + 7) &^ 7
// We could allocate a larger initial stack if necessary.
// Not worth it: this is almost always an error.
// 4*sizeof(uintreg): extra space added below
// sizeof(uintreg): caller's LR (arm) or return address (x86, in gostartcall).
if siz >= _StackMin-4*sys.RegSize-sys.RegSize {
throw("newproc: function arguments too large for new goroutine")
}
_p_ := _g_.m.p.ptr()
newg := gfget(_p_)
if newg == nil {
newg = malg(_StackMin)
casgstatus(newg, _Gidle, _Gdead)
allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
}
if newg.stack.hi == 0 {
throw("newproc1: newg missing stack")
}
if readgstatus(newg) != _Gdead {
throw("newproc1: new g is not Gdead")
}
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 usesLR {
// caller's LR
*(*uintptr)(unsafe.Pointer(sp)) = 0
prepGoExitFrame(sp)
spArg += sys.MinFrameSize
}
if narg > 0 {
memmove(unsafe.Pointer(spArg), unsafe.Pointer(argp), uintptr(narg))
// This is a stack-to-stack copy. If write barriers
// are enabled and the source stack is grey (the
// destination is always black), then perform a
// barrier copy. We do this *after* the memmove
// because the destination stack may have garbage on
// it.
if writeBarrier.needed && !_g_.m.curg.gcscandone {
f := findfunc(fn.fn)
stkmap := (*stackmap)(funcdata(f, _FUNCDATA_ArgsPointerMaps))
// We're in the prologue, so it's always stack map index 0.
bv := stackmapdata(stkmap, 0)
bulkBarrierBitmap(spArg, spArg, uintptr(narg), 0, bv.bytedata)
}
}
memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
newg.sched.sp = sp
newg.stktopsp = sp
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)
newg.gopc = callerpc
newg.startpc = fn.fn
if _g_.m.curg != nil {
newg.labels = _g_.m.curg.labels
}
if isSystemGoroutine(newg) {
atomic.Xadd(&sched.ngsys, +1)
}
newg.gcscanvalid = false
casgstatus(newg, _Gdead, _Grunnable)
if _p_.goidcache == _p_.goidcacheend {
// Sched.goidgen is the last allocated id,
// this batch must be [sched.goidgen+1, sched.goidgen+GoidCacheBatch].
// At startup sched.goidgen=0, so main goroutine receives goid=1.
_p_.goidcache = atomic.Xadd64(&sched.goidgen, _GoidCacheBatch)
_p_.goidcache -= _GoidCacheBatch - 1
_p_.goidcacheend = _p_.goidcache + _GoidCacheBatch
}
newg.goid = int64(_p_.goidcache)
_p_.goidcache++
if raceenabled {
newg.racectx = racegostart(callerpc)
}
if trace.enabled {
traceGoCreate(newg, newg.startpc)
}
runqput(_p_, newg, true)
if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted {
wakep()
}
_g_.m.locks--
if _g_.m.locks == 0 && _g_.preempt { // restore the preemption request in case we've cleared it in newstack
_g_.stackguard0 = stackPreempt
}
return newg
}
先大致看下newproc1 函數邏輯流程:
newproc1 --> newg
newg[gfget] --> nil{is nil?}
nil -->|yes|E[init stack]
nil -->|no|C[malg]
C --> D[set g status=> idle->dead]
D --> allgadd
E --> G[set g status=> dead-> runnable]
allgadd --> G
G --> runqput
runtime.newproc1的處理如下:
- 調用getg獲取當前的g, 會編譯爲讀取FS寄存器(TLS), 這裏會獲取到g0
- 設置g對應的m的locks++, 禁止搶佔
- 獲取m擁有的p
- 新建一個g
- 把參數複製到g的棧上
- 把返回地址複製到g的棧上, 這裏的返回地址是goexit, 表示調用完目標函數後會調用goexit
- 設置g的調度數據(sched)
- 設置sched.sp等於參數+返回地址後的rsp地址
- 設置sched.pc等於目標函數的地址, 查看gostartcallfn和gostartcall
- 設置sched.g等於g
- 設置g的狀態爲待運行(_Grunnable)
- 調用runqput把g放到運行隊列
- 首先隨機把g放到p.runnext, 如果放到runnext則入隊原來在runnext的g
- 然後嘗試把g放到P的"本地運行隊列"
- 如果本地運行隊列滿了則調用runqputslow把g放到"全局運行隊列"
- runqputslow會把本地運行隊列中一半的g放到全局運行隊列, 這樣下次就可以繼續用快速的本地運行隊列了
- 如果當前有空閒的P, 但是無自旋的M(nmspinning等於0), 並且主函數已執行則喚醒或新建一個M
- 這一步非常重要, 用於保證當前有足夠的M運行G, 具體請查看上面的"空閒M鏈表"
- 喚醒或新建一個M會通過wakep函數
創建goroutine的流程就這麼多了, 接下來看看M是如何調度的.
(未完,疲憊中.............)