os:osx 10.14.5

go version: 1.12.5 darwin/amd64

不加特说说明的文件都是在 runtime 包下

程序入口

整个程序入口是在 rt0_darwin_amd64.s的第8行。此处只有一行汇编JMP _rt0_amd64(SB),跳转到asm_amd64.s中的第14行,配置好 argc argv后又跳转到 rt0_go(87行)。正式开始初始化go程序的运行时环境。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
TEXT runtime·rt0_go(SB),NOSPLIT,$0
// 将参数向前复制到一个偶数栈上
MOVQ DI, AX // argc
MOVQ SI, BX // argv
SUBQ $(4*8+7), SP // 2args 2auto
ANDQ $~15, SP
MOVQ AX, 16(SP)
MOVQ BX, 24(SP)

// 从给定(操作系统)栈中创建 istack。
// _cgo_init 可能更新 stackguard
MOVQ $runtime·g0(SB), DI
LEAQ (-64*1024+104)(SP), BX
MOVQ BX, g_stackguard0(DI)
MOVQ BX, g_stackguard1(DI)
MOVQ BX, (g_stack+stack_lo)(DI)
MOVQ SP, (g_stack+stack_hi)(DI)

// 寻找正在运行的处理器信息
MOVL $0, AX
CPUID
MOVL AX, SI

// CPU 相关的一些检测
(...)

#ifdef GOOS_darwin
// 跳过 TLS 设置 on Darwin
JMP ok
#endif

LEAQ runtime·m0+m_tls(SB), DI
CALL runtime·settls(SB)

// 使用它进行存储,确保能正常运行
get_tls(BX)
MOVQ $0x123, g(BX)
MOVQ runtime·m0+m_tls(SB), AX
CMPQ AX, $0x123
JEQ 2(PC)
CALL runtime·abort(SB)
ok:
// 程序刚刚启动,此时位于主 OS 线程
// 设置 per-goroutine 和 per-mach 寄存器
get_tls(BX)
LEAQ runtime·g0(SB), CX
MOVQ CX, g(BX)
LEAQ runtime·m0(SB), AX

// 保存 m->g0 = g0
MOVQ CX, m_g0(AX)
// 保存 m0 to g0->m
MOVQ AX, g_m(CX)

CLD // 约定 D 总是被清除
CALL runtime·check(SB)

MOVL 16(SP), AX // 复制 argc
MOVL AX, 0(SP)
MOVQ 24(SP), AX // 复制 argv
MOVQ AX, 8(SP)
CALL runtime·args(SB)
CALL runtime·osinit(SB)
CALL runtime·schedinit(SB)

// 创建一个新的 goroutine 来启动程序
MOVQ $runtime·mainPC(SB), AX // 入口
PUSHQ AX
PUSHQ $0 // 参数大小
CALL runtime·newproc(SB)
POPQ AX
POPQ AX

// 启动这个 M
CALL runtime·mstart(SB)

CALL runtime·abort(SB) // mstart 应该永不返回
RET

(...)
RET

DATA runtime·mainPC+0(SB)/8,$runtime·main(SB)
GLOBL runtime·mainPC(SB),RODATA,$8

引导

从上边汇编代码可以看出,整个过程按如下顺序执行:

  1. 初始化系统运行时信息
  2. 初始化调度时全局变量 m0 g0及关联关系
  3. runtime.check (实现在runtime1.go)对编译器工作进行校验,确保运行时类型正确
  4. runtime.args (runtime1.go).处理程序参数
  5. runtime.osinit (不同os在不同文件,os_darwin.go).获得CPU核心数
  6. runtime.schedinit (proc.go). 初始化调度器
  7. runtime.newproc (proc.go). 根据主goroutine入口地址创建G,并放至G队列中。
  8. runtime.mstart (proc.go). 开始调度循环

部分过程详细

runtime.schedinit 和 runtime.newproc

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// runtime/proc.go
func schedinit() {
\_g_ := getg()

(...)
// 最大系统线程数量(即 M),参考标准库 runtime/debug.SetMaxThreads
sched.maxmcount = 10000

(...)

// 模块数据验证
moduledataverify()

// 栈、内存分配器、调度器相关初始化。
// 栈初始化,复用管理链表
stackinit()
// 内存分配器初始化
mallocinit()
// 初始化当前 M
mcommoninit(_g_.m)

// cpu 相关的初始化
cpuinit() // 必须在 alginit 之前运行
alginit() // maps 不能在此调用之前使用,从 CPU 指令集初始化哈希算法

// 模块加载相关的初始化
modulesinit() // 模块链接,提供 activeModules
typelinksinit() // 使用 maps, activeModules
itabsinit() // 初始化 interface table,使用 activeModules

// 信号屏蔽字初始化
msigsave(_g_.m)
initSigmask = _g_.m.sigmask

// 处理y命令行用户参数和环境变量
goargs()
goenvs()

// 处理 GODEBUG、GOTRACEBACK 调试相关的环境变量设置
parsedebugvars()

// 垃圾回收器初始化
gcinit()

// 网络的上次轮询时间
sched.lastpoll = uint64(nanotime())

// 通过 CPU 核心数和 GOMAXPROCS 环境变量确定 P 的数量
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
procs = n
}

// 调整 P 的数量
// 这时所有 P 均为新建的 P,因此不能返回有本地任务的 P
if procresize(procs) != nil {
throw("unknown runnable goroutine during bootstrap")
}

// 不重要,调试相关
// For cgocheck > 1, we turn on the write barrier at all times
// and check all pointer writes. We can't do this until after
// procresize because the write barrier needs a P.
if debug.cgocheck > 1 {
writeBarrier.cgo = true
writeBarrier.enabled = true
for _, p := range allp {
p.wbBuf.reset()
}
}

if buildVersion == "" {
// 该条件永远不会被触发,此处只是为了防止 buildVersion 被编译器优化移除掉。
buildVersion = "unknown"
}
}

核心组件初始化

msigsave

stackinit

goroutine栈结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type g struct {
// stack 描述了实际的栈内存:[stack.lo, stack.hi)
stack stack
// stackguard0 是对比 Go 栈增长的 prologue 的栈指针
// 如果 sp 寄存器比 stackguard0 小(由于栈往低地址方向增长),会触发栈拷贝和调度
// 通常情况下:stackguard0 = stack.lo + StackGuard,但被抢占时会变为 StackPreempt
stackguard0 uintptr
// stackguard1 是对比 C 栈增长的 prologue 的栈指针
// 当位于 g0 和 gsignal 栈上时,值为 stack.lo + StackGuard
// 在其他栈上值为 ~0 用于触发 morestackc (并 crash) 调用
stackguard1 uintptr
(...)
// sched 描述了执行现场
sched gobuf
(...)
}

G的创建:(proc.go newproc())

  1. 首先检查go函数及其参数的合法性
  2. 尝试从本地P的自由G列表和调度器的自由G列表获取可用G(line3271:gfget(_p_)),如果未获取到,则新建G(line3273:malg(_StackMin))
  3. 初始化G,包括关联go函数及设置该G的状态和ID等
  4. 尝试将G放入本地P的runnext字段(line3348:runqput()):如果runnext为空,则直接放到runnext并返回;如果不为空,则替换,并将原runnext值放到本地P.runq末尾,如果满了将后一半的G移动至全局G队列

mallocinit

内存分配器的初始化除去一些例行检查外,就是对堆的初始化了

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
func mallocinit() {
// 一些涉及内存分配器的常量的检查,包括
// heapArenaBitmapBytes, physPageSize 等等
...

// 初始化堆
mheap_.init()
_g_ := getg()
_g_.m.mcache = allocmcache()

// 创建初始的 arena 增长 hint
if sys.PtrSize == 8 && GOARCH != "wasm" {
for i := 0x7f; i >= 0; i-- {
var p uintptr
switch {
case GOARCH == "arm64" && GOOS == "darwin":
p = uintptr(i)<<40 | uintptrMask&(0x0013<<28)
(...)
default:
p = uintptr(i)<<40 | uintptrMask&(0x00c0<<32)
}
hint := (*arenaHint)(mheap_.arenaHintAlloc.alloc())
hint.addr = p
hint.next, mheap_.arenaHints = mheap_.arenaHints, hint
}
} else {
// 32 位机器,不关心
(...)
}
}

堆的初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 堆初始化
func (h *mheap) init() {
// 初始化堆中各个组件的分配器
h.treapalloc.init(unsafe.Sizeof(treapNode{}), nil, nil, &memstats.other_sys)
h.spanalloc.init(unsafe.Sizeof(mspan{}), recordspan, unsafe.Pointer(h), &memstats.mspan_sys)
h.cachealloc.init(unsafe.Sizeof(mcache{}), nil, nil, &memstats.mcache_sys)
h.specialfinalizeralloc.init(unsafe.Sizeof(specialfinalizer{}), nil, nil, &memstats.other_sys)
h.specialprofilealloc.init(unsafe.Sizeof(specialprofile{}), nil, nil, &memstats.other_sys)
h.arenaHintAlloc.init(unsafe.Sizeof(arenaHint{}), nil, nil, &memstats.other_sys)

// 不对 mspan 的分配清零,后台扫描可以通过分配它来并发的检查一个 span
// 因此 span 的 sweepgen 在释放和重新分配时候能存活,从而可以防止后台扫描
// 不正确的将其从 0 进行 CAS。
//
// 因为 mspan 不包含堆指针,因此它是安全的
h.spanalloc.zero = false

// h->mapcache 不需要初始化
for i := range h.central {
h.central[i].mcentral.init(spanClass(i))
}
}

mcommoninit

M初始化

M 创建时机:

  1. 程序运行之初的 M0,无需创建已经存在的系统线程,只需对其进行初始化即可。
    1.1 schedinit –> mcommoninit –> mpreinit –> msigsave –> initSigmask –> mstart
  2. 需要时创建的 M,某些特殊情况下一定会创建一个新的 M并进行初始化,而后创建系统线程:
    2.1 startm 时没有空闲 m
    2.2 startTemplateThread 时
    2.3 startTheWorldWithSema 时 p 如果没有 m
    2.4 main 时创建系统监控
    2.5 oneNewExtraM 时
    2.6 初始化过程: newm –> allocm –> mcommoninit –> mpreinit –> newm1 –> newosproc –> mstart

P初始化

G初始化

创建 G 的过程也是相对比较复杂的,我们来总结一下这个过程:
首先尝试从 P 本地 gfree 链表或全局 gfree 队列获取已经执行过的、已经执行过的 g 初始化过程中程序无论是本地队列还是全局队列都不可能获取到 g,因此创建一个新的 g,并为其分配运行线程(执行栈)。
这时 g 处于 _Gidle 状态创建完成后,g 被更改为 _Gdead 状态,并根据要执行函数的入口地址和参数,初始化执行栈的 SP 和参数的入栈位置,并将需要的参数拷贝一份存入执行栈中根据 SP、参数,在 g.sched 中保存 SP 和 PC 指针来初始化 g 的运行现场将调用方、要执行的函数的入口 PC 进行保存,并将 g 的状态更改为 _Grunnable给 goroutine 分配 id,并将其放入 P 本地队列的队头或全局队列(初始化阶段队列肯定不是满的,因此不可能放入全局队列)检查空闲的 P,将其唤醒,准备执行 G,但我们目前处于初始化阶段,主 goroutine 尚未开始执行,因此这里不会唤醒 P。

gcinit

procresize

  1. 调用时已经 STW;
  2. 记录调整 P 的时间;
  3. 按需调整 allp 的大小;
  4. 按需初始化 allp 中的 P;
  5. 从 allp 移除不需要的 P,将释放的 P 队列中的任务扔进全局队列;
  6. 如果当前的 P 还可以继续使用(没有被移除),则将 P 设置为 _Prunning;
  7. 否则将第一个 P 抢过来给当前 G 的 M 进行绑定
  8. 最后挨个检查 P,将没有任务的 P 放入 idle 队列
  9. 出去当前 P 之外,将有任务的 P 彼此串联成链表,将没有任务的 P 放回到 idle 链表中

runtime.mstart