vault backup: 2026-06-07 16:52:15
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
---
|
||||
tags: [go, golang, go-principle, gmp-scheduler, preemption]
|
||||
create time: 2026-06-07 16:00
|
||||
---
|
||||
|
||||
# GMP 调度原理 — 抢占式调度
|
||||
|
||||
## 概述
|
||||
|
||||
前面讨论的让渡都是 G 的主动行为。但如果一个 G 执行纯计算循环,从不阻塞、不让出 CPU,整个系统就会被它拖垮。本节介绍 Go 调度器的"第三只手"——由后台 `sysmon` 线程发起的**抢占式调度**。涵盖 §5:system monitor、系统调用抢占、协作与非协作抢占。
|
||||
|
||||
## 正文
|
||||
|
||||
### 5.1 sysmon:永不休息的巡逻兵
|
||||
|
||||
Go 程序启动时,runtime 通过 `newm(sysmon, nil, -1)` 创建一个独立的 OS 线程专跑 `sysmon`。它全程唯一、终身运行。
|
||||
|
||||
```go
|
||||
func sysmon() {
|
||||
for {
|
||||
usleep(delay) // 自适应休眠(最长 10ms)
|
||||
|
||||
if netpollinited() && lastpoll+10ms < now {
|
||||
list := netpoll(0) // ① 非阻塞网络轮询
|
||||
injectglist(&list) // 将就绪 G 放回全局队列
|
||||
}
|
||||
|
||||
retake(now) // ② 抢占检查
|
||||
|
||||
if t.test() && forcegc.idle != 0 {
|
||||
// ③ GC 触发检查
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
三次巡检职责:
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **netpoll** | 从 epoll 取出已完成的 IO 事件,唤醒对应 G |
|
||||
| **retake** | 遍历所有 P,发现超时或 syscall 过久的立即抢占 |
|
||||
| **GC 检查** | 判断是否需要触发强制垃圾回收 |
|
||||
|
||||
### 5.2 系统调用抢占
|
||||
|
||||
当一个 G 发起 syscall 时,对应的 M 会被操作系统挂起,绑定的 P 也随之闲置。Go 的策略是:**人走可以,但办公桌留下。**
|
||||
|
||||
**进入 syscall**(`reentersyscall`):
|
||||
|
||||
```go
|
||||
func reentersyscall(pc, sp uintptr) {
|
||||
casgstatus(_g_, _Grunning, _Gsyscall)
|
||||
|
||||
pp := _g_.m.p.ptr()
|
||||
pp.m = 0 // 解除 P → M
|
||||
_g_.m.p = 0 // 解除 M → P
|
||||
|
||||
_g_.m.oldp.set(pp) // 记住原 P(弱引用)
|
||||
atomic.Store(&pp.status, _Psyscall)
|
||||
}
|
||||
```
|
||||
|
||||
退出 syscall 时有两条路径:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["G 退出 syscall"] --> B{exitsyscallfast?}
|
||||
B -->|"是: oldP 仍单身"| C["快速路径<br/>复用 oldP<br/>状态→Grunning"]
|
||||
B -->|"否: oldP 被抢"| D["慢速路径<br/>mcall exitsyscall0<br/>找新 P / 入 GRQ / stopm"]
|
||||
style C fill:#e8f5e9
|
||||
style D fill:#fff3e0
|
||||
```
|
||||
|
||||
```go
|
||||
func exitsyscall() {
|
||||
oldp := _g_.m.oldp.ptr()
|
||||
if exitsyscallfast(oldp) { // 快速路径
|
||||
casgstatus(_g_, _Gsyscall, _Grunning)
|
||||
return
|
||||
}
|
||||
mcall(exitsyscall0) // 慢速路径
|
||||
}
|
||||
```
|
||||
|
||||
**sysmon 介入**:若 P 处于 `_Psyscall` 超过 10ms,`retake` 会强制将 P 从 syscall 的 M 处夺走,分配给新的空闲 M:
|
||||
|
||||
```go
|
||||
// retake 中关键片段
|
||||
if s == _Psyscall {
|
||||
if runqempty(_p_) && pd.syscallwhen+10ms > now {
|
||||
continue // 刚进去不久且队列为空,暂不抢占
|
||||
}
|
||||
atomic.Cas(&_p_.status, s, _Pidle)
|
||||
handoffp(_p_) // 抢夺 P,分配给新 M
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 运行超时抢占
|
||||
|
||||
对持续运行的 G(如纯计算死循环),`sysmon` 通过 `preemptone` 发起超时抢占。这分为两代实现:
|
||||
|
||||
#### 5.3.1 协作式抢占(Go ≤ 1.13)
|
||||
|
||||
`preemptone` 在目标 G 上打两个标记:
|
||||
|
||||
```go
|
||||
func preemptone(_p_ *p) bool {
|
||||
mp := _p_.m.ptr()
|
||||
gp := mp.curg
|
||||
|
||||
gp.preempt = true // 抢占标志
|
||||
gp.stackguard0 = stackPreempt // 栈保护区特殊值
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
G 在执行函数调用时(尤其是触发栈扩容的 `newstack`),会检查 `stackguard0`:
|
||||
|
||||
```go
|
||||
func newstack() {
|
||||
stackguard0 := atomic.Loaduintptr(&gp.stackguard0)
|
||||
if stackguard0 == stackPreempt {
|
||||
if canPreemptM(thisg.m) {
|
||||
gopreempt_m(gp) // 响应抢占,殊途同归 goschedImpl()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**缺点**:如果一个 G 一直在跑无函数调用的纯计算死循环,永远不检查 `stackguard0`,就不会响应抢占意图。
|
||||
|
||||
#### 5.3.2 非协作式抢占(Go ≥ 1.14)
|
||||
|
||||
为解决上述短板,Go 1.14 引入基于 POSIX 信号的硬抢占机制:
|
||||
|
||||
```go
|
||||
func preemptone(_p_ *p) bool {
|
||||
// ... 上面设置协作标记的代码不变 ...
|
||||
|
||||
if preemptMSupported && debug.asyncpreemptoff == 0 {
|
||||
preemptM(mp) // 向目标线程发送 sigPreempt 信号
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func signalM(mp *m, sig int) {
|
||||
pthread_kill(pthread(mp.procid), uint32(sig)) // 底层 syscall
|
||||
}
|
||||
```
|
||||
|
||||
信号到达后,操作系统的信号处理函数 `sighandler` → `doSigPreempt` 会通过修改寄存器的"指令注入"方式强行接管执行流:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["sigPreempt 信号到达"] --> B["sighandler 检查 safepoint"]
|
||||
B --> C["pushCall: 修改 PC + SP"]
|
||||
C --> D["下一条指令跳入 asyncPreempt"]
|
||||
D --> E["mcall gopreempt_m"]
|
||||
E --> F["goschedImpl: 状态→Grunnable → GRQ"]
|
||||
style A fill:#ffebee
|
||||
style D fill:#fff9c4
|
||||
style F fill:#e8f5e9
|
||||
```
|
||||
|
||||
```go
|
||||
func doSigPreempt(gp *g, ctxt *sigctxt) {
|
||||
if wantAsyncPreempt(gp) {
|
||||
if ok, newpc := isAsyncSafePoint(...); ok {
|
||||
ctxt.pushCall(abi.FuncPCABI0(asyncPreempt), newpc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pushCall(targetPC, resumePC uintptr) {
|
||||
sp -= goarch.PtrSize
|
||||
*(*uintptr)(unsafe.Pointer(sp)) = resumePC // 压入返回地址
|
||||
c.set_rsp(uint64(sp)) // 更新栈指针
|
||||
c.set_rip(uint64(targetPC)) // 劫持程序计数器
|
||||
}
|
||||
```
|
||||
|
||||
这条链路的特点是:**无论 G 在做什么**——不管有没有函数调用、是不是死循环——只要收到信号并被确认为安全中断点,就强制执行抢占。这是操作系统级别的强制手段,Go runtime 无法绕过。
|
||||
|
||||
### 5.4 小结对比
|
||||
|
||||
| 抢占类型 | 触发条件 | 方式 | 生效时机 |
|
||||
|----------|----------|------|----------|
|
||||
| 系统调用抢占 | P 处于 `_Psyscall` > 10ms | `handoffp` 夺回 P | sysmon 定期检查 |
|
||||
| 协作式抢占 | G 运行 > 10ms | 设置 `stackPreempt` 标记 | G 下次函数调用/栈检查时 |
|
||||
| 非协作式抢占 | G 运行 > 10ms | 发送 `sigPreempt` 信号并注入代码 | 信号中断后立即执行 |
|
||||
|
||||
> [!question] ❓ 为什么保留协作式抢占?
|
||||
> 信号机制有平台限制(Windows 不支持),协作式作为兜底方案始终有效;同时对于大多数有 IO 或 channel 操作的 G,协作式抢占已经足够及时。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-overview]] — GMP 概览
|
||||
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-datastructures]] — G/M/P/Schedt 数据结构
|
||||
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-lifecycle]] — Goroutine 生命周期
|
||||
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-summary]] — 知识卡片
|
||||
- [[hzh/GolangStar/Go语言原理/垃圾回收]] — sysmon 如何触发 GC
|
||||
Reference in New Issue
Block a user