# 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去向何方?