欢迎交流。
Go Runtime(十):Timer实现
本文源码基线为 Go 1.26.2、Linux/amd64。Go 1.23 重写了 Timer Channel 的可回收性与 Stop/Reset 保证;阅读旧版本分析时必须先确认版本边界。 Timer 不是一个独立后台 goroutine 加一张全局表。Go 1.26 把真实时间 Timer 组织在每个 P 的四叉最小堆中,由调度器和 netpoll 共同决定睡到何时、何时执行到期回调。 它同时服务 time.Sleep、time.Timer、time.Ticker、AfterFunc 和网络 Deadline。难点不只是“按时间排序”,而是并发 Stop/Reset、跨 P 修改、周期跳期、Channel 旧值以及 GC 可达性。 time包如何进入runtime time 包通过 runtime 提供的私有入口创建和修改 Timer: TEXTtime.Sleep -> runtime.timeSleep time.NewTimer -> runtime.newTimer(sendTime, channel) Timer.Stop -> runtime.stopTimer Timer.Reset -> runtime.resetTimer time.AfterFunc -> runtime.newTimer(goFunc, nil) net deadline -> runtime timer callback time.NewTimer 的源码仍写 make(chan Time, 1)。默认模式下 runtime 借助 hchan.timer 对外呈现同步、容量为零的语义;设置 GODEBUG=asynctimerchan=1 才恢复 Go 1.23 以前的异步 Timer Channel 行为。因此不能只看 make 参数推断 cap(t.C) 和 Stop/Reset 语义。 ...
Go Runtime(九):Runtime信号量与同步原语
本文源码基线为 Go 1.26.2、Linux/amd64。sync 的公开语义由标准库保证;本文讨论的状态位、队列和 runtime 接口属于版本相关实现,不应被业务代码依赖。 sync.Mutex 的竞争慢路径、RWMutex 的读写协调和 WaitGroup 的等待,最终都需要把 G 停下来并在条件满足时重新放回调度器。它们共享 runtime semaphore,但并不共享完全相同的高层状态机。 先建立边界:runtime semaphore 是“按地址等待与唤醒”的底层设施,不是公开的计数信号量类型。存放在 uint32 地址中的 token 只服务于防丢唤醒和交接协议。 从sync包到runtime 标准库通过无函数体声明和 go:linkname 连接 runtime: TEXTinternal/sync Mutex slow path -> runtime_SemacquireMutex -> runtime_Semrelease sync RWMutex -> readerSem / writerSem WaitGroup -> sema Cond -> notifyList 公开类型保留自己的原子状态;runtime 只负责等待队列、park/ready 和 contention profile。把所有语义都归因于 semaphore,会看不懂 Mutex 的 starvation、RWMutex 的 writer preference 和 WaitGroup 的 misuse 检测。 ...
Go Runtime(八):Channel与Select
本文源码基线为 Go 1.26.2、Linux/amd64。Channel 的语言语义跨平台一致;文中的字段、函数和行号固定到该版本,后续版本可能调整实现。 Channel 不是“带锁队列”的同义词。它同时实现值传递、goroutine 阻塞与唤醒、close 广播、select 多路竞争,并且要和栈复制、GC、race detector、Timer 配合。 理解 Channel 的关键,是区分三种正常数据路径:直接交接、环形缓冲区、等待队列。前两条可以立即完成;进入等待队列才会把当前 G 变为 _Gwaiting。nil Channel 的阻塞操作是没有数据路径的永久 park,属于另一个边界情况。 从语法到runtime入口 编译器把普通发送和接收分别降低为 runtime.chansend1、runtime.chanrecv1/2。非阻塞的单 case select 会被优化为 selectnbsend 或 selectnbrecv;多个有效 case 才构造 case 数组并调用 runtime.selectgo。 GOch <- x // runtime.chansend1 x = <-ch // runtime.chanrecv1 x, ok = <-ch // runtime.chanrecv2 close(ch) // runtime.closechan 这意味着 select 不是对每个 case 依次执行一次普通 channel 操作。它必须在所有相关 Channel 锁的保护下完成一次原子决策。 源码入口:walk/select.go 与 chan.go。 hchan保存什么 hchan 是 Channel 的运行时对象: ...
Go Runtime(七):Signal与cgo线程管理
本文源码基线为 Go 1.26.2、Linux/amd64。Signal 的编号、trampoline 和上下文结构具有平台差异;本文只把 Unix/Linux 主线作为具体实现,Windows 异常和 IOCP 回调需要单独对照平台文件。 信号和 cgo 都会打破普通 Go 调用链的假设:信号可以在任意指令附近异步到达,C 代码可能阻塞、修改线程状态,甚至从一个非 Go 创建的线程回调 Go。 runtime 必须在这些边界上重新建立可识别的线程、栈和执行现场,同时避免在不安全上下文中执行分配、加锁或调度操作。 Signal承担哪些职责 Go runtime 使用或处理信号的目的并不相同: 把 SIGINT、SIGTERM 等转交给 os/signal。 将某些同步 fault 转换为 Go panic。 用异步信号抢占长时间运行的 goroutine。 采样 SIGPROF 生成 CPU profile。 在 cgo 场景与 C 安装的 handler 协作。 对致命信号打印 traceback 并退出。 因此不能笼统地说“Go 捕获了信号”。同一个 handler 入口会根据 signal 类型、发生线程和当前 G/M 状态走不同分支。 sigtable描述策略 runtime 为平台信号维护一张策略表,描述信号的默认行为和处理属性。概念上包括: TEXT是否通知用户层 是否默认忽略 是否导致 panic 是否打印 stack 是否导致进程退出 是否属于同步 fault 策略还会受程序是否导入 os/signal、是否使用 cgo、signal disposition 是否被外部代码修改等因素影响。 ...
Go Runtime(六):Netpoll与Syscall
本文源码基线为 Go 1.26.2、Linux/amd64。尤其注意:Go 1.26.2 已不再使用历史上的 _Psyscall 状态;旧文章基于 _Psyscall 描述的状态机不能直接套用到当前源码。 网络等待和阻塞系统调用都会让当前 goroutine 暂时无法继续,但 runtime 的处理方式不同: TEXT非阻塞 fd + netpoll G 等在 runtime 中,M/P 可以继续调度 阻塞 syscall M 等在内核中,runtime 设法把 P 交给其他 M 理解这一区别,就能解释 Go 为什么能用同步风格编写高并发网络服务,以及为什么某些 syscall 仍会增加线程数量。 从net.Conn.Read到runtime 以 Unix 上 TCP 读取为例,调用链可简化为: TEXTnet.(*TCPConn).Read -> netFD.Read -> internal/poll.(*FD).Read -> syscall.Read -> 数据就绪:直接返回 -> EAGAIN:pollDesc.waitRead -> runtime_pollWait -> runtime.netpollblock -> gopark socket 被设置为非阻塞。第一次 read 仍然是真实系统调用;只有内核返回 EAGAIN/EWOULDBLOCK 时,当前 G 才进入 netpoll 等待。 ...
Go Runtime(五):GC、写屏障与Pacer
本文源码基线为 Go 1.26.2、Linux/amd64,默认 GOEXPERIMENT。Go 1.26.2 源码包含 build-tag 隔离的 Green Tea GC 实现,但默认 GOEXPERIMENT 下 goexperiment.GreenTeaGC == false;本文主线分析默认标记器,不把实验实现混入结论。 Go GC 的核心不是“三色”这个名词,而是在用户 goroutine 持续修改对象图时,怎样并发找出仍然可达的对象,并把暂停时间与 CPU、内存开销控制在目标范围内。 一轮 GC 可以理解为四套机制协作:标记算法判断对象生死,写屏障维护并发正确性,分配 goroutine 通过 assist 偿还标记债务,pacer 控制何时开始以及投入多少资源。 为什么需要GC 函数返回只能自然回收栈帧,无法决定堆对象是否仍被其他对象引用: GOtype Node struct { next *Node } func newNode() *Node { return &Node{} } 对象可能通过全局变量、其他堆对象、goroutine 栈或 runtime 结构继续可达。GC 必须从一组根出发遍历引用图,未被发现的堆对象才可以回收。 根对象 典型根包括: 全局变量和包级数据中的指针。 各 goroutine 活动栈帧中的指针。 寄存器或保存执行现场中的指针。 runtime 持有的特定对象引用。 编译器生成的类型位图和栈图告诉 GC 哪些位置是指针。Go 的标记器不会把任意看起来像地址的整数都当作指针,这提高了准确性,也要求 syscall、汇编和 cgo 遵守指针与栈管理规则。 ...
Go Runtime(四):内存分配器
本文源码基线为 Go 1.26.2、Linux/amd64。文中的源码链接固定到 go1.26.2 tag;对象大小阈值、size class 表和局部快路径可能随版本变化。 Go 内存分配器要同时满足三类目标:小对象分配足够快,多 P 并发时减少共享锁竞争,GC 又能由任意堆地址快速定位对象边界和标记元数据。它采用按对象规格分级、按 P 缓存、按页统一管理的结构。 对象是否进入这里首先由编译器逃逸分析决定;本文从已经确定需要堆分配的对象开始,追踪 mallocgc 到 mcache、mcentral、mheap 和页分配器。 先确认对象是否真的进入堆 new(T) 不等于堆分配,返回局部变量地址也不必机械地等于堆分配;编译器根据值是否可能在当前栈帧结束后继续可达、地址是否暴露给未知调用等条件做逃逸分析,随后内联和标量替换还可能消除分配。 GOpackage alloc type Node struct { next *Node val int } //go:noinline func makeNode(v int) *Node { return &Node{val: v} } BASHgo test -gcflags='all=-m=2' ./... go test -run='^$' -bench=. -benchmem ./... 第一条解释编译器决定,第二条测量最终二进制的每次操作分配。只看源码语法或只看逃逸日志都不足以判断生产路径成本。 分配器的层级 Go 将小对象归入 size class,再按 spanClass 管理 span。spanClass 同时编码 size class 和对象是否为 noscan。 ...
Go Runtime(三):Goroutine栈管理
本文源码基线为 Go 1.26.2、Linux/amd64。文中的源码链接固定到 go1.26.2 tag;其他版本和架构可能具有不同的汇编入口、常量或实现细节。 goroutine 能够以较低成本大量创建,一个重要原因是它不需要预留传统线程栈那样大的连续空间。Go 为普通 G 使用可增长的连续栈:空间不足时分配更大的栈、复制仍在使用的部分、修正指针,再从原调用点继续执行。 本文只讨论 goroutine 栈的运行时管理。对象为什么逃逸到堆由编译器决定,堆对象如何分配放在下一篇:内存分配器中。 三类栈不要混淆 一个 M 至少关联三种执行上下文: 上下文 作用 是否普通调度G 用户 G 的栈 执行用户 Go 函数,可增长和缩小 是 m.g0 的系统栈 调度、栈扩容、部分 runtime 内部路径 否 m.gsignal 的信号栈 Unix 信号处理 否 g0 和 gsignal 都是 g,但它们不是放进运行队列的普通 goroutine。newstack 必须在 g0 上运行,因为当前用户栈已经被判定为不足,不能继续依赖它完成内存分配和现场搬迁。 g中的栈字段 runtime.g 的栈相关字段是理解整条链路的入口: GOtype g struct { stack stack stackguard0 uintptr stackguard1 uintptr m *m sched gobuf syscallsp uintptr syscallpc uintptr // ... } type stack struct { lo uintptr hi uintptr } stack 描述实际区间 [lo, hi)。Go 栈在 amd64 上向低地址增长,SP 越来越接近 lo。stackguard0 是普通 Go 函数入口用于比较的保护值;正常情况下约为 stack.lo + stackGuard,也可以被设置成特殊值 stackPreempt,借用同一套入口检查触发协作式抢占。 ...
Go Runtime(二):GMP与Goroutine调度
本文源码基线为 Go 1.26.2、Linux/amd64。调度器变化频繁;函数名、队列容量和分支顺序均以固定版本源码为准。 Go 调度器解决的核心问题是:怎样把大量 goroutine 映射到较少的 OS 线程,同时控制竞争、阻塞和调度延迟。 理解调度器不需要记住所有函数。先抓住三件事:执行资格属于 P,等待发生在 G 上,真正运行机器指令的是 M。 GMP模型 TEXTG(goroutine) 栈、入口函数、调度现场、状态、等待原因 M(machine) OS线程、当前G、g0、信号处理上下文 P(processor) 本地运行队列、runnext、mcache、timer、执行资格 抽象关系如下: TEXT P.local runq [G][G][G][G] | v M(OS thread) + P ----> 执行某个 G | P.runnext M 必须绑定 P 才能执行普通 Go 代码。没有 P 的 M 可以停在线程阻塞、syscall、cgo 或 runtime 的特定路径中,但不能继续执行任意用户 Go 代码。 GOMAXPROCS 控制 P 的数量。它限制的是并行执行 Go 代码的能力,不限制 goroutine 或 OS 线程的总数。 ...
Go Runtime(一):运行时架构与程序启动
本文源码基线为 Go 1.26.2、Linux/amd64。源码链接固定到 go1.26.2 tag;其他操作系统、架构和构建模式的入口汇编会不同。 Go runtime 是链接进 Go 可执行文件的一组基础设施。它不解释字节码,也不需要作为独立进程启动;它和编译器、标准库及操作系统共同实现 Go 的语言语义和执行模型。 本文先建立 runtime 的模块边界,再从 Linux/amd64 可执行文件入口追踪到 main.main。后续文章分别沿调度、栈、分配、GC、Channel、同步、Timer 和系统集成深入。 Runtime解决什么问题 一段普通 Go 程序表面上只包含函数调用、对象分配和 goroutine: GOpackage main func main() { ch := make(chan int) go func() { ch <- 1 }() println(<-ch) } 但这些语法背后至少需要 runtime 完成以下工作: 语法或行为 Runtime职责 go f() 创建 G、准备初始栈、加入可运行队列 new(T)、&T{} 按对象大小和指针信息分配内存 channel 收发 匹配发送方与接收方,必要时阻塞或唤醒 G 栈空间不足 分配更大连续栈,复制现场并修正栈内指针 堆对象失去引用 并发标记、清扫和复用内存 网络读写等待 将 G 挂起到 netpoll,fd 就绪后恢复运行 panic/defer/recover 展开调用栈并按语言规则执行延迟调用 因此,runtime 不是一个单独功能,而是支撑程序启动、调度、内存、GC、同步和系统交互的一组机制。 ...