vault backup: 2026-06-07 16:52:15

This commit is contained in:
2026-06-07 16:52:15 +08:00
parent 53ab5f58b2
commit dcbf185468
7 changed files with 860 additions and 1507 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,123 @@
---
tags: [go, golang, go-principle, gmp-scheduler, datastructure]
create time: 2026-06-07 16:00
---
# GMP 调度原理 — 数据结构
## 概述
本节深入 `runtime/runtime2.go`,逐字段拆解 G、M、P、schedt 四个核心结构体的源码定义及其设计意图。理解这些结构是后续追踪调度流程的基础。
## 正文
### 2.1 G(Goroutine)
G 是 goroutine 的运行时实体,承载栈空间、执行状态和生命周期信息。
```go
type g struct {
stack stack // 栈空间 [stack.lo, stack.hi)
stackguard0 uintptr // 栈保护区边界;也用于传递抢占标记
_panic *_panic // panic 链表头
_defer *_defer // defer 链表头(LIFO)
m *m // 当前绑定的 M(未运行时为 nil)
atomicstatus uint32 // 原子状态:_Gidle → _Grunnable → _Grunning → ...
schedlink guintptr // 全局队列 / 空闲链表中的 next 指针
}
```
| 关键字段 | 说明 |
|----------|------|
| `stack` | Goroutine 的执行栈,初始约 2KB,可动态扩容 |
| `stackguard0` | 函数调用前比较值。若等于 `stackPreempt` 表示被标记抢占;若接近 `stack.lo` 则触发栈扩容 |
| `atomicstatus` | 生命周期状态的原子快照,通过 `casgstatus()` 切换 |
**状态流转**:
```mermaid
stateDiagram-v2
[*] --> _Gidle: 未初始化
_Gidle --> _Grunnable: newproc()
_Grunnable --> _Grunning: schedule() 从队列取出
_Grunning --> _Grunnable: Gosched() / preempt()
_Grunning --> _Gdead: 执行完毕
_Grunning --> _Gwaiting: gopark() 阻塞
_Gwaiting --> _Grunnable: goready() 唤醒
_Gwaiting --> _Gsyscall: (间接)
_Gsyscall --> _Grunning: exitsyscall()
_Gdead --> [*]: 回收
style _Grunning fill:#e8f5e9
style _Gwaiting fill:#fff3e0
```
### 2.2 M(Machine)
M 是 OS 线程的运行时封装,真正执行代码。
```go
type m struct {
g0 *g // 调度协程,每个 M 独有
procid uint64 // M 的唯一 ID
gsignal *g // 信号处理协程
curg *g // 当前正在运行的用户 G
p puintptr // 当前绑定的 P
schedlink muintptr // 空闲 M 链表 next 指针
}
```
M 在两个角色间切换:
- 执行 `g0` 时:**调度者**——调用 `schedule()` 寻找下一个待执行的 G
- 执行 `curg` 时:**执行者**——运行用户代码
### 2.3 P(Processor)
P 是逻辑处理器,作为调度器的核心组件管理本地队列。
```go
type p struct {
id int32 // P 的编号
status uint32 // _Pidle / _Prunning / _Psyscall
link puintptr // 空闲 P 链表
schedtick uint32 // 每次 schedule() 自增
syscalltick uint32 // 每次系统调用自增
m muintptr // 回指绑定的 M(idle 时为 0)
runqhead uint32 // LRQ 头部索引
runqtail uint32 // LRQ 尾部索引
runq [256]guintptr // 本地 G 队列(环形数组)
runnext guintptr // VIP 位置:高优先级下一个 G
}
```
| 关键字段 | 作用 |
|----------|------|
| `runq[256]` | 定长环形数组作为 LRQ,CAS 无锁存取 |
| `runnext` | 新创建的 G 优先放入此处,下次调度直接执行,跳过队列开销 |
| `schedtick` | 配合防饥饿机制:`schedtick % 61 == 0` 时检查 GRQ |
### 2.4 schedt(全局调度器)
`schedt` 管理跨 P 的全局资源,访问需持有 `sched.lock`。
```go
type schedt struct {
lock mutex // 全局互斥锁
midle muintptr // 空闲 M 队列
pidle puintptr // 空闲 P 队列
runq gQueue // 全局 G 队列(GRQ)
runqsize int32 // GRQ 中 G 的数量
// ...
}
```
> [!note] 📝 idle 队列的设计意图
> `midle` 和 `pidle` 实现了资源的休眠与复用——不忙时释放回池中,有新任务时快速唤醒,避免 CPU 空转或频繁创建线程。
## 关联笔记
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-overview]] — GMP 概览
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-lifecycle]] — 创建与调度流程
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-preemption]] — 抢占机制
@@ -0,0 +1,285 @@
---
tags: [go, golang, go-principle, gmp-scheduler, lifecycle]
create time: 2026-06-07 16:00
---
# GMP 调度原理 — Goroutine 生命周期
## 概述
本节从第一人称视角追踪 goroutine 的完整生命周期:从 `go func()` 触发的创建流程,到被调度器翻牌子执行,再到主动让出执行权的三种方式。涵盖 §3(正向追踪)和 §4(逆向追踪)。
## 正文
### 3.1 main 函数的特殊性
`main` 函数是 Go 程序的唯一起点,由全局唯一的 `m0`(主线程)直接执行,不涉及普通的调度流程。
```go
func main() {
fn := main_main // 获取用户定义的 main 函数
fn() // 直接执行
}
```
### 3.2 普通 G 的创建流程
自己编写的 `go func(){...}` 最终被编译器转换为对 `runtime.newproc` 的调用:
```go
func newproc(fn *funcval) {
gp := getg()
pc := getcallerpc()
systemstack(func() {
newg := newproc1(fn, gp, pc) // ① 切换到 g0 栈 → 构造 G 实例
_p_ := getg().m.p.ptr()
runqput(_p_, newg, true) // ② 放入就绪队列
if mainStarted { wakep() } // ③ 唤醒空闲 P
})
// 切回原用户 G 继续执行
}
```
**核心步骤**:
1. **切换栈**:通过 `systemstack` 从当前用户 G 栈切到 M 的 `g0` 栈——创建 G 是调度层面的工作,必须交由 `g0` 处理。
2. **构造 G**:`newproc1` 分配并初始化新的 `g` 结构体(设置入口地址、程序计数器等)。
3. **入队**:`runqput` 按优先级放置:`runnext` → LRQ → GRQ(满时通过 `runqputslow` 批量迁移)。
4. **唤醒**:若有休眠中的 P,`wakep` 将其拉起来干活。
### 3.3 调度循环:从 g0 到 g
每个 M 的生命周期在两种角色间循环:**执行 `g0`(找任务)** ↔ **执行用户 G(做任务)**。
```mermaid
flowchart LR
A["M 执行 g0"] -->|"schedule()"| B["findRunnable()"]
B -->|"找到 G"| C["execute(gp)"]
C -->|"gogo()"| D["M 执行用户 G"]
D -->|"G 让渡 / 阻塞 / 结束"| A
style A fill:#fff9c4
style D fill:#e8f5e9
```
关键桩函数:
| 函数 | 方向 | 说明 |
|------|------|------|
| `mcall` / `systemstack` | G → g0 | 用户代码中切换至调度栈 |
| `gogo` | g0 → G | 恢复用户 G 的上下文,交还 CPU |
### 3.4 findRunnable:寻找任务的优先级策略
这是调度环最核心的函数,按固定优先级依次查找:
```mermaid
flowchart TD
A["每 61 次检查 GRQ<br/>防饥饿"] -->|"命中"| Z["返回 G ✓"]
A -->|"未命中"| B["LRQ: runqget<br/>无锁 CAS"]
B -->|"有"| Z
B -->|"空"| C["GRQ: globrunqget<br/>加锁"]
C -->|"有"| Z
C -->|"空"| D["netpoll<br/>非阻塞 epoll_wait"]
D -->|"有"| Z
D -->|"空"| E["work-stealing<br/>随机偷其他 P 的一半"]
E -->|"成功"| Z
E -->|"失败"| F["再次检查 GRQ"]
F -->|"有"| Z
F -->|"空"| G["释放 P → pidleput<br/>阻塞 netpoll 最后一次机会<br/>否则 stopm → 休眠"]
style A fill:#e8f5e9
style B fill:#c8e6c9
style C fill:#fff3e0
style D fill:#fff9c4
style E fill:#ffebee
style G fill:#eceff1
```
源码路径:`runtime/proc.go`
### 3.5 本地与全局队列获取细节
#### 本地队列(无锁)
```go
func runqget(_p_ *p) (gp *g, inheritTime bool) {
// 先尝试 runnext VIP 位
next := _p_.runnext
if next != 0 && _p_.runnext.cas(next, 0) {
return next.ptr(), true
}
// CAS 自旋取头部
for {
h := atomic.LoadAcq(&_p_.runqhead)
t := _p_.runqtail
if t == h { return nil, false }
gp := _p_.runq[h%256].ptr()
if atomic.CasRel(&_p_.runqhead, h, h+1) {
return gp, false
}
}
}
```
`runnext` 存放刚创建的或高优先级的 G,跳过队列开销直接执行。
#### 全局队列(加锁)
```go
func globrunqget(_p_ *p, max int32) *g {
assertLockHeld(&sched.lock)
if sched.runqsize == 0 { return nil }
gp := sched.runq.pop()
return gp
}
```
> [!question] ❓ 为什么需要锁?
> LRQ 是 per-P 私有数据,并发操作可通过 CAS 保证原子性;GRQ 被所有 P 共享,必须用互斥锁。
### 3.6 网络 IO 事件处理
```mermaid
sequenceDiagram
participant Net as netpoll
participant EP as epoll_wait
participant Q as gList
loop 遍历就绪事件
Net->>EP: epollwait(fd, events, 128, timeout)
EP-->>Net: n 个就绪事件
Net->>Q: netpollready(event)
end
Note over Q: 返回待运行的 G 列表
```
位于 `runtime/netpoll_epoll.go`,每次最多批量处理 128 个事件。
### 3.7 Work-Stealing 工作窃取
当本地和全局队列为空时,P 会从其他繁忙的 P 那里"偷"一半 G:
```go
func stealWork(now int64) (...) {
const stealTries = 4 // 最多试探 4 轮
for i := 0; i < stealTries; i++ {
for enum := stealOrder.start(fastrand()); !enum.done(); enum.next() {
p2 := allp[enum.position()]
if pp == p2 { continue } // 不偷自己
if idlepMask.read(enum.position()){ continue } // 目标也是 idle,跳过
if gp := runqsteal(pp, p2, ...); gp != nil {
return gp, ... // 偷到一半,成功!
}
}
}
return nil, ...
}
```
设计要点:
- **随机起始**:`fastrand()` 打乱探查顺序,避免热点竞争
- **偷一半**:`runqsteal` 窃取目标 P LRQ 中的一半而非全部,减少反复争夺
- **4 轮试探**:兼顾负载均衡与 CPU 利用率
### 3.8 P/M 回收机制
无事可做时,将闲置资源归还池中以节省 CPU:
```go
// 释放 P 并放回 pidle 队列
releasep()
_p_ = pidleput(_p_, now)
// 停止 M 并放入 midle 队列
stopm() // → mPark() → pthread_cond_wait()
```
> [!tip] 💡 优雅缩容
> sysmon 线程还会定期清理长时间处于 `_Psyscall` 状态的 P(见抢占篇),防止因系统调用导致 P 被白白占用。
### 4.1 让渡总览
G 拿到 CPU 后需要适时归还执行权,让给其他 G 使用。存在三种让渡方式:
```mermaid
flowchart TD
A["正在执行的 G"] --> B{"如何让渡?"}
B -->|"正常结束"| C["goexit1 → goexit0<br/>状态→Gdead,回收复用"]
B -->|"主动让出"| D["Gosched → gosched_m<br/>状态→Grunnable,入 GRQ"]
B -->|"等待外部条件"| E["gopark → park_m<br/>状态→Gwaiting,上层保管"]
C & D & E --> F["g0 调用 schedule() 寻找下一个 G"]
style C fill:#e8f5e9
style D fill:#fff9c4
style E fill:#e3f2fd
```
### 4.2 功成身退:执行结束
```go
func goexit1() { mcall(goexit0) } // G → g0
func goexit0(gp *g) { // g0 执行
casgstatus(gp, _Grunning, _Gdead) // 标记死亡
dropg() // 解除 M 绑定
gfput(_p_, gp) // 回收到 gfree 链表
schedule() // 下一轮调度
}
```
被回收的 G 缓存在 P 的 `gfree` 链表中,下次 `newproc` 可直接复用,无需重新 malloc。
### 4.3 主动让出:Gosched
```go
func Gosched() { mcall(gosched_m) } // G → g0
func goschedImpl(gp *g) { // g0 执行
casgstatus(gp, _Grunning, _Grunnable)
dropg()
lock(&sched.lock); globrunqput(gp); unlock(&sched.lock) // 入 GRQ
schedule()
}
```
> [!note] 📝 与 goexit 的区别
> Gosched 将 G 放回 **GRQ**(等待再次调度),goexit 将 G 放回 **gfree**(彻底回收)。
### 4.4 情非得已:阻塞让渡(gopark / goready)
最常见的让渡方式——等待 channel 数据、Mutex、Timer 等外部条件触发。
**阻塞流程**(`gopark`):
```go
func gopark(...) { mcall(park_m) } // G → g0
func park_m(gp *g) { // g0 执行
casgstatus(gp, _Grunning, __Gwaiting)
dropg()
schedule() // g0 去找别的 G
}
```
关键点:`_Gwaiting` 状态的 G **不入任何就绪队列**,由上层调用者(channel / mutex 等)自行保管。
**唤醒流程**(`goready` → `ready`):
```go
func goready(gp *g, traceskip int) {
systemstack(func() { ready(gp, traceskip, true) })
}
func ready(gp *g, traceskip int, next bool) { // g0 执行
casgstatus(gp, _Gwaiting, _Grunnable)
runqput(_g_.m.p.ptr(), gp, next) // 入 LRQ 或 GRQ
wakep() // 唤醒空闲 P
}
```
这一 `park` 一 `ready` 构成了 Go 并发原语的基础——G 级别阻塞,不拖垮 OS 线程。
## 关联笔记
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-overview]] — GMP 概览
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-datastructures]] — G/M/P/Schedt 数据结构
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-preemption]] — 抢占式调度机制
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-summary]] — 知识卡片
@@ -0,0 +1,147 @@
---
tags: [go, golang, go-principle, gmp-scheduler, overview]
create time: 2026-06-07 16:00
---
# GMP 调度原理 — 概览
## 概述
本文档是 Go GMP 调度模型的入门篇章,从宏观架构角度回答三个核心问题:Go 为什么不用原生 OS 线程做并发?G/M/P 三个角色各自做什么?它们之间如何协作完成工作窃取、负载均衡和 IO 轮询?
读完本节后,你将建立起 GMP 的全局视图,后续章节将深入源码层面拆解每个组件的数据结构和调度流程。
## 正文
### 1.1 从线程到协程
在理解 goroutine 之前,需要先区分两个基础概念:**线程(Thread)** 和 **协程(Coroutine)**。
```mermaid
graph LR
A["内核态"] --> B["线程<br/>OS 管理<br/>切换成本高"]
C["用户态"] --> D["协程<br/>自我管理<br/>切换成本极低"]
style A fill:#fce4ec
style C fill:#e8f5e9
style B fill:#fff9c4
style D fill:#e3f2fd
```
- **线程**:操作系统内核管理的执行单元,每次上下文切换需要陷入内核态(保存寄存器、切换页表等),开销较大但稳定性高。
- **协程**:运行在用户态的轻量级执行流,由应用程序自主调度,切换仅需修改栈指针,开销极小。
**结论**:线程重而稳,协程轻而快。
### 1.2 Go 的答案:goroutine
Go 没有直接采用传统协程模型,而是在其之上构建了 **GMP 调度体系**,使 goroutine 获得两大核心优势:
1. **灵活调度**:G(goroutine)、M(machine,OS 线程)、P(processor,逻辑处理器)三者可动态绑定和解绑,支持水平伸缩。
2. **动态栈**:初始栈仅 2~8KB,随使用量自动扩容或收缩,兼顾便利性和内存利用率。
Go 在顶层屏蔽了系统线程的概念——开发者只与 goroutine 打交道,调度细节全部由 runtime 接管。
### 1.3 GMP 架构全景
| 角色 | 职责 | 类比(工厂) |
|------|------|-------------|
| **G** | 任务单元,包含执行栈、状态、要执行的函数体 | 工作任务 |
| **M** | OS 线程封装,真正执行代码的"工人" | 工人 |
| **P** | 调度器,管理本地队列,协调 M 与 G 的关系 | 车间主管 |
```mermaid
graph TB
subgraph P0["P0"]
LRQ0["LRQ: G5 → G6 → G7"]
end
subgraph P1["P1"]
LRQ1["LRQ: G8"]
end
GRQ["GRQ 全局队列<br/>[G1, G2, G3, G4]"]
M0["M0"] --> P0_node["P0"]
M1["M1"] --> P1_node["P1"]
P0_node --> LRQ0
P1_node --> LRQ1
LRQ0 -.work-stealing.-> LRQ1
LRQ1 -.work-stealing.-> LRQ0
GRQ -->|"LRQ 空时取"| P0_node
GRQ -->|"LRQ 空时取"| P1_node
style GRQ fill:#fff9c4
style LRQ0 fill:#e8f5e9
style LRQ1 fill:#e8f5e9
```
#### 两种队列
G 存放在两种队列中,获取优先级由高到低:
| 队列 | 说明 | 锁 | 容量 |
|------|------|-----|------|
| **LRQ**(Local Run Queue) | 每个 P 私有的 G 队列 | 无锁 CAS | 最多 256 个 |
| **GRQ**(Global Run Queue) | 所有 P 共享的全局队列 | `sched.lock` | 理论上无限 |
**放置策略**:新创建的 G 优先放入当前 P 的 LRQ;LRQ 满时才加锁放入 GRQ。
**获取策略**(`findrunnable` 的执行顺序):
```mermaid
flowchart TD
A["LRQ<br/>无锁 CAS,最快"] -->|"空"| B{"GRQ<br/>需加锁"}
B -->|"空"| C["netpoll IO 就绪<br/>非阻塞 epoll_wait"]
B -->|"有"| E["找到 G ✓"]
C -->|"有"| E
C -->|"空"| D["work-stealing<br/>偷其他 P 的一半"]
D -->|"成功"| E
D -->|"失败"| F["P/M 进入休眠"]
style A fill:#e8f5e9
style B fill:#fff3e0
style C fill:#fff9c4
style D fill:#ffebee
style F fill:#eceff1
```
> [!note] 📝 防饥饿机制
> 为防止 GRQ 中的 G 长期饿死,每 61 次调度循环强制检查一次 GRQ,保证公平性。
### 1.4 GMP 生态圈
GMP 是 Go 运行时的心跳中枢,上层多数子系统都围绕它设计。
#### 1.4.1 内存管理
Go 借鉴 Google TCMalloc 思想,为每个 P 配备私有缓存 `mcache`。P 上的 G 分配小对象时直接从 `mcache` 取,完全无锁。
```mermaid
graph LR
G1["G₁"] -->|无锁| MC1["P₀ → mcache"]
G2["G₂"] -->|无锁| MC1
MC1 -->|"mcache 耗尽"| MH["mcentral → mspan"]
MH -->|"mspan 耗尽"| MM["mmapped 直接映射"]
style MC1 fill:#e8f5e9
style MH fill:#fff9c4
style MM fill:#fce4ec
```
#### 1.4.2 并发工具(Mutex / Channel)
Go 的同步原语是 **G 级别** 的——当 G 因等待 channel 数据或 Mutex 而阻塞时,它调用 `gopark` 主动让出 M,而不是阻塞整条 OS 线程。这使得单个 OS 线程上可以承载海量并发。
> [!question] ❓ 对比思考
> C++ 标准库锁一旦持有,阻塞的是整个线程,该线程上的所有协程都被迫等待。Go 通过 G 级别阻塞实现了更细粒度的并发控制。
#### 1.4.3 IO 多路复用(netpoll)
网络 IO 场景下,Go 使用 Linux `epoll` 作为底层轮询机制,并通过 `netpoll` 将其包装为 G 级别的异步通知:
- G 发起 IO → `gopark` 挂起自身
- OS 就绪 → netpoll 发现 → `goready` 唤醒 G
IO 操作由此无缝融入 GMP 调度环,无需额外线程。
## 关联笔记
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-datastructures]] — G/M/P/Schedt 数据结构源码剖析
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-lifecycle]] — Goroutine 创建、调度与让渡流程
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-preemption]] — sysmon 抢占式调度机制
- [[hzh/GolangStar/Go语言原理/gmp调度原理/gmp-summary]] — 知识卡片与全景回顾
- [[hzh/GolangStar/Go语言进阶/Goroutine]] — Goroutine 基础概念
@@ -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
@@ -0,0 +1,91 @@
---
tags: [go, golang, go-principle, gmp-scheduler, summary, cheat-sheet]
create time: 2026-06-07 16:00
---
# GMP 调度原理 — 知识卡片
## 概述
本节汇总 GMP 调度模型的全景流程与核心要点,适合作为复习回顾的速查卡片。涵盖 §6:完整生命周期流程图和关键概念总结。
## 正文
### 6.1 GMP 全景流程
```mermaid
flowchart TD
subgraph Create["创建 goroutine"]
NewProc["runtime.newproc<br/>systemstack → newproc1<br/>runqput → LRQ/GRQ"]
end
subgraph Schedule["调度循环"]
ScheduleFn["schedule → findRunnable"]
FindR["findRunnable: LRQ → GRQ → netpoll → steal"]
Execute["execute → gogo(g)"]
end
subgraph Yield["让出执行权"]
End["goexit1 → goexit0<br/>status=Gdead, gfput"]
Gosched["Gosched → gosched_m<br/>status=Grunnable, globrunqput"]
Park["gopark → park_m<br/>status=Gwaiting, 上层管理"]
end
subgraph Preempt["抢占 sysmon"]
Sysmon["sysmon 线程"]
Retake["retake: syscall + timeout"]
Collab["协作式: stackPreempt"]
Signal["非协作式: sigPreempt"]
end
subgraph Recover["恢复执行"]
Ready["goready → ready<br/>status=Grunnable, runqput"]
end
Create --> Schedule
Schedule --> Yield
Yield --> Recover
Recover --> Schedule
Sysmon --> Preempt
Preempt --> Yield
style Create fill:#e3f2fd
style Schedule fill:#e8f5e9
style Yield fill:#fff3e0
style Preempt fill:#ffebee
style Recover fill:#e8f5e9
```
### 6.2 核心要点速查
| 维度 | 要点 |
|------|------|
| **角色分工** | G = 任务(goroutine),M = 工人(OS 线程),P = 主管(逻辑处理器) |
| **队列优先级** | LRQ(无锁 CAS)> GRQ(加锁)> netpoll(IO)> steal(工作窃取) |
| **防饥饿** | 每 61 次调度强制检查一次 GRQ |
| **LRQ 容量** | 256 个 G;满时触发 `runqputslow` 批量迁移至 GRQ |
| **runnext** | VIP 位置,新创建的 G 优先放入,下次调度直接执行 |
| **协程状态机** | `_Gidle → _Grunnable → _Grunning → { _Gdead / _Gwaiting / _Gsyscall }` |
| **让渡方式** | 结束→回收到 `gfree`;`Gosched`→入 GRQ;`gopark`→`_Gwaiting` 由上层保管 |
| **唤醒机制** | `goready → ready`:改状态→`runqput`→`wakep()` |
| **系统调用抢占** | `reentersyscall` 解绑 P/M;exitsyscall 快速路径复用 oldP / 慢速路径重新分配 |
| **协作式抢占** | 设置 `stackPreempt`,在函数调用/栈检查时响应 |
| **非协作式抢占** | Go ≥ 1.14,通过 POSIX 信号注入 `asyncPreempt`,绕过函数调用依赖 |
| **sysmon 三板斧** | netpoll(IO 轮询)→ retake(抢占检查)→ GC 触发 |
### 6.3 关键数据记忆点
| 数字 | 含义 |
|------|------|
| **256** | LRQ 最大容量 |
| **61** | 每次 GRQ 强制检查间隔(schedtick) |
| **10ms** | syscall 超时阈值、运行抢占阈值(`forcePreemptNS`) |
| **4** | work-stealing 最大试探轮数 |
| **128** | netpoll epoll_wait 单次最多返回事件数 |
## 关联笔记
- [[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-preemption]] — 抢占式调度机制
- [[hzh/GolangStar/Go面试题库/GMP面试题]] — 高频面试题
@@ -74,8 +74,8 @@ timeline
Task A : 执行 : 执行 : 执行
Task B : 执行 : 执行 : 执行
section 并发 (单核)
Task A : 执行1 : : 执行3
Task B : : 执行2 :
Task A : 执行1 : 空闲 : 执行3
Task B : 空闲 : 执行2 : 空闲
```
如图所示: