一、type g struct - Goroutine 结构体
1. 栈管理相关 (Stack Management)
go
stack stack // 栈内存范围 [stack.lo, stack.hi)
stackguard0 uintptr // Go 栈增长检查点,通常是 stack.lo+StackGuard,也可设为 StackPreempt 触发抢占
stackguard1 uintptr // C 栈增长检查点,用于 g0 和 gsignal 栈
- stack: 描述实际栈内存的上下界
- stackguard0: 函数序言(prologue)中检查栈溢出的哨兵值,设为特殊值可触发抢占
- stackguard1: 仅用于特殊 goroutine(g0、信号栈),防止 C 代码栈溢出
2. 异常处理链表
go
_panic *_panic // 最内层的 panic 链表头
_defer *_defer // 最内层的 defer 链表头
- 采用链表存储,支持嵌套的 panic/defer
3. 调度核心字段
go
m *m // 当前绑定的 M(如果正在运行)
sched gobuf // 调度上下文:保存 SP、PC、G 等寄存器
syscallsp uintptr // 系统调用时的 SP,供 GC 使用
syscallpc uintptr // 系统调用时的 PC,供 GC 使用
stktopsp uintptr // 栈顶 SP,用于 traceback 验证
- sched: 核心!保存 goroutine 被调度出去时的 CPU 寄存器状态
- syscallsp/pc: 系统调用期间,栈可能被扫描,需要记录位置
4. 状态与标识
go
atomicstatus atomic.Uint32 // Goroutine 状态(_Gidle/_Grunnable/_Grunning/_Gsyscall/_Gwaiting/_Gdead...)
stackLock uint32 // 栈操作锁,防止并发修改栈
goid uint64 // Goroutine 唯一 ID
schedlink guintptr // 链表指针,用于构建运行队列
5. 阻塞追踪
go
waitsince int64 // G 阻塞的近似时间戳
waitreason waitReason // 阻塞原因(chan receive、sync.Mutex.Lock 等)
6. 抢占相关
go
preempt bool // 抢占信号(与 stackguard0 = stackpreempt 重复)
preemptStop bool // 抢占时是否转为 _Gpreempted 状态
preemptShrink bool // 是否在安全点收缩栈
asyncSafePoint bool // 是否在异步安全点停止(栈上可能有非精确指针信息)
- Go 1.14+ 异步抢占的关键标志位
7. 调试与 Profiling
go
parentGoid uint64 // 创建此 goroutine 的父 goroutine ID
gopc uintptr // 创建此 goroutine 的 PC(go 语句位置)
startpc uintptr // goroutine 函数的 PC
ancestors *[]ancestorInfo // 祖先信息(debug.tracebackancestors=1 时启用)
8. 同步与通道
go
param unsafe.Pointer // 通用参数指针:
// 1. channel 唤醒时指向 sudog
// 2. GC assist 完成信号
// 3. debugCallWrap 传参
waiting *sudog // 此 G 正在等待的 sudog 链表
9. 性能追踪
go
tracking bool // 是否追踪调度延迟统计
trackingSeq uint8 // 追踪序列号
trackingStamp int64 // 开始追踪的时间戳
runnableTime int64 // 在 _Grunnable 状态累计的时间
10. GC 辅助
go
gcAssistBytes int64 // GC 辅助信用(正值=可分配字节,负值=需辅助扫描)
- 控制 mutator 是否需要参与 GC 标记工作
二、type m struct - 机器线程结构体
1. 特殊 Goroutine
go
g0 *g // 调度专用 goroutine(更大的栈,用于执行调度代码)
gsignal *g // 信号处理 goroutine
curg *g // 当前正在运行的用户 goroutine
- g0: 每个 M 都有,用于执行调度器、GC 等 runtime 代码
- curg: 指向用户代码的 goroutine
2. P 绑定
go
p puintptr // 当前绑定的 P(执行 Go 代码时必须持有)
nextp puintptr // 唤醒 M 时即将绑定的 P
oldp puintptr // 系统调用前绑定的 P(用于恢复)
3. M 状态
go
id int64 // M 的唯一 ID
spinning bool // 是否正在窃取任务(Work Stealing 状态)
blocked bool // 是否在 note 上阻塞
locks int32 // 持有的锁计数(用于检测死锁)
- spinning: 关键指标!在 schedtrace 中看到的 spinningthreads 就是统计这个
4. 抢占控制
go
preemptoff string // 非空表示禁止抢占 curg(值为原因描述)
5. CGO 相关
go
incgo bool // 是否正在执行 cgo 调用
ncgocall uint64 // 累计 cgo 调用次数
ncgo int32 // 当前进行中的 cgo 调用数
cgoCallers *cgoCallers // cgo 崩溃时的追踪信息
6. 线程本地存储
go
tls [tlsSlots]uintptr // Thread-Local Storage(x86 使用)
7. 系统调用支持
go
syscalltick uint32 // 系统调用计数(用于 sysmon 检测阻塞)
libcall libcall // 低级系统调用参数存储
8. 信号抢占(Go 1.14+)
go
preemptGen atomic.Uint32 // 完成的抢占信号计数
signalPending atomic.Uint32 // 是否有待处理的抢占信号
9. 链表管理
go
alllink *m // 全局 M 链表(allm)
schedlink muintptr // 调度器管理的 M 链表
freelink *m // 空闲 M 链表(sched.freem)
三、type p struct - 逻辑处理器结构体
1. 基本信息
go
id int32 // P 的 ID(0 到 GOMAXPROCS-1)
status uint32 // P 的状态(_Pidle/_Prunning/_Psyscall/_Pgcstop/_Pdead)
m muintptr // 反向引用:当前绑定的 M
link puintptr // 空闲 P 链表指针
2. 调度统计
go
schedtick uint32 // 每次调度调用递增
syscalltick uint32 // 每次系统调用递增
sysmontick sysmontick // sysmon 最后观察的 tick 值(用于检测长时间阻塞)
- schedtick :
findRunnable中每 61 次从全局队列取任务,就是检查这个
3. 本地运行队列 🔥 核心
go
runqhead uint32 // 队列头索引
runqtail uint32 // 队列尾索引
runq [256]guintptr // 环形队列,最多 256 个 G
runnext guintptr // 优先级最高的下一个 G(生产者-消费者模式优化)
- runq: 无锁访问的本地队列,Work Stealing 的来源
- runnext: 避免调度延迟,直接运行刚创建的 goroutine
4. 对象缓存池(性能优化)
go
deferpool []*_defer // defer 结构体池
gFree struct{ ... } // 已终止的 G 缓存(避免频繁分配)
sudogcache []*sudog // sudog 缓存
mspancache struct{ ... } // mspan 缓存
pinnerCache *pinner // pinner 对象缓存
5. 内存分配
go
mcache *mcache // 每个 P 的内存缓存(TCMalloc 风格)
pcache pageCache // 页缓存
palloc persistentAlloc // 持久化分配器
6. Goroutine ID 生成
go
goidcache uint64 // 本地 ID 缓存起始值
goidcacheend uint64 // 本地 ID 缓存结束值
- 批量从全局
sched.goidgen获取 ID 段,减少锁竞争
7. Timer 管理 ⏰
go
timers []*timer // 定时器堆
timer0When atomic.Int64 // 堆顶定时器的触发时间
timerModifiedEarliest atomic.Int64 // 最早被修改的定时器
timersLock mutex // 定时器锁
numTimers atomic.Uint32 // 定时器数量
deletedTimers atomic.Uint32 // 已删除定时器数量
- Go 1.14+ 每个 P 独立管理定时器,避免全局锁
8. GC 相关
go
gcAssistTime int64 // GC 辅助标记耗时
gcFractionalMarkTime int64 // 分数标记工作者耗时
gcMarkWorkerMode gcMarkWorkerMode // 下一个标记工作者模式
gcw gcWork // GC 工作缓冲
wbBuf wbBuf // 写屏障缓冲
9. 抢占标志
go
preempt bool // 标记此 P 应尽快进入调度器(不管正在运行什么 G)
sysmon会设置此标志强制调度
10. 统计与调试
go
statsSeq atomic.Uint32 // 统计写入计数器(奇数=正在写入)
maxStackScanDelta int64 // 累计的栈空间量
scannedStackSize uint64 // 已扫描的栈大小
scannedStacks uint64 // 已扫描的 goroutine 数量
runSafePointFn uint32 // 是否在下一个安全点执行 safePointFn
四、关键关系图
perl
用户代码执行路径:
G (goroutine) --绑定--> M (thread) --必须持有--> P (processor)
↓ ↓ ↓
用户栈 g0栈 本地runq[256]
调度上下文 调度逻辑 mcache
五、面试高频问题
Q: 为什么需要 P? A: P 的本地队列 runq 实现无锁调度,避免所有 M 竞争全局队列。同时 P 可以在 M 阻塞时移交(Hand Off),保证并行度。
Q: runnext 的作用? A: 生产者-消费者模式优化。当前 G 创建新 G 时,新 G 放入 runnext,继承当前时间片,减少调度延迟。
Q: g0 是什么? A: 每个 M 的调度专用 goroutine,栈更大(8KB),用于执行调度器、GC、栈增长等 runtime 代码。用户代码在 curg 上运行,调度代码在 g0 上运行。
Q: spinning 的含义? A: M 处于"自旋"状态,即没有绑定 G,正在执行 Work Stealing 寻找任务。GODEBUG=schedtrace 中的 spinningthreads 就是统计这个。
这些结构体的设计体现了 Go 调度器的精髓:去中心化(本地队列)+ 工作窃取(负载均衡)+ 抢占式调度(公平性)。