# 2.4 主 Goroutine 的生与死
上一节中我们已经知道`schedinit`完成初始化工作后并不会立即执行`runtime.main`(即主 Goroutine 运行的地方)。相反,会在后续的`mstart`调用中被调度器调度执行。 这个过程中,只会将`runtime.main`的入口地址压栈,进而将其传递给`newproc`进行使用, 而后`newproc`完成 G 的创建保存到 G 的运行现场中,因此真正执行会等到`mstart`后才会被调度执行。 我们在调度器一章中详细讨论调度器的调度过程,现在我们先将目光聚焦在`runtime.main`已经开始执行时的情况。
## 2.4.1 主 Goroutine 的一生
运行时包的 main 函数`runtime.main`承载了用户代码的 main 函数`main.main`, 并在同一个 Goroutine 上执行:
```
// 主 Goroutine
func main() {
...
// 执行栈最大限制:1GB(64位系统)或者 250MB(32位系统)
if sys.PtrSize == 8 {
maxstacksize = 1000000000
} else {
maxstacksize = 250000000
}
...
// 启动系统后台监控(定期垃圾回收、抢占调度等等)
systemstack(func() {
newm(sysmon, nil)
})
...
// 执行 runtime.init。运行时包中有多个 init 函数,编译器会将他们链接起来。
runtime_init()
...
// 启动垃圾回收器后台操作
gcenable()
...
// 执行用户 main 包中的 init 函数,因为链接器设定运行时不知道 main 包的地址,处理为非间接调用
fn := main_init
fn()
...
// 执行用户 main 包中的 main 函数,同理
fn = main_main
fn()
...
// 退出
exit(0)
}
```
整个执行过程有这样几个关键步骤:
1. `systemstack`会运行`newm(sysmon, nil)`启动后台监控
2. `runtime_init`负责执行运行时的多个初始化函数`runtime.init`
3. `gcenable`启用垃圾回收器
4. `main_init`开始执行用户态`main.init`函数,这意味着所有的`main.init`均在同一个主 Goroutine 中执行
5. `main_main`开始执行用户态`main.main`函数,这意味着`main.main`和`main.init`均在同一个 Goroutine 中执行。
## 2.4.2`pkg.init`的执行顺序
运行时的`runtime_init`则由编译器将多个`runtime.init`进行链接,我们可以从 函数的声明中看到:
```
//go:linkname runtime_init runtime.init
func runtime_init()
```
运行时存在多个 init 函数,其中较为重要的几个函数包括:
1. 垃圾回收器所需的参数检查并创建强制启动 GC 的监控 Goroutine
```
const (
_WorkbufSize = 2048
workbufAlloc = 32 << 10
)
func init() {
if workbufAlloc%pageSize != 0 || workbufAlloc%_WorkbufSize != 0 {
throw("bad workbufAlloc")
}
}
func init() {
go forcegchelper()
}
```
2. 确定`defer`的运行时类型:
```
var deferType *_type // _defer 结构的类型
func init() {
var x interface{}
x = (*_defer)(nil)
deferType = (*(**ptrtype)(unsafe.Pointer(&x))).elem
}
```
从这两个`init`函数可以看出,在用户代码正式启动之前,运行时还额外准备了强制 GC 的 监控并确定了 defer 的类型。 本节中我们不对这些方法做详细分析,等到他们各自的章节中再做详谈。那么我们仍然还会有这样 的疑问:包含多个`init`的执行顺序怎样由编译器控制的? 我们可以验证下面这两个不同的程序:
```
// main1.go
package main
import (
"fmt"
_ "net/http"
)
func main() {
fmt.Printf("hello, %s", "world!")
}
```
```
// main2.go
package main
import (
_ "net/http"
"fmt"
)
func main() {
fmt.Printf("hello, %s", "world!")
}
```
他们的唯一区别就是导入包的顺序不同,通过`go tool objdump -s "main.init"`可以获得`init`函数的实际汇编代码:
~~~asm
TEXT main.init.0(SB)
main1.go:8 0x11f0f40 65488b0c2530000000 MOVQ GS:0x30, CX
...
main1.go:9 0x11f0f76 e8a5b8e3ff CALL runtime.printstring(SB)
...
TEXT main.init(SB) <autogenerated>
...
<autogenerated>:1 0x11f10a8 e8e3b0ebff CALL fmt.init(SB)
<autogenerated>:1 0x11f10ad e88e5affff CALL net/http.init(SB)
<autogenerated>:1 0x11f10b2 e889feffff CALL main.init.0(SB)
...
~~~
~~~asm
TEXT main.init.0(SB)
...
main2.go:10 0x11f0f76 e8a5b8e3ff CALL runtime.printstring(SB)
...
TEXT main.init(SB) <autogenerated>
<autogenerated>:1 0x11f1060 65488b0c2530000000 MOVQ GS:0x30, CX
...
<autogenerated>:1 0x11f10a8 e8935affff CALL net/http.init(SB)
<autogenerated>:1 0x11f10ad e81e40ecff CALL fmt.init(SB)
<autogenerated>:1 0x11f10b2 e889feffff CALL main.init.0(SB)
...
~~~
从实际的汇编代码可以看到,init 的顺序由实际包调用顺序给出,所有引入的外部包的 init 均会被 编译器安插在当前包的`main.init.0`之前执行,而外部包的顺序与引入包的顺序有关。
那么某个包内的多个 init 函数是否有顺序可言?我们简单看一看编译器关于 init 函数的实现:
```
// cmd/compile/internal/gc/init.go
// 将 init 的名字 pkg.init 重命名为 pkg.init.0
var renameinitgen int
func renameinit() *types.Sym {
s := lookupN("init.", renameinitgen)
renameinitgen++
return s
}
```
`renameinit`这个函数中实现了对 init 函数的重命名,并通过`renameinitgen`在全局记录了 init 的索引后缀。`renameinit`会在处理函数声明时被调用:
```
// cmd/compile/internal/gc/noder.go
func (p *noder) funcDecl(fun *syntax.FuncDecl) *Node {
name := p.name(fun.Name)
t := p.signature(fun.Recv, fun.Type)
f := p.nod(fun, ODCLFUNC, nil, nil)
// 函数没有 reciver
if fun.Recv == nil {
// 且名字叫做 init
if name.Name == "init" {
name = renameinit() // 对其进行重命名
...
}
...
}
...
}
```
而`funcDecl`则会在 AST 的`noder`结构的方法`decls`中被调用:
```
func (p *noder) decls(decls []syntax.Decl) (l []*Node) {
var cs constState
for _, decl := range decls {
p.lineno(decl)
switch decl := decl.(type) {
case *syntax.FuncDecl:
l = append(l, p.funcDecl(decl))
...
}
}
return
}
```
一个包内的 init 函数的调用顺序取决于声明的顺序,即从上而下依次调用。
## 2.4.3 小结
看到这里我们已经结束了整个 Go 程序的执行,但仍有海量的细节还没有被敲定,完全还没有深入 运行时的三大核心组件,运行时各类机制也都还没有接触。总结一下这节讨论中遗留下来的问题:
1. `mstart`会如何将主 Goroutine 调度执行?
2. `sysmon`系统监控做了什么事情,它的工作原理是什么?
3. `runtime.init`的`forcegchelper`是什么?`gcenable`又做了什么?
我们在随后的章节中一一介绍。
## 进一步阅读的参考文献
1. [Command compile](https://golang.org/cmd/compile/)
2. [`main_init_done`can be implemented more efficiently](https://github.com/golang/go/issues/15943)
- 第一部分 :基础篇
- 第1章 Go语言的前世今生
- 1.2 Go语言综述
- 1.3 顺序进程通讯
- 1.4 Plan9汇编语言
- 第2章 程序生命周期
- 2.1 从go命令谈起
- 2.2 Go程序编译流程
- 2.3 Go 程序启动引导
- 2.4 主Goroutine的生与死
- 第3 章 语言核心
- 3.1 数组.切片与字符串
- 3.2 散列表
- 3.3 函数调用
- 3.4 延迟语句
- 3.5 恐慌与恢复内建函数
- 3.6 通信原语
- 3.7 接口
- 3.8 运行时类型系统
- 3.9 类型别名
- 3.10 进一步阅读的参考文献
- 第4章 错误
- 4.1 问题的演化
- 4.2 错误值检查
- 4.3 错误格式与上下文
- 4.4 错误语义
- 4.5 错误处理的未来
- 4.6 进一步阅读的参考文献
- 第5章 同步模式
- 5.1 共享内存式同步模式
- 5.2 互斥锁
- 5.3 原子操作
- 5.4 条件变量
- 5.5 同步组
- 5.6 缓存池
- 5.7 并发安全散列表
- 5.8 上下文
- 5.9 内存一致模型
- 5.10 进一步阅读的文献参考
- 第二部分 运行时篇
- 第6章 并发调度
- 6.1 随机调度的基本概念
- 6.2 工作窃取式调度
- 6.3 MPG模型与并发调度单
- 6.4 调度循环
- 6.5 线程管理
- 6.6 信号处理机制
- 6.7 执行栈管理
- 6.8 协作与抢占
- 6.9 系统监控
- 6.10 网络轮询器
- 6.11 计时器
- 6.12 非均匀访存下的调度模型
- 6.13 进一步阅读的参考文献
- 第7章 内存分配
- 7.1 设计原则
- 7.2 组件
- 7.3 初始化
- 7.4 大对象分配
- 7.5 小对象分配
- 7.6 微对象分配
- 7.7 页分配器
- 7.8 内存统计
- 第8章 垃圾回收
- 8.1 垃圾回收的基本想法
- 8.2 写屏幕技术
- 8.3 调步模型与强弱触发边界
- 8.4 扫描标记与标记辅助
- 8.5 免清扫式位图技术
- 8.6 前进保障与终止检测
- 8.7 安全点分析
- 8.8 分代假设与代际回收
- 8.9 请求假设与实务制导回收
- 8.10 终结器
- 8.11 过去,现在与未来
- 8.12 垃圾回收统一理论
- 8.13 进一步阅读的参考文献
- 第三部分 工具链篇
- 第9章 代码分析
- 9.1 死锁检测
- 9.2 竞争检测
- 9.3 性能追踪
- 9.4 代码测试
- 9.5 基准测试
- 9.6 运行时统计量
- 9.7 语言服务协议
- 第10章 依赖管理
- 10.1 依赖管理的难点
- 10.2 语义化版本管理
- 10.3 最小版本选择算法
- 10.4 Vgo 与dep之争
- 第12章 泛型
- 12.1 泛型设计的演进
- 12.2 基于合约的泛型
- 12.3 类型检查技术
- 12.4 泛型的未来
- 12.5 进一步阅读的的参考文献
- 第13章 编译技术
- 13.1 词法与文法
- 13.2 中间表示
- 13.3 优化器
- 13.4 指针检查器
- 13.5 逃逸分析
- 13.6 自举
- 13.7 链接器
- 13.8 汇编器
- 13.9 调用规约
- 13.10 cgo与系统调用
- 结束语: Go去向何方?