# 6.9 系統監控
我們已經完整分析過調度器的調度執行了。 當我們通過`runtime.newproc`創建好主 Goroutine 后,會將其加入到一個 P 的本地隊列中。 隨著`runtime.mstart`啟動調度器,主 Goroutine 便開始得以調度。
```
// src/runtime/proc.go
// 主 Goroutine
func main() {
(...)
// 啟動系統后臺監控(定期垃圾回收、并發任務調度)
systemstack(func() {
newm(sysmon, nil)
})
(...)
}
```
那么是時候看看主 Goroutine 中的系統監控`newm(sysmon, nil)`到底在干什么了。
## 6.9.1 監控循環
```
// 系統監控在一個獨立的 m 上運行
// 總是在沒有 P 的情況下運行,因此不能出現寫屏障
//go:nowritebarrierrec
func sysmon() {
lock(&sched.lock)
// 不計入死鎖的系統 m 的數量
sched.nmsys++
// 死鎖檢查
checkdead()
unlock(&sched.lock)
idle := 0 // 沒有 wokeup 的周期數
delay := uint32(0)
for {
if idle == 0 { // 每次啟動先休眠 20us
delay = 20
} else if idle > 50 { // 1ms 后就翻倍休眠時間
delay *= 2
}
if delay > 10*1000 { // 增加到 10ms
delay = 10 * 1000
}
// 休眠
usleep(delay)
now := nanotime()
next := timeSleepUntil()
// 如果在 STW,則暫時休眠
if debug.schedtrace <= 0 && (sched.gcwaiting != 0 || atomic.Load(&sched.npidle) == uint32(gomaxprocs)) {
lock(&sched.lock)
if atomic.Load(&sched.gcwaiting) != 0 || atomic.Load(&sched.npidle) == uint32(gomaxprocs) {
if next > now {
atomic.Store(&sched.sysmonwait, 1)
unlock(&sched.lock)
// 確保 wake-up 周期足夠小從而進行正確的采樣
sleep := forcegcperiod / 2
if next-now < sleep {
sleep = next - now
}
shouldRelax := sleep >= osRelaxMinNS
if shouldRelax {
osRelax(true)
}
notetsleep(&sched.sysmonnote, sleep)
if shouldRelax {
osRelax(false)
}
now = nanotime()
next = timeSleepUntil()
lock(&sched.lock)
atomic.Store(&sched.sysmonwait, 0)
noteclear(&sched.sysmonnote)
}
idle = 0
delay = 20
}
unlock(&sched.lock)
}
// 需要時觸發 libc interceptor
if *cgo_yield != nil {
asmcgocall(*cgo_yield, nil)
}
// 如果超過 10ms 沒有 poll,則 poll 一下網絡
lastpoll := int64(atomic.Load64(&sched.lastpoll))
if netpollinited() && lastpoll != 0 && lastpoll+10*1000*1000 < now {
atomic.Cas64(&sched.lastpoll, uint64(lastpoll), uint64(now))
list := netpoll(0) // 非阻塞,返回 Goroutine 列表
if !list.empty() {
// 需要在插入 g 列表前減少空閑鎖住的 m 的數量(假裝有一個正在運行)
// 否則會導致這些情況:
// injectglist 會綁定所有的 p,但是在它開始 M 運行 P 之前,另一個 M 從 syscall 返回,
// 完成運行它的 G ,注意這時候沒有 work 要做,且沒有其他正在運行 M 的死鎖報告。
incidlelocked(-1)
injectglist(&list)
incidlelocked(1)
}
}
if next < now {
// There are timers that should have already run,
// perhaps because there is an unpreemptible P.
// Try to start an M to run them.
startm(nil, false)
}
// 搶奪在 syscall 中阻塞的 P、運行時間過長的 G
if retake(now) != 0 {
idle = 0
} else {
idle++
}
// 檢查是否需要強制觸發 GC
if t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && atomic.Load(&forcegc.idle) != 0 {
lock(&forcegc.lock)
forcegc.idle = 0
var list gList
list.push(forcegc.g)
injectglist(&list)
unlock(&forcegc.lock)
}
(...)
}
}
```
系統監控在運行時扮演的角色無需多言, 因為使用的是運行時通知機制,在 Linux 上由 Futex 實現,不依賴調度器, 因此它自身通過`newm`在一個 M 上獨立運行, 自身永遠保持在一個循環內直到應用結束。休眠有好幾種不同的休眠策略:
1. 至少休眠 20us
2. 如果搶占 P 和 G 失敗次數超過五十、且沒有觸發 GC,則說明很閑,翻倍休眠
3. 如果休眠翻倍時間超過 10ms,保持休眠 10ms 不變
休眠結束后,先觀察目前的系統狀態,如果正在進行 GC,那么繼續休眠。 這時的休眠會被設置超時。
如果沒有超時被喚醒,則說明 GC 已經結束,一切都很好,繼續做本職工作。 如果超時,則無關 GC,必須開始進行本職善后:
1. 如果 cgo 調用被 libc 攔截,繼續觸發起調用
2. 如果已經有 10ms 沒有 poll 網絡數據,則 poll 一下網絡數據
3. 搶占在系統調用中阻塞的 P 已經運行時間過長的 G
4. 檢查是不是該觸發 GC 了
5. 如果距離上一次堆清理已經超過了兩分半,則執行清理工作
其中的`note`同步機制`retake`搶占已在[6.8 協作與搶占](https://golang.design/under-the-hood/zh-cn/part2runtime/ch06sched/preemption)和[6.8 同步原語](https://golang.design/under-the-hood/zh-cn/part2runtime/ch06sched/sync)中詳細討論過了。
## 6.9.2 小結
總的來說系統監控的本職工作還是比較明確的,它在一個單獨的 M 上執行,負責處理網絡數據、搶占 P/G、觸發 GC、清理堆 span。 對于這些職責,我們需要確定一些細節工作:
2. `gcTrigger`如何觸發 GC?在[垃圾回收器:初始化](https://golang.design/under-the-hood/zh-cn/part2runtime/ch08gc/init)一節中詳細討論。
3. `scavenge`如何清理堆 span?
4. `netpoll`如何 poll 網絡數據?
5.
- 第一部分 :基礎篇
- 第1章 Go語言的前世今生
- 1.2 Go語言綜述
- 1.3 順序進程通訊
- 1.4 Plan9匯編語言
- 第2章 程序生命周期
- 2.1 從go命令談起
- 2.2 Go程序編譯流程
- 2.3 Go 程序啟動引導
- 2.4 主Goroutine的生與死
- 第3 章 語言核心
- 3.1 數組.切片與字符串
- 3.2 散列表
- 3.3 函數調用
- 3.4 延遲語句
- 3.5 恐慌與恢復內建函數
- 3.6 通信原語
- 3.7 接口
- 3.8 運行時類型系統
- 3.9 類型別名
- 3.10 進一步閱讀的參考文獻
- 第4章 錯誤
- 4.1 問題的演化
- 4.2 錯誤值檢查
- 4.3 錯誤格式與上下文
- 4.4 錯誤語義
- 4.5 錯誤處理的未來
- 4.6 進一步閱讀的參考文獻
- 第5章 同步模式
- 5.1 共享內存式同步模式
- 5.2 互斥鎖
- 5.3 原子操作
- 5.4 條件變量
- 5.5 同步組
- 5.6 緩存池
- 5.7 并發安全散列表
- 5.8 上下文
- 5.9 內存一致模型
- 5.10 進一步閱讀的文獻參考
- 第二部分 運行時篇
- 第6章 并發調度
- 6.1 隨機調度的基本概念
- 6.2 工作竊取式調度
- 6.3 MPG模型與并發調度單
- 6.4 調度循環
- 6.5 線程管理
- 6.6 信號處理機制
- 6.7 執行棧管理
- 6.8 協作與搶占
- 6.9 系統監控
- 6.10 網絡輪詢器
- 6.11 計時器
- 6.12 非均勻訪存下的調度模型
- 6.13 進一步閱讀的參考文獻
- 第7章 內存分配
- 7.1 設計原則
- 7.2 組件
- 7.3 初始化
- 7.4 大對象分配
- 7.5 小對象分配
- 7.6 微對象分配
- 7.7 頁分配器
- 7.8 內存統計
- 第8章 垃圾回收
- 8.1 垃圾回收的基本想法
- 8.2 寫屏幕技術
- 8.3 調步模型與強弱觸發邊界
- 8.4 掃描標記與標記輔助
- 8.5 免清掃式位圖技術
- 8.6 前進保障與終止檢測
- 8.7 安全點分析
- 8.8 分代假設與代際回收
- 8.9 請求假設與實務制導回收
- 8.10 終結器
- 8.11 過去,現在與未來
- 8.12 垃圾回收統一理論
- 8.13 進一步閱讀的參考文獻
- 第三部分 工具鏈篇
- 第9章 代碼分析
- 9.1 死鎖檢測
- 9.2 競爭檢測
- 9.3 性能追蹤
- 9.4 代碼測試
- 9.5 基準測試
- 9.6 運行時統計量
- 9.7 語言服務協議
- 第10章 依賴管理
- 10.1 依賴管理的難點
- 10.2 語義化版本管理
- 10.3 最小版本選擇算法
- 10.4 Vgo 與dep之爭
- 第12章 泛型
- 12.1 泛型設計的演進
- 12.2 基于合約的泛型
- 12.3 類型檢查技術
- 12.4 泛型的未來
- 12.5 進一步閱讀的的參考文獻
- 第13章 編譯技術
- 13.1 詞法與文法
- 13.2 中間表示
- 13.3 優化器
- 13.4 指針檢查器
- 13.5 逃逸分析
- 13.6 自舉
- 13.7 鏈接器
- 13.8 匯編器
- 13.9 調用規約
- 13.10 cgo與系統調用
- 結束語: Go去向何方?