5.3 主 Goroutine 的生与死

5.3 主 Goroutine 的生与死

上一节中我们已经知道 schedinit 完成初始化工作后并不会立即执行 runtime.main (即主 Goroutine 运行的地方)。相反,会在后续的 mstart 调用中被调度器调度执行。 这个过程中,只会将 runtime.main 的入口地址压栈,进而将其传递给 newproc 进行使用, 而后 newproc 完成 G 的创建保存到 G 的运行现场中,因此真正执行会等到 mstart 后才会被调度执行。 我们在调度器一章中详细讨论调度器的调度过程,现在我们先将目光聚焦在 runtime.main 已经开始执行时的情况。

5.3.1 主 Goroutine 的一生

运行时包的 main 函数 runtime.main 承载了用户代码的 main 函数 main.main, 并在同一个 Goroutine 上执行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 主 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.mainmain.init 均在同一个 Goroutine 中执行。

5.3.2 pkg.init 的执行顺序

运行时的 runtime_init 则由编译器将多个 runtime.init 进行链接,我们可以从 函数的声明中看到:

1
2
//go:linkname runtime_init runtime.init
func runtime_init()

运行时存在多个 init 函数,其中较为重要的几个函数包括:

  1. 垃圾回收器所需的参数检查并创建强制启动 GC 的监控 Goroutine

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    const (
        _WorkbufSize = 2048
        workbufAlloc = 32 << 10
    )
    func init() {
        if workbufAlloc%pageSize != 0 || workbufAlloc%_WorkbufSize != 0 {
            throw("bad workbufAlloc")
        }
    }
    func init() {
        go forcegchelper()
    }
    

    ``

  2. 确定 defer 的运行时类型:

    1
    2
    3
    4
    5
    6
    
    var deferType *_type // _defer 结构的类型
    func init() {
        var x interface{}
        x = (*_defer)(nil)
        deferType = (*(**ptrtype)(unsafe.Pointer(&x))).elem
    }
    

    ``

从这两个 init 函数可以看出,在用户代码正式启动之前,运行时还额外准备了强制 GC 的 监控并确定了 defer 的类型。 本节中我们不对这些方法做详细分析,等到他们各自的章节中再做详谈。那么我们仍然还会有这样 的疑问:包含多个 init 的执行顺序怎样由编译器控制的? 我们可以验证下面这两个不同的程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// main1.go
package main

import (
	"fmt"
	_ "net/http"
)

func main() {
	fmt.Printf("hello, %s", "world!")
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// main2.go
package main

import (
	_ "net/http"
	"fmt"
)

func main() {
	fmt.Printf("hello, %s", "world!")
}

他们的唯一区别就是导入包的顺序不同,通过 go tool objdump -s "main.init" 可以获得 init 函数的实际汇编代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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)
  ...
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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 函数的实现:

1
2
3
4
5
6
7
8
// 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 会在处理函数声明时被调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 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 中被调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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 函数的调用顺序取决于声明的顺序,即从上而下依次调用。

5.3.3 小结

看到这里我们已经结束了整个 Go 程序的执行,但仍有海量的细节还没有被敲定,完全还没有深入 运行时的三大核心组件,运行时各类机制也都还没有接触。总结一下这节讨论中遗留下来的问题:

  1. mstart 会如何将主 Goroutine 调度执行?
  2. sysmon 系统监控做了什么事情,它的工作原理是什么?
  3. runtime.initforcegchelper 是什么?gcenable 又做了什么?

我们在随后的章节中一一介绍。

进一步阅读的参考文献

  1. Command compile
  2. The Go Memory Model
  3. main_init_done can be implemented more efficiently