GMP 調度器(上篇)- 數據結構
概述
G: 表示
goroutine
M: 表示 操作系統的線程
P: 表示處理器
,運行在線程上的本地調度器
內部實現
本文主要研究一下 GMP
的內部實現,相關文件目錄爲 $GOROOT/src/runtime
,筆者的 Go 版本爲 go1.19 linux/amd64
。
GMP 數據結構
G
M
P
相關的數據結構定義,全部定義在 $GOROOT/src/runtime/runtime2.go
文件中。
GMP 關係圖
GMP 數據結構關係圖
G
goroutine 只存在於 Go 語言的運行時,是 Go 語言在用戶態提供的線程,但是內存佔用和上下文切換開銷更少,同時啓動速度更快。作爲一種粒度更細的資源調度單元,能夠在高併發的場景下更高效地利用機器的 CPU 資源。
stack 對象
stack
對象表示 goroutine
執行棧內存範圍,棧的上下邊界分別是 [lo, hi), 兩側沒有隱式的數據結構 (有些運行時對象會有隱式數據結構)。
type stack struct {
lo uintptr
hi uintptr
}
g 對象
goroutine
的運行時表示。
type g struct {
// stack 描述了棧的實際上下邊界 [stack.lo, stack.hi)
// stackguard0 是在 Go 棧增長 prologue 中用來和 sp 寄存器做比較
// 正常情況下,stackguard0 = stack.lo+StackGuard, 但是可以用 StackPreempt 觸發搶佔
// stackguard1 是在 C 棧增長 prologue 中用來和 sp 寄存器做比較
// 在 g0 和 gsignal 棧上,stackguard1 = stack.lo+StackGuard
// 在其他棧上,stackguard1 = ~0 (按 0 取反), 觸發 morestack 調用(並 crash)
stack stack
stackguard0 uintptr
stackguard1 uintptr
_panic *_panic // _panic 鏈表頭節點
_defer *_defer // _defer 鏈表頭節點
m *m // 當前關聯的 m (線程)
sched gobuf // goroutine 調度相關數據
...
atomicstatus uint32 // goroutine 狀態
stackLock uint32 // sigprof/scang lock
goid int64 // goroutine ID (對應用層不可見,但是可以通過其他方法獲取到,詳情見之前的文章)
...
preempt bool // 搶佔信號
preemptStop bool // 搶佔時將狀態修改成 _Gpreempted
preemptShrink bool // 在同步安全的臨界區收縮棧
...
}
sudog 對象
sudog
對象表示等待隊列裏面的 goroutine
對象, 比如向 channel
發送 / 接收數據時。
sudog
對象主要作爲一層中間抽象層,因爲 goroutine
和同步對象之間是多對多關係,一個 goroutine
可能在多個等待隊列中,可以有多個 sudog
, 同時,多個 goroutine
也可能在等待同一個同步對象,一個對象可以有多個 sudog
。
爲了提升程序的運行時性能,sudog
對象從一個特殊的對象池中分配,調用 acquireSudog
函數分配,releaseSudog
函數歸還。
type sudog struct {
g *g
next *sudog
prev *sudog
acquiretime int64
releasetime int64
ticket uint32
// isSelect 表示一個 g 是否正處於 select
isSelect bool
// 如果 goroutine 因爲 channel c 傳遞值被喚醒,success 的值爲 true
// 如果 goroutine 因爲 channel c 關閉被喚醒,success 的值爲 false
success bool
...
}
gobuf 對象
gobuf
對象表示 goroutine
的運行現場表示,該對象在調度器保存數據或者恢復上下文的時候用到,sp
和 pc
寄存器字段用來存儲或者恢復寄存器中的值,改變程序即將執行的代碼。
type gobuf struct {
sp uintptr // sp 寄存器
pc uintptr // pc 寄存器
g guintptr // goroutine 對象指針
ret uintptr // 系統調用返回值
lr uintptr // arm 上用的寄存器,amd64 忽略
}
狀態列表
goroutine
的狀態列表,最常見是 _Grunnable
, _Grunning
, _Gwaiting
。
const (
// goroutine 剛被分配並且還沒有被初始化
_Gidle = iota // 0
// goroutine 處於運行隊列中,沒有在執行代碼,沒有棧的所有權
_Grunnable // 1
// goroutine 可以執行代碼並且擁有有棧的所有權,M 和 P 已經設置並且有效
_Grunning // 2
// goroutine 正在執行系統調用,沒有在執行代碼,擁有棧的所有權但是不在運行隊列中,此外,M 已經設置
_Gsyscall // 3
// goroutine 處於阻塞中,沒有在執行代碼並且不在運行隊列中,但是可能存在於 Channel 的等待隊列上
_Gwaiting // 4
// 沒有使用這個狀態,但是被硬編碼到了 gbd 腳本中
_Gmoribund_unused // 5
// goroutine 沒有被使用 (可能已經退出或剛剛初始化),沒有在執行代碼,可能存在分配的棧
_Gdead // 6
// 沒有使用這個狀態
_Genqueue_unused // 7
// goroutine 的棧正在被移動,沒有在執行代碼並且不在運行隊列中
_Gcopystack // 8
// goroutine 由於搶佔而阻塞,等待喚醒
_Gpreempted // 9
// GC 正在掃描棧空間,沒有在執行代碼,可以與上述其他狀態同時存在
_Gscan = 0x1000
// 下面幾個是組合狀態
_Gscanrunnable = _Gscan + _Grunnable // 0x1001
_Gscanrunning = _Gscan + _Grunning // 0x1002
_Gscansyscall = _Gscan + _Gsyscall // 0x1003
_Gscanwaiting = _Gscan + _Gwaiting // 0x1004
_Gscanpreempted = _Gscan + _Gpreempted // 0x1009
)
M
調度器最多可以創建 10000 個線程,但是其中大多數的線程都不會執行用戶代碼(例如陷入系統調用或 IO 調用),最多隻會有 GOMAXPROCS 個活躍線程能夠正常運行。在默認情況下,運行時會將 GOMAXPROCS 設置成當前機器的核數,我們也可以在程序中使用 runtime.GOMAXPROCS 來改變最大的活躍線程數。
三種類型
-
主線程 m : 全局變量
runtime.m0
表示 (全局只有一個實例) -
sysmon m : 監控線程 (全局只有一個實例)
-
用戶線程 m : 和處理器
p
綁定,執行具體的goroutine
邏輯代碼
m 對象
線程
的運行時表示。
type m struct {
g0 *g // 執行調度的 goroutine
...
curg *g // 當前運行的 goroutine
p puintptr // 正在運行代碼的處理器 (如果爲 nil, 說明當前沒有代碼運行)
nextp puintptr // 暫存的處理器
oldp puintptr // 執行系統調用之前使用線程的處理器
id int64 // ID
preemptoff string // 如果不爲空,保持當前 goroutine 在這個 m 上運行
spinning bool // m 正在積極尋找活兒幹
blocked bool // m 阻塞在 note
incgo bool // m 正在執行 cgo 調用
ncgocall uint64 // cgo 調用總次數
ncgo int32 // 當前正在運行的 cgo 調用次數
...
}
P
處理器是線程和 goroutine 的中間層,提供線程需要的上下文環境,負責調度線程上的等待隊列,通過處理器 P 的調度, 每一個內核線程都能夠執行多個 goroutine,它能在 goroutine 進行一些 I/O 操作時及時讓出計算資源,提高線程的資源利用率。
p 對象
處理器(p)
的運行時表示,線程(m)
必須持有 (綁定)p
纔可以運行goroutine
。
type p struct {
id int32 // ID
status uint32 // p 的狀態
schedtick uint32 // 調度時自增
syscalltick uint32 // 系統調用時自增
sysmontick sysmontick // sysmon 最後觀察到的 tick 時間
m muintptr // 關聯的 m 的指針,如果 p 處於空閒狀態,指針爲 nil
...
deferpool []*_defer // 可用的 _defer 對象池
deferpoolbuf [32]*_defer // _defer 對象池
goidcache uint64 // 緩存 goroutine ID, 優化 runtime·sched.goidgen
// goroutine 運行隊列,訪問時無需加鎖
runqhead uint32 // runnable 隊列頭索引
runqtail uint32 // runnable 隊列尾索引
runq [256]guintptr // runnable 隊列 (環形隊列,數據結構爲數組,元素數量最多爲 256)
// runnext 如果不等於 nil, 表示下一個可運行的 goroutine
// 說明它已經被當前 goroutine 修改爲 ready 狀態,並且比隊列中的其他 goroutine 擁有更高的優先級
// 如果運行 goroutine 對應的時間片中還有剩餘的時間,那麼直接運行這個 goroutine,而不是放入隊列中
runnext guintptr
...
}
狀態列表
const (
// 處理器沒有在執行代碼或者調度,處於空閒狀態
_Pidle = iota
// 處理器被線程 M 持有,並且正在執行代碼或者調度
_Prunning
// 處理器沒有在執行代碼,線程陷入系統調用
_Psyscall
// 處理器被線程 M 持有,由於 GC 被停止
_Pgcstop
// 處理器不再被使用
_Pdead
)
GMP 數據結構關係圖
GMP 數據結構關係圖
調度器數據結構
schedt 對象
schedt
對象是全局調度器的運行時表示,全局只有一個 schedt
對象實例,定義在 $GOROOT/src/runtime/runtime2.go
文件中。
type schedt struct {
goidgen uint64 // 原子性訪問,保持在 struct 頂部,確保 32 位系統上對齊
lastpoll uint64 // network poll 的最後時間,如果爲 0, 說明正在 poll
pollUntil uint64 // 當前 poll 的休眠時間
lock mutex
// 增加 nmidle, nmidlelocked, nmsys, nmfreed 這幾個值的時候, 確保調用 checkdead()
midle muintptr // 空閒的 m 隊列
nmidle int32 // 空閒的 m 數量
nmidlelocked int32 // 空閒的被鎖住的 m 數量
mnext int64 // 預創建的 m 數量,該數量會作爲下一個創建的 m 的 ID
maxmcount int32 // 允許的 m 數量上限
nmsys int32 // 因爲死鎖未計算的系統 m 數量
nmfreed int64 // 累計釋放的 m 數量
ngsys uint32 // 系統 goroutine 數量,原子性更新
pidle puintptr // 空閒的處理器隊列
npidle uint32 // 空閒的處理器數量
runq gQueue // 全局可運行 goroutine 隊列
runqsize int32 // 全局可運行 goroutine 數量
// dead 狀態的 goroutine 的全局緩存
gFree struct {
lock mutex
stack gList // Gs with stacks
noStack gList // Gs without stacks
n int32
}
// sudog 對象的集中緩存
sudoglock mutex
sudogcache *sudog
// 可用的 _defer 對象的集中緩存
deferlock mutex
deferpool *_defer
// 當 m 被設置了 m.exited 標記之後,會掛載到 freem 鏈表上面等待被釋放
// 鏈表使用 m.freelink 字段鏈接
freem *m
...
}
schedt
對象字段非常多 (畢竟是全局調度器),這裏我們重點關注 3 個字段:
-
midle
表示空閒的線程 (m)
,數據結構是指針,具體的get + set
操作是通過指針 + 位置偏移量
實現的 -
pidle
表示空閒的處理器 (p)
,數據結構和midle
類似 -
runq
表示可運行的goroutine (g)
隊列, 數據結構是鏈表
schedt 對象
小結
本文主要對 GMP
調度器中的數據結構部分做了簡單的概述:
-
g
對象表示goroutine
, 是用來執行具體的任務的 (也就是幹活的) -
m
對象表示線程
, 和真正的操作系統線程
綁定之後,就可以執行具體的goroutine
代碼了 -
p
表示處理器,作爲抽象中間層用來管理goroutine
隊列以及調度goroutine
到具體的m
上執行
除此之外:
-
sudog
對象包裝了一層g
, 用來表示在隊列中等待的goroutine
對象 -
gobuf
對象包裝了一層g
, 用來表示goroutine
的運行現場,在調度器保存數據或者恢復上下文的時候可以用到
最後,我們列出了 g
和 p
對象的不同狀態值,這些值在程序整個生命週期內的調度過程中都會使用到。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/8OSK8anzNPuslwt61XuNUA