文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

详解 Go 程序的启动流程,你知道 g0,m0 是什么吗?

2024-12-03 06:39

关注

大家好,我是煎鱼。

自古应用程序均从 Hello World 开始,你我所写的 Go 语言亦然:

  1. import "fmt" 
  2.  
  3. func main() { 
  4.  fmt.Println("hello world."

这段程序的输出结果为 hello world.,就是这么的简单又直接。但这时候又不禁思考了起来,这个 hello world. 是怎么输出来,经历了什么过程。

真是非常的好奇,今天我们就一起来探一探 Go 程序的启动流程。其中涉及到 Go Runtime 的调度器启动,g0,m0 又是什么?

车门焊死,正式开始吸鱼之路。

Go 引导阶段

查找入口

首先编译上文提到的示例程序:

  1. $ GOFLAGS="-ldflags=-compressdwarf=false" go build  

在命令中指定了 GOFLAGS 参数,这是因为在 Go1.11 起,为了减少二进制文件大小,调试信息会被压缩。导致在 MacOS 上使用 gdb 时无法理解压缩的 DWARF 的含义是什么(而我恰恰就是用的 MacOS)。

因此需要在本次调试中将其关闭,再使用 gdb 进行调试,以此达到观察的目的:

  1. $ gdb awesomeProject  
  2. (gdb) info files 
  3. Symbols from "/Users/eddycjy/go-application/awesomeProject/awesomeProject"
  4. Local exec file: 
  5.  `/Users/eddycjy/go-application/awesomeProject/awesomeProject', file type mach-o-x86-64. 
  6.  Entry point: 0x1063c80 
  7.  0x0000000001001000 - 0x00000000010a6aca is .text 
  8.  ... 
  9. (gdb) b *0x1063c80 
  10. Breakpoint 1 at 0x1063c80: file /usr/local/Cellar/go/1.15/libexec/src/runtime/rt0_darwin_amd64.s, line 8. 

通过 Entry point 的调试,可看到真正的程序入口在 runtime 包中,不同的计算机架构指向不同。例如:

其最终指向了 rt0_darwin_amd64.s 文件,这个文件名称非常的直观:

Breakpoint 1 at 0x1063c80: file /usr/local/Cellar/go/1.15/libexec/src/runtime/rt0_darwin_amd64.s, line 8.

rt0 代表 runtime0 的缩写,指代运行时的创世,超级奶爸:

同时 Go 语言还支持更多的目标系统架构,例如:AMD64、AMR、MIPS、WASM 等:

源码目录

若有兴趣可到 src/runtime 目录下进一步查看,这里就不一一介绍了。

入口方法

在 rt0_linux_amd64.s 文件中,可发现 _rt0_amd64_darwin JMP 跳转到了 _rt0_amd64 方法:

  1. TEXT _rt0_amd64_darwin(SB),NOSPLIT,$-8 
  2.  JMP _rt0_amd64(SB) 
  3. ... 

紧接着又跳转到 runtime·rt0_go 方法:

  1. TEXT _rt0_amd64(SB),NOSPLIT,$-8 
  2.  MOVQ 0(SP), DI // argc 
  3.  LEAQ 8(SP), SI // argv 
  4.  JMP runtime·rt0_go(SB) 

该方法将程序输入的 argc 和 argv 从内存移动到寄存器中。

栈指针(SP)的前两个值分别是 argc 和 argv,其对应参数的数量和具体各参数的值。

开启主线

程序参数准备就绪后,正式初始化的方法落在 runtime·rt0_go 方法中:

  1. TEXT runtime·rt0_go(SB),NOSPLIT,$0 
  2.  ... 
  3.  CALL runtime·check(SB) 
  4.  MOVL 16(SP), AX  // copy argc 
  5.  MOVL AX, 0(SP) 
  6.  MOVQ 24(SP), AX  // copy argv 
  7.  MOVQ AX, 8(SP) 
  8.  CALL runtime·args(SB) 
  9.  CALL runtime·osinit(SB) 
  10.  CALL runtime·schedinit(SB) 
  11.  
  12.  // create a new goroutine to start program 
  13.  MOVQ $runtime·mainPC(SB), AX  // entry 
  14.  PUSHQ AX 
  15.  PUSHQ $0   // arg size 
  16.  CALL runtime·newproc(SB) 
  17.  POPQ AX 
  18.  POPQ AX 
  19.  
  20.  // start this M 
  21.  CALL runtime·mstart(SB) 
  22.  ... 

初始化完毕后进行主协程(main goroutine)的运行,并放入等待队列(GMP 模型),最后调度器开始进行循环调度。

小结

根据上述源码剖析,可以得出如下 Go 应用程序引导的流程图:

Go 程序引导过程

在 Go 语言中,实际的运行入口并不是用户日常所写的 main func,更不是 runtime.main 方法,而是从 rt0_*_amd64.s 开始,最终再一路 JMP 到 runtime·rt0_go 里去,再在该方法里完成一系列 Go 自身所需要完成的绝大部分初始化动作。

其中整体包括:

后续将会继续剖析将进一步剖析 runtime·rt0_go 里的爱与恨,尤其像是 runtime.main、runtime.schedinit 等调度方法,都有非常大的学习价值,有兴趣的小伙伴可以持续关注。

Go 调度器初始化

知道了 Go 程序是怎么引导起来的之后,我们需要了解 Go Runtime 中调度器是怎么流转的。

runtime.mstart

这里主要关注 runtime.mstart 方法:

  1. func mstart() { 
  2.  // 获取 g0 
  3.  _g_ := getg() 
  4.  
  5.  // 确定栈边界 
  6.  osStack := _g_.stack.lo == 0 
  7.  if osStack { 
  8.   size := _g_.stack.hi 
  9.   if size == 0 { 
  10.    size = 8192 * sys.StackGuardMultiplier 
  11.   } 
  12.   _g_.stack.hi = uintptr(noescape(unsafe.Pointer(&size))) 
  13.   _g_.stack.lo = _g_.stack.hi - size + 1024 
  14.  } 
  15.  _g_.stackguard0 = _g_.stack.lo + _StackGuard 
  16.  _g_.stackguard1 = _g_.stackguard0 
  17.    
  18.   // 启动 m,进行调度器循环调度 
  19.  mstart1() 
  20.  
  21.  // 退出线程 
  22.  if mStackIsSystemAllocated() { 
  23.   osStack = true 
  24.  } 
  25.  mexit(osStack) 

runtime.mstart1

这么看来其实质逻辑在 mstart1 方法,我们继续往下剖析:

  1. func mstart1() { 
  2.  // 获取 g,并判断是否为 g0 
  3.  _g_ := getg() 
  4.  if _g_ != _g_.m.g0 { 
  5.   throw("bad runtime·mstart"
  6.  } 
  7.  
  8.  // 初始化 m 并记录调用方 pc、sp 
  9.  save(getcallerpc(), getcallersp()) 
  10.  asminit() 
  11.  minit() 
  12.  
  13.  // 设置信号 handler 
  14.  if _g_.m == &m0 { 
  15.   mstartm0() 
  16.  } 
  17.  // 运行启动函数 
  18.  if fn := _g_.m.mstartfn; fn != nil { 
  19.   fn() 
  20.  } 
  21.  
  22.  if _g_.m != &m0 { 
  23.   acquirep(_g_.m.nextp.ptr()) 
  24.   _g_.m.nextp = 0 
  25.  } 
  26.  schedule() 

忙活了一大圈,终于进入到开题的主菜了,原来潜伏的很深的 schedule 方法才是真正做调度的方法,其他都是前置处理和准备数据。

由于篇幅问题,schedule 方法会放到下篇再继续剖析,我们先聚焦本篇的一些细节点。

问题深剖

不过到这里篇幅也已经比较长了,积累了不少问题。我们针对在 Runtime 中出镜率最高的两个元素进行剖析:

  1. m0 是什么,作用是?
  2. g0 是什么,作用是?

m0

m0 是 Go Runtime 所创建的第一个系统线程,一个 Go 进程只有一个 m0,也叫主线程。

从多个方面来看:

g0

执行调度任务的叫 g0。。

g0 比较特殊,每一个 m 都只有一个 g0(仅此只有一个 g0),且每个 m 都只会绑定一个 g0。在 g0 的赋值上也是通过汇编赋值的,其余后续所创建的都是常规的 g。

从多个方面来看:

数据结构:g0 和其他创建的 g 在数据结构上是一样的,但是存在栈的差别。在 g0 上的栈分配的是系统栈,在 Linux 上栈大小默认固定 8MB,不能扩缩容。而常规的 g 起始只有 2KB,可扩容。

运行状态:g0 和常规的 g 不一样,没有那么多种运行状态,也不会被调度程序抢占,调度本身就是在 g0 上运行的。

变量声明:g0 和常规 g,g0 的定义就是 var g0 g,没什么特别之处。

小结

在本章节中我们讲解了 Go 调度器初始化的一个过程,分别涉及:

基于此也了解到了在调度器初始化过程中,需要准备什么,初始化什么。另外针对调度过程中最常提到的 m0、g0 的概念我们进行了梳理和说明。

总结

在今天这篇文章中,我们详细的介绍了 Go 语言的引导启动过程中的所有流程和初始化动作。

同时针对调度器的初始化进行了初步分析,详细介绍了 m0、g0 的用途和区别。在下一篇文章中我们将进一步对真正调度的 schedule 方法进行详解,这块也是个硬骨头了。

 

来源: 脑子进煎鱼了内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯