news 2026/6/10 13:15:33

Go 调度器 (GMP) 揭秘:从汇编角度看 Goroutine 是如何实现“协程切换”的?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Go 调度器 (GMP) 揭秘:从汇编角度看 Goroutine 是如何实现“协程切换”的?

标签:#Go #Golang #GMP #Assembly #Runtime #Concurrency


🚀 前言:GMP 的本质是“复用”

操作系统线程(OS Thread)太重了。创建一个线程需要 1-8MB 栈内存,切换一次需要进入内核态,耗时 1-2 微秒。
Go 的 GMP 模型本质上是一个二级调度系统

  • G (Goroutine): 仅仅是一个数据结构 (struct g),包含自己的栈和指令指针 (PC),初始只占 2KB。
  • M (Machine): 真正的 OS 线程。它不懂什么协程,它只知道执行代码。
  • P (Processor): 逻辑处理器,维护了一个本地运行队列(Local Run Queue)。

核心秘密:M 并不直接执行 G 的代码,而是通过一个名为g0的特殊 Goroutine 来充当“调度中介”。
所有的切换,都不是 G 到 G 直连,而是 G1 -> g0 -> G2。


🧬 一、 切换的“黑盒”:gobuf

在 Go 的源码runtime/runtime2.go中,struct g里有一个至关重要的字段:sched
它的类型是gobuf。这就是保存 Goroutine “灵魂”的地方。

typegobufstruct{spuintptr// Stack Pointer (栈顶指针)pcuintptr// Program Counter (指令指针/下一条要执行的指令)g guintptr// 属于哪个 Gctxt unsafe.Pointer retuintptrlruintptrbpuintptr// Base Pointer (栈底指针)}

协程切换的本质,就是把 CPU 的寄存器(SP, PC, BP 等)保存到当前 G 的gobuf里,然后从目标 G 的gobuf里把寄存器恢复出来。


🎬 二、 切出 (Yield):mcallg0

当一个 Goroutine 因为channel阻塞、系统调用或被抢占时,它会调用runtime.mcall
mcall的作用是:保留案发现场,切换到g0栈,开始调度。

让我们看runtime/asm_amd64.s中的汇编代码(Plan9 汇编):

// func mcall(fn func(*g)) TEXT runtime·mcall(SB), NOSPLIT, $0-8 // 1. 取出参数 fn (通常是 schedule 函数) MOVQ fn+0(FP), DI // 2. 获取当前运行的 g (我们称之为 g_cur) get_tls(CX) MOVQ g(CX), AX // AX = g_cur // 3. 保存现场!将寄存器值写入 g_cur.sched (gobuf) MOVQ 0(SP), BX // 保存调用者的 PC (返回地址) MOVQ BX, (g_sched+gobuf_pc)(AX) LEAQ 8(SP), BX // 保存调用者的 SP MOVQ BX, (g_sched+gobuf_sp)(AX) MOVQ BP, (g_sched+gobuf_bp)(AX) // 保存 BP // 4. 切换堆栈!从 g_cur 切换到 g0 MOVQ g_m(AX), BX // BX = g_cur.m (获取当前 M) MOVQ m_g0(BX), SI // SI = m.g0 (获取 g0) // 关键一跳:把 CPU 的 SP 寄存器修改为 g0 的栈顶 MOVQ (g_sched+gobuf_sp)(SI), SP // 5. 现在我们已经运行在 g0 的栈上了 // 设置当前 g 为 g0 MOVQ SI, g(CX) // 6. 执行调度函数 fn(g_cur) // 这里的 AX 还是旧的 g_cur,作为参数传给 schedule PUSHQ AX MOVQ DI, DX CALL DX

人话翻译:
正在跑的 G 说:“我不行了,我要休息。”
于是它把自己的 SP、PC 记在小本本(gobuf)上,然后把 CPU 的 SP 指针瞬间指向了g0的栈。
瞬间,CPU 就以为自己一直是在g0上运行。接着,g0开始执行schedule()函数,去队列里找下一个幸运儿。


🚀 三、 切入 (Resume):gogo

schedule()找到下一个要运行的 G(我们叫它g_next)后,会调用runtime.execute,最终调用汇编实现的runtime.gogo
gogo的作用是:读取g_next的存档,通过修改寄存器,让 CPU “穿越”到g_next上次暂停的地方。

// func gogo(buf *gobuf) TEXT runtime·gogo(SB), NOSPLIT, $16-8 // 1. buf 是 g_next.sched MOVQ buf+0(FP), BX // BX = gobuf // 2. 从 gobuf 恢复寄存器 MOVQ gobuf_g(BX), DX MOVQ 0(DX), CX // 检查 g 是否为 nil get_tls(CX) MOVQ DX, g(CX) // 将 TLS (Thread Local Storage) 设置为 g_next MOVQ gobuf_sp(BX), SP // 🔥 恢复栈指针 SP!此刻切换完成 MOVQ gobuf_ret(BX), AX MOVQ gobuf_ctxt(BX), DX MOVQ gobuf_bp(BX), BP // 恢复 BP // 3. 准备起跳 // 清空 gobuf.sp,防止重复使用 MOVQ $0, gobuf_sp(BX) // 4. 获取下一条指令地址 PC MOVQ gobuf_pc(BX), BX // 5. 飞!跳转到 g_next 的代码位置 JMP BX

人话翻译:
g0说:“也就是你了,g_next。”
于是g0g_next的小本本拿出来,把 CPU 的 SP、BP 全部改成g_next的值。
最后由JMP BX指令,直接跳转到g_next上次停下的代码行。
对 CPU 来说,仿佛什么都没发生过,只是继续执行下一行指令而已。


🔄 四、 宏观流程:G1 -> G2

将上述两个过程结合,就是一次完整的协程切换。

切换流程图 (Mermaid):

G2 用户栈

g0 系统栈

G1 用户栈

JMP

G1 运行中

调用 mcall

mcall: 保存 G1 现场到 gobuf

mcall: 切换 SP 到 g0

执行 runtime.schedule

找到 G2

执行 runtime.execute

调用 gogo

gogo: 从 gobuf 恢复 G2 现场

G2 恢复运行


📊 五、 为什么 Go 切换这么快?

对比一下 Linux 线程切换和 Goroutine 切换:

维度Linux 线程切换Goroutine 切换
模式用户态 -> 内核态 -> 用户态纯用户态
内存涉及页表切换、L1/L2 Cache 失效仅涉及少量寄存器、L1 Cache 亲和性好
寄存器保存所有通用寄存器 + AVX/FPU 状态只保存 SP, PC, BP 等少数几个
耗时~1-2 微秒 (us)~0.2 微秒 (us)

结论:Go 通过在用户空间复写了一套微型操作系统(Runtime),避免了昂贵的系统调用(System Call)开销。


🎯 总结

  • GMP 的 M是执行载体,G是数据存档。
  • g0是连接 G1 和 G2 的桥梁,所有调度逻辑都在 g0 栈上跑。
  • mcall负责“存档并切到 g0”。
  • gogo负责“读档并切回用户 G”。
  • Go 的汇编魔法JMPMOV SP实现了这一切。

Next Step:
既然看懂了切换,建议下载Delve调试器,在汇编层面单步调试一次 Goroutine 的切换过程。在断点处输入disass,亲眼看看MOVQ (g_sched+gobuf_sp)(AX), SP这行指令是如何改变世界线的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 9:24:57

数据结构3.0 栈、队列和数组

一、栈的基本概念①栈的定义②栈的基本操作③常考题型④小结二、栈的顺序存储实现①顺序栈的定义②初始化操作③进栈操作④出栈操作⑤读栈顶元素操作⑥共享栈⑦小结三、栈的链式存储实现①链栈的定义②小结#include <stdio.h> #include <stdlib.h>// 链栈的结点结构…

作者头像 李华
网站建设 2026/6/10 9:22:18

强烈安利9个AI论文写作软件,专科生搞定毕业论文!

强烈安利9个AI论文写作软件&#xff0c;专科生搞定毕业论文&#xff01; 论文写作的救星&#xff0c;AI 工具如何改变你的学术之路 对于专科生来说&#xff0c;毕业论文可能是大学生活中最令人头疼的一关。从选题、查资料到撰写、修改&#xff0c;每一步都充满了挑战。而如今&a…

作者头像 李华
网站建设 2026/6/10 1:09:46

Flutter × OpenHarmony 跨端汇率转换应用:货币数据模型与页面实现

文章目录Flutter OpenHarmony 跨端汇率转换应用&#xff1a;货币数据模型与页面实现前言背景Flutter OpenHarmony 跨端开发介绍开发核心代码代码解析心得总结Flutter OpenHarmony 跨端汇率转换应用&#xff1a;货币数据模型与页面实现 前言 在全球化经济背景下&#xff0c;…

作者头像 李华
网站建设 2026/6/10 9:21:52

COE_Main()代码注释

///////////////////////////////////////////////////////////////////////////////////////// /*** \brief CoE (CANopen over EtherCAT) 服务主处理函数。* \details 此函数在后台被调用&#xff0c;用于处理挂起的SDO&#xff08;服务数据对象&#xff09;读写请求。* …

作者头像 李华
网站建设 2026/6/10 9:14:08

主流 AI IDE 之一的 Claude Code 介绍

Claude Code 是 Anthropic&#xff08;Claude AI 的开发公司&#xff09;于 2025 年推出的代理式&#xff08;agentic&#xff09;编码工具&#xff0c;目前被公认为终端里最强的 AI 编程助手之一。它直接运行在你的终端&#xff08;Terminal / PowerShell / cmd&#xff09;&a…

作者头像 李华
网站建设 2026/6/10 11:11:56

AI 模型输出学术内容准确率飙升97%!我只用了这个简单提示词技巧

经常用AI工具辅助学术科研与写作的同仁,可能已经对提示词工程掌握的程度比较深了。为了让模型输出的内容更准确一点,提示词工程师也会研究各种复杂的提示词技巧,比如设定角色、思维链、多样本学习等等。 但最近七哥发现还有一种能提升模型输出内容准确率的方法,该方法出自…

作者头像 李华