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<ABIInternal>(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  杨阳的技术博客  阅读(75)  评论(0编辑  收藏  举报