vault backup: 2026-06-07 16:52:15
This commit is contained in:
+10
-1505
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 : 空闲
|
||||
```
|
||||
|
||||
如图所示:
|
||||
|
||||
Reference in New Issue
Block a user