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 的運行現場表示,該對象在調度器保存數據或者恢復上下文的時候用到,sppc 寄存器字段用來存儲或者恢復寄存器中的值,改變程序即將執行的代碼。

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 對象

線程 的運行時表示。

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 個字段:

schedt 對象


小結

本文主要對 GMP 調度器中的數據結構部分做了簡單的概述:

除此之外:

最後,我們列出了 gp 對象的不同狀態值,這些值在程序整個生命週期內的調度過程中都會使用到。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/8OSK8anzNPuslwt61XuNUA