Go runtime 调度器精讲(一):Go 程序初始化

news/2024/11/16 13:04:51/文章来源:https://www.cnblogs.com/xingzheanan/p/18407990

原创文章,欢迎转载,转载请注明出处,谢谢。


0. 前言

本系列将介绍 Go runtime 调度器。要学好 Go 语言,runtime 运行时是绕不过去的,它相当于一层“操作系统”对我们的程序做“各种类型”的处理。其中,调度器作为运行时的核心,是必须要了解的内容。本系列会结合 Go plan9 汇编,深入到 runtime 调度器的源码层面去看程序运行时,goroutine 协程创建等各种场景下 runtime 调度器是如何工作的。

本系列会运用到 Go plan9 汇编相关的知识,不熟悉的同学可先看看 这里 了解下。

1. Go 程序初始化

首先,从一个经典的 Hello World 程序入手,查看程序的启动,以及启动该程序时调度器做了什么。

package mainfunc main() {println("Hello World")
}

1.1 准备

程序启动经过编译和链接两个阶段,我们可以通过 go build -x hello.go 查看构建程序的过程:

# go build -x hello.go 
...
// compile 编译 hello.go
/usr/local/go/pkg/tool/linux_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p main -complete -buildid uHBjeIlqt1oQO9TLC5SE/uHBjeIlqt1oQO9TLC5SE -goversion go1.21.0 -c=3 -nolocalimports -importcfg $WORK/b001/importcfg -pack ./hello.go
...
// link 链接库文件生成可执行文件
/usr/local/go/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=27kmwBgRtsWy6cL5ofDV/uHBjeIlqt1oQO9TLC5SE/Ye3W7EEwzML-FanTsWbe/27kmwBgRtsWy6cL5ofDV -extld=gcc $WORK/b001/_pkg_.a

这里省略了不相关的输出,经过编译,链接过程之后得到可执行文件 hello

# ls
go.mod  hello  hello.go
# ./hello 
Hello World

1.2 进入程序

上一节生成了可执行程序 hello。接下来进入本文的主题,通过 dlv 进入 hello 程序,查看在执行 Go 程序时,运行时做了什么。

我们可以通过 readelf 查看可执行程序的入口:

# readelf -h ./hello
ELF Header:...Entry point address:               0x455e40

省略了不相关的信息,重点看 Entry point address,它是进入 hello 程序的入口点。通过 dlv 进入该入口点:

# dlv exec ./hello
Type 'help' for list of commands.
(dlv) b *0x455e40
Breakpoint 1 set at 0x455e40 for _rt0_amd64_linux() /usr/local/go/src/runtime/rt0_linux_amd64.s:8

可以看到入口点指向的是 /go/src/runtime/rt0_linux_amd64.s 中的 _rt0_amd64_linux() 函数。

接下来,进入该函数查看启动 Go 程序时,运行时做了什么。

// c 命令执行到入口点位置
(dlv) c
> _rt0_amd64_linux() /usr/local/go/src/runtime/rt0_linux_amd64.s:8 (hits total:1) (PC: 0x455e40)
Warning: debugging optimized function3: // license that can be found in the LICENSE file.4:5: #include "textflag.h"6:7: TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
=>   8:         JMP     _rt0_amd64(SB)      // 跳转到 _rt0_amd64// si 单步执行指令
(dlv) si
> _rt0_amd64() /usr/local/go/src/runtime/asm_amd64.s:16 (PC: 0x454200)
Warning: debugging optimized function
TEXT _rt0_amd64(SB) /usr/local/go/src/runtime/asm_amd64.s
=>      asm_amd64.s:16  0x454200        488b3c24        mov rdi, qword ptr [rsp]asm_amd64.s:17  0x454204        488d742408      lea rsi, ptr [rsp+0x8]asm_amd64.s:18  0x454209        e912000000      jmp $runtime.rt0_go     // 这里跳转到 runtime 的 rt0_go// 进入 rt0_go
(dlv) si
> runtime.rt0_go() /usr/local/go/src/runtime/asm_amd64.s:161 (PC: 0x454220)
Warning: debugging optimized function
TEXT runtime.rt0_go(SB) /usr/local/go/src/runtime/asm_amd64.s
=>      asm_amd64.s:161 0x454220        4889f8          mov rax, rdiasm_amd64.s:162 0x454223        4889f3          mov rbx, rsiasm_amd64.s:163 0x454226        4883ec28        sub rsp, 0x28asm_amd64.s:164 0x45422a        4883e4f0        and rsp, -0x10asm_amd64.s:165 0x45422e        4889442418      mov qword ptr [rsp+0x18], raxasm_amd64.s:166 0x454233        48895c2420      mov qword ptr [rsp+0x20], rbx

rt0_go 是 runtime 执行 Go 程序的入口。

需要补充说明的是:我们使用的 si 显示的是 CPU 单步执行的指令,是 CPU 真正执行的指令。而 Go plan9 汇编是“优化”了的汇编指令,所以会发现 si 显示的输出和 asm_amd64.s 中定义的不一样。在实际分析的时候可以结合两者一起分析。

结合着 asm_amd64.s/rt0_go 分析 si 输出的 CPU 指令:

=>      asm_amd64.s:161 0x454220        4889f8          mov rax, rdi                    // 将 rdi 寄存器中的 argc 移到 rax 寄存器:rax = argcasm_amd64.s:162 0x454223        4889f3          mov rbx, rsi                    // 将 rsi 寄存器中的 argv 移到 rbx 寄存器:rbx = argvasm_amd64.s:163 0x454226        4883ec28        sub rsp, 0x28                   // 开辟栈空间asm_amd64.s:164 0x45422a        4883e4f0        and rsp, -0x10                  // 对齐栈空间为 16 字节的整数倍(因为 CPU 的一组 SSE 指令需要内存地址必须是 16 字节的倍数)asm_amd64.s:165 0x45422e        4889442418      mov qword ptr [rsp+0x18], rax   // 将 argc 移到栈空间 [rsp+0x18]asm_amd64.s:166 0x454233        48895c2420      mov qword ptr [rsp+0x20], rbx   // 将 argv 移到栈空间 [rsp+0x20]

画出栈空间如下图:

image

继续分析:

(dlv) si
> runtime.rt0_go() /usr/local/go/src/runtime/asm_amd64.s:170 (PC: 0x454238)
Warning: debugging optimized functionasm_amd64.s:166 0x454233        48895c2420              mov qword ptr [rsp+0x20], rbx
=>      asm_amd64.s:170 0x454238        488d3d815b0700          lea rdi, ptr [runtime.g0]           // 将 runtime.g0 的地址移到 rdi 寄存器中,rdi = &g0asm_amd64.s:171 0x45423f        488d9c240000ffff        lea rbx, ptr [rsp+0xffff0000]       // 将 [rsp+0xffff0000] 地址的值移到 rbx 中,后面会讲asm_amd64.s:172 0x454247        48895f10                mov qword ptr [rdi+0x10], rbx       // 将 rbx 中的地址,移到 [rdi+0x10],实际是移到 g0.stackguard0asm_amd64.s:173 0x45424b        48895f18                mov qword ptr [rdi+0x18], rbx       // 将 rbx 中的地址,移到 [rdi+0x18],实际是移到 g0.stackguard1asm_amd64.s:174 0x45424f        48891f                  mov qword ptr [rdi], rbx            // 将 rbx 中的地址,移到 [rdi],实际是移到 g0.stack.loasm_amd64.s:175 0x454252        48896708                mov qword ptr [rdi+0x8], rsp        // 将 rsp 中的地址,移到 [rdi+0x8],实际是移到 g0.stack.hi

指令中 runtime.g0 为运行时主线程提供运行的执行环境,它并不是执行用户代码的 goroutine。

使用 regs 查看寄存器 rbx 存储的是什么:

(dlv) regsRip = 0x000000000045423fRsp = 0x00007ffec8d155f0Rbx = 0x00007ffec8d15628(dlv) si
> runtime.rt0_go() /usr/local/go/src/runtime/asm_amd64.s:172 (PC: 0x454247)
Warning: debugging optimized functionasm_amd64.s:171 0x45423f        488d9c240000ffff        lea rbx, ptr [rsp+0xffff0000]
=>      asm_amd64.s:172 0x454247        48895f10                mov qword ptr [rdi+0x10], rbx(dlv) regsRip = 0x0000000000454247Rsp = 0x00007ffec8d155f0Rbx = 0x00007ffec8d055f0

可以看到,这段指令实际指向的是一段栈空间,rsp:0x00007ffec8d155f0 指向的是栈底,rbx:0x00007ffec8d055f0 指向的是栈顶,它们的内存空间是 64KB。

根据上述分析,画出栈空间布局如下图:

image

继续往下分析,省略一些不相关的汇编代码。直接从 asm_amd64.s/runtime·rt0_go:258 开始看:

258	    LEAQ	runtime·m0+m_tls(SB), DI
259	    CALL	runtime·settls(SB)
260
261	    // store through it, to make sure it works
262	    get_tls(BX)
263	    MOVQ	$0x123, g(BX)
264	    MOVQ	runtime·m0+m_tls(SB), AX
265	    CMPQ	AX, $0x123
266	    JEQ 2(PC)
267	    CALL	runtime·abort(SB)

dlv 打断点,进入 258 行汇编指令处:

(dlv) b /usr/local/go/src/runtime/asm_amd64.s:258
Breakpoint 2 set at 0x4542cb for runtime.rt0_go() /usr/local/go/src/runtime/asm_amd64.s:258
(dlv) c
(dlv) si
> runtime.rt0_go() /usr/local/go/src/runtime/asm_amd64.s:259 (PC: 0x4542d2)
Warning: debugging optimized function// 将 [runtime.m0+136] 地址移到 rdi,rdi = &runtime.m0.tlsasm_amd64.s:258 0x4542cb*       488d3d565f0700                  lea rdi, ptr [runtime.m0+136]// 调用 runtime.settls 设置线程本地存储
=>      asm_amd64.s:259 0x4542d2        e809240000                      call $runtime.settls// 将 0x123 移到 fs:[0xfffffff8]asm_amd64.s:263 0x4542d7        6448c70425f8ffffff23010000      mov qword ptr fs:[0xfffffff8], 0x123// 将 [runtime.m0+136] 的值移到 rax 寄存器中asm_amd64.s:264 0x4542e4        488b053d5f0700                  mov rax, qword ptr [runtime.m0+136]// 比较 rax 寄存器的值是否等于 0x123,如果不等于则执行 call $runtime.abortasm_amd64.s:265 0x4542eb        483d23010000                    cmp rax, 0x123asm_amd64.s:266 0x4542f1        7405                            jz 0x4542f8asm_amd64.s:267 0x4542f3        e808040000                      call $runtime.abort

这段指令涉及到线程本地存储的知识。线程本地存储(TLS)是一种机制,允许每个线程有自己独立的一组变量,即使这些变量在多个线程之间共享相同的代码。在 Go runtime 中,每个操作系统线程(M)都需要知道自己当前正在执行哪个 goroutine(G)。为了高效地访问这些信息,Go runtime 使用 TLS 来存储 G 的指针。这样每个线程都可以通过 TLS 快速找到自己当前运行的 G。m0 是 Go 程序启动时的第一个操作系统线程,并且负责初始化整个 Go runtime。在其他线程通过 Go runtime 的调度器创建时,调度器会自动为它们设置 TLS,并将 G 的指针写入 TLS。但 m0 是一个特殊的线程,它直接由操作系统创建,而没有经过 Go 调度器,因此需要通过汇编指令设置 TLS。

这段指令的逻辑是将 runtime.m0.tls 的地址送到 rdi 寄存器中,接着调用 runtime.settls 设置 fs 段基址寄存器的值,使得通过段基址和偏移量就能访问到 m0.tls。最后验证设置的 [段基址:偏移量] 能否正确的访问到 m0.tls,将 0x123 传到 [段基址:偏移量],这时如果访问正确,应该传给的是 m0.tls[0] = 0x123,然后将 [runtime.m0+136] 的内容,即 m0.tls[0] 拿出来移到 rax 寄存器做比较,如果一样,则说明通过 [段基址:偏移量] 可以正确访问到 m0.tls,否则调用 runtime.abort 退出 runtime

每个线程都有自己的一组 CPU 寄存器值,不同的线程通过不同的段 fs 基址寄存器私有的存储全局变量。更详细的信息请参考 Go语言调度器源代码情景分析之十:线程本地存储。

为加深这块理解,我们从汇编角度看具体是怎么设置的。asm_amd64.s:258 0x4542cb*       488d3d565f0700                  lea rdi, ptr [runtime.m0+136]
=> rdi = &runtime.m0.tls = 0x00000000004ca228asm_amd64.s:259 0x4542d2        e809240000                      call $runtime.settls
=> 设置的是 Fs_base 段基址寄存器的值,regs 查看 Fs_base=0x00000000004ca230asm_amd64.s:263 0x4542d7        6448c70425f8ffffff23010000      mov qword ptr fs:[0xfffffff8], 0x123
=> fs:[0xfffffff8],fs 是段基址,实际是 Fs_base 段基址寄存器的值,[0xfffffff8] 是偏移量。fs:[0xfffffff8] = 0x00000000004ca230:[0xfffffff8] = 0x00000000004ca228
=> 实际通过段基址寄存器 fs:[0xfffffff8] 访问的内存地址就是 m0.tls 的地址 0x00000000004ca228

继续往下执行:

=>      asm_amd64.s:271 0x4542f8        488d0dc15a0700                  lea rcx, ptr [runtime.g0]               // 将 runtime.g0 的地址移到 rcx,rcx = &runtime.g0asm_amd64.s:272 0x4542ff        6448890c25f8ffffff              mov qword ptr fs:[0xfffffff8], rcx      // 将 rcx 移到 m0.tls,实际是 m0.tls[0] = &runtime.g0asm_amd64.s:273 0x454308        488d05915e0700                  lea rax, ptr [runtime.m0]               // 将 runtime.m0 的地址移到 rax,rax = &runtime.m0asm_amd64.s:276 0x45430f        488908                          mov qword ptr [rax], rcx                // 将 runtime.g0 的地址移到 runtime.m0,实际是 runtime.m0.g0 = &runtime.g0asm_amd64.s:278 0x454312        48894130                        mov qword ptr [rcx+0x30], rax           // 将 runtime.m0 的地址移到 runtime.g0.m,实际是 runtime.g0.m = &runtime.m0

上述指令做的是关联主线程 m0g0,这样 m0 就有了运行时执行环境。画出内存布局如下图:

image

2. 小结

至此,我们的程序初始化部分就告一段落了,下一篇将正式进入调度器的部分。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/795384.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

煤矿皮带跑偏监测识别系统

煤矿皮带跑偏监测识别系统对皮带状况进行实时监测,不用手动控制。一旦监测到皮带跑偏或者其他异常情况时,应该马上开展警报,通知监督管理办公室,并提醒负责人及时处置,并把警报截屏和视频储存到数据库系统系统中生成表格。煤矿皮带跑偏监测识别系统根据时间段对告警记录和…

GIS建库软件:地理信息与遥感领域的数据基石

在地理信息与遥感科学的浩瀚星海中,建库软件是构筑数据宇宙的基石,它不仅承载着海量地理空间数据,更是连接信息与应用的桥梁。本文将从其定义、关键技术、应用场景及未来发展四个方面,深入剖析建库软件如何支撑并推动整个行业的发展。建库软件:数据海洋的港湾建库软件,顾…

mysql月份前后格式变化记录

select DATE_FORMAT(2024-10-11 - INTERVAL 1 MONTH,%Y-%m) 一, DATE_FORMAT(2024-10-11,%Y-%m) 二, DATE_ADD(2024-10-11, INTERVAL 1 MONTH) 三 ,DATE_FORMAT(DATE_ADD(2024-10-11, INTERVAL 1 MONTH) ,%Y-%m) 四,REPLACE(DATE_FORMAT(DATE_ADD(2024-10-11, INTERVAL 1 MONT…

煤矿皮带撕裂检测系统

煤矿皮带撕裂检测系统可以全天候监管皮带的运送的工作情况,当煤矿皮带撕裂检测系统监管皮带撕裂时,马上停止皮带的运送,精准定位到皮带的裂开部位,工作员能够及时到现场维护保养。及时发现皮带撕裂,能够减少安全事故带来的损失,能够有效提升皮带机生产运输过程的效率。煤…

GIS数据采集软件:地理信息与遥感技术的智慧之眼

在信息时代,数据如同血液,滋养着各行各业的创新与进步,而地理信息与遥感领域中的数据采集软件,正是这股生命之源,它不仅为科学研究、城市规划、环境保护、灾害监测、资源管理等提供了精确数据支持,更是智慧城市建设的基石。本文将深入探讨其技术发展、应用及未来趋势,为…

工地反光衣穿戴识别

工地反光衣穿戴识别系统依据现场监控摄像头采集的视频图像,应用最新的神经网络算法,检测到一些人不穿反光衣服时,前端抓拍设备实时上传视频流至系统服务器,然后实时读取视频流并进行语音报警广播。工地反光衣穿戴识别系统的另一个闪光点是能够应用现有的监控摄像机,实时监…

深入理解DocumentFragment -文档片段

什么是文档片段? (MDN解释:)DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document 使用,就像标准的document一样,存储由节点(nodes)组成的文档结构。 作用是什么 与document相比,最大的区别是DocumentFragment 不是真实 DOM…

LeetCode Hot100刷题记录-141.环形链表

给你一个链表的头节点 head ,判断链表中是否有环。 如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅…

[Python] Python 基础教程

1 概述 1.1 简介Python 是一种解释型、面向对象、动态数据类型的高级程序设计语言。https://www.python.org/Python 由 Guido van Rossum 于 1989 年底发明,第一个公开发行版发行于 1991 年。 像 Perl 语言一样, Python 源代码同样遵循 GPL(GNU General Public License) 协议。…

从数据洞察到智能决策:合合信息infiniflow RAG技术的实战案例分享

从数据洞察到智能决策:合合信息&infiniflow RAG技术的实战案例分享从数据洞察到智能决策:合合信息&infiniflow RAG技术的实战案例分享 标题取自 LLamaIndex,这个内容最早提出于今年 2 月份 LlamaIndex 官方博客。从 22 年 chatGpt 爆火,23 年大模型尝鲜,到 24 年真…

数据飞轮转进快递行业 能够为企业带来哪些新想象

快递行业的2024年下半场竞赛已然开始,鉴于双11、年货节等电商营销重要节点都集中在下半年,流转出快递行业在下半年的巨大市场机会,数据飞轮或许能成为更多快递企业的增长动力,转出更为稳健的业绩增长。更多技术交流、求职机会,欢迎关注字节跳动数据平台微信公众号,回复【…