go程序启动过程

go的启动入口函数

对go有开发经验的朋友都知道,main函数不是真正的启动入口,只是go暴露给用户编写的业务的接口。
这点上基本所有的语言都是类似,在main函数调用前,go需要做一系列的准备工作。

go的启动在 runtime/rto XXX.s, xxx是因为平台的差异。不同系统不同芯片都有自己的启动方法。

go runtime包中,给不同平台的定义的启动函数。.s 是汇编的文件与.asm一样。

以Linux为例,探究启动阶段都做了什么

include "textflag.h"

TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB)

TEXT _rt0_amd64_linux_lib(SB),NOSPLIT,$0
JMP _rt0_amd64_lib(SB)

// 上边入口函数 调用的函数定义
TEXT _rt0_amd64(SB),NOSPLIT,$-8
// 两个参数存入了寄存器
MOVQ 0(SP), DI // argc
LEAQ 8(SP), SI // argv
// 跳转到了新的方法
JMP runtime·rt0_go(SB)

关键代码:

TEXT runtime·rt0_go(SB),NOSPLIT|NOFRAME|TOPFRAME,$0
// Copy arguments forward on an even stack.
// Users of this function jump to it, they don't call it.
MOVL 0(SP), AX
MOVL 4(SP), BX
SUBL $128, SP // plenty of scratch
ANDL $~15, SP
MOVL AX, 120(SP) // save argc, argv away
MOVL BX, 124(SP)

// set default stack bounds.
// _cgo_init may update stackguard.
// 初始化 g0协程
MOVL $runtime·g0(SB), BP
LEAL (-64*1024+104)(SP), BX
MOVL BX, g_stackguard0(BP)
MOVL BX, g_stackguard1(BP)
MOVL BX, (g_stack+stack_lo)(BP)
MOVL SP, (g_stack+stack_hi)(BP)

// 中间删除了一些源码 太多判断

// save m->g0 = g0
MOVL DX, m_g0(AX)
// save g0->m = m0
MOVL AX, g_m(DX)

CALL runtime·emptyfunc(SB) // fault if stack check is wrong

// convention is D is always cleared
CLD

// 运行时检测
CALL runtime·check(SB)

// saved argc, argv
MOVL 120(SP), AX
MOVL AX, 0(SP)
MOVL 124(SP), AX
MOVL AX, 4(SP)
// 拷贝参数到 go程序中
CALL runtime·args(SB)
// 初始化系统相关参数,如cpu是几核
CALL runtime·osinit(SB)
// 初始化调度器
CALL runtime·schedinit(SB)

// 新建一个 goroutine,该 goroutine 绑定 runtime.main,放在 P 的本地队列,等待调度
// create a new goroutine to start program
PUSHL $runtime·mainPC(SB) // entry
// go 创建协程底层都是采用newproc 函数
CALL runtime·newproc(SB)
POPL AX

// 创建一个 M 这里开始真正调用 runtime·main
// start this M
CALL runtime·mstart(SB)

CALL runtime·abort(SB)
RET

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

到此,系统中有了两个协程goroutine ,一个g0和一个主协程。

往下走看 runtime.main

跳到 proc.go 目录
// 删除了一些代码
// The main goroutine.
func main() {
mp := getg().m

// Lock the main goroutine onto this, the main OS thread,
// during initialization. Most programs won't care, but a few
// do require certain calls to be made by the main thread.
// Those can arrange for main.main to run in the main thread
// by calling runtime.LockOSThread during initialization
// to preserve the lock.
lockOSThread()

// 一些初始化的工作
doInit(runtime_inittasks) // Must be before defer.

// 启动垃圾回收器
gcenable()
// 删除了一些代码,有判断是否为 cgo

/ / Run the initializing tasks. Depending on build mode this
// list can arrive a few different ways, but it will always
// contain the init tasks computed by the linker for all the
// packages in the program (excluding those added at runtime
// by package plugin).
// 用户依赖包的init方法,在这个时候执行了。
for _, m := range activeModules() {
doInit(m.inittasks)
}

// 链接到 main包的main函数,开始调用。
fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
fn()

if raceenabled {
runExitHooks(0) // run hooks now, since racefini does not return
racefini()
}

// Make racy client program work: if panicking on
// another goroutine at the same time as main returns,
// let the other goroutine finish printing the panic trace.
// Once it does, it will exit. See issues 3934 and 20018.
// 进行panic trace的配置
if runningPanicDefers.Load() != 0 {
// Running deferred functions should not take long.
for c := 0; c < 1000; c++ {
if runningPanicDefers.Load() == 0 {
break
}
Gosched()
}
}
if panicking.Load() != 0 {
gopark(nil, nil, waitReasonPanicWait, traceBlockForever, 1)
}
runExitHooks(0)
}

编译阶段回去link到main函数。
//go:linkname main_main main.main
func main_main()

总结:

前面为了不影响阅读,有些分支的代码,未贴出来,这里来统一总结下:

1. g0是每个go程序的第一个协程,目的是为了调度其他协程

2. 运行时的检测 主要检测内容

// 运行时检测
CALL runtime·check(SB)

•检查各种类型的长度
•检查结构体字段的偏移量
•检查 CAS 操作
•检查指针操作
•检查 atomic 原子操作
•检查栈大小是否是 2的幂次

3.调度器的初始化

// 初始化调度器
CALL runtime·schedinit(SB)

全局栈空间内存分配
堆内存空间的初始化
初始化当前系统线程
加载命令行参数到 os.Args
加载操作系统环境变量
垃圾回收器的参数初始化
算法初始化 (map、hash等)
设置 process 数量

4. 创建了一个主协程

用于执行 runtime.main

5. 初始化了一个 M,用来调度主协程

6.主协程在执行主函数包含了这几个工作

执行runtime 包中的init 方法
启动GC 垃圾收集器
执行用户包依赖的 init 方法
执行用户主函数 main.main0
配置了panic trace的收集

posted @ 2023-11-28 12:56  杨阳的技术博客  阅读(179)  评论(0)    收藏  举报