--- 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["快速路径
复用 oldP
状态→Grunning"] B -->|"否: oldP 被抢"| D["慢速路径
mcall exitsyscall0
找新 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