# 5. Go 程序是如何启动起来的?

运行 runtime/rt0_xxx.s 汇编文件

  1. 读取命令行参数:复制参数变量 argc 和 argv 到栈上;

    TEXT runtime·rt0_go<ABIInternal>(SB),NOSPLIT,$0
    	// copy arguments forward on an even stack
    	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)
    
  2. 初始化 g0 执行栈:g0 是为了调度协程而产生的协程,是每个 Go 程序的第一个协程;

    TEXT runtime·rt0_go<ABIInternal>(SB),NOSPLIT,$0
       ...
       // create istack out of the given (operating system) stack.
       // _cgo_init may update 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)
    
  3. 运行时检查:

    • 类型长度
    • 指针操作
    • 结构体字段偏移量
    • atomic 原子操作
    • CAS 操作
    • 栈大小是否是 2 的幂次
    TEXT runtime·rt0_go<ABIInternal>(SB),NOSPLIT,$0
    	...
      CALL	runtime·check(SB)
    
    // runtime/runtime1.go
    func check() {
       var (
          a     int8
          ...
          k     unsafe.Pointer
          l     *uint16
          m     [4]byte
       )
       ...
       if unsafe.Sizeof(a) != 1 {
          throw("bad a")
       }
    		...
    }
    
  4. 判断系统字长 & CPU 核数:runtime.osinit

    TEXT runtime·rt0_go<ABIInternal>(SB),NOSPLIT,$0
    	...
      CALL	runtime·osinit(SB)
    
    // runtime/os_linux.go
    func osinit() {
    	ncpu = getproccount()
    	physHugePageSize = getHugePageSize()
    	if iscgo {
    		sigdelset(&sigsetAllExiting, 32)
    		sigdelset(&sigsetAllExiting, 33)
    		sigdelset(&sigsetAllExiting, 34)
    	}
    	osArchInit()
    }
    
  5. 参数初始化 runtime.args:对命令行中的参数进行处理,参数数量复制给 argc(int32),参数值赋值给 argv(**byte)

    TEXT runtime·rt0_go<ABIInternal>(SB),NOSPLIT,$0
    	...
      MOVL	16(SP), AX		// copy argc
    	MOVL	AX, 0(SP)
    	MOVQ	24(SP), AX		// copy argv
    	MOVQ	AX, 8(SP)
    	CALL	runtime·args(SB)
    
  6. 初始化调度器 runtime.schedinit:

    • 全局栈空间内存分配
    • 堆内存空间的初始化
    • 初始化当前系统线程
    • 算法初始化(map、hash)
    • 加载命令行参数到 os.Args
    • 加载操作系统环境变量
    • gc 的参数初始化
    • 设置 process 的数量
    TEXT runtime·rt0_go<ABIInternal>(SB),NOSPLIT,$0
    	...
      CALL	runtime·schedinit(SB)
    
    // runtime/proc.go
    func schedinit() {
      ...
    	lockInit(&sched.lock, lockRankSched)
    	...
    	// The world starts stopped.
    	...
    	goargs()
    	goenvs()
    	parsedebugvars()
    	gcinit()
      ...
    }
    
  7. 创建主协程:执行 runtime.main 并将其放入调度器等待调度;

    TEXT runtime·rt0_go<ABIInternal>(SB),NOSPLIT,$0
    	...
    	// create a new goroutine to start program
    	MOVQ	$runtime·mainPC(SB), AX		// 去 runtime.main
    	PUSHQ	AX
    	PUSHQ	$0			// arg size
    	CALL	runtime·newproc(SB)				// 创建一个协程
    	POPQ	AX
    	POPQ	AX
      // start this M
    	CALL	runtime·mstart(SB)				// 启动调度器
      ...
    DATA	runtime·mainPC+0(SB)/8,$runtime·main<ABIInternal>(SB)
    
  8. 主协程执行主函数 runtime.main,它会执行:

    • runtime.init
    • 启动 gc 垃圾回收器
    • 用户包依赖的 init 方法
    • 用户主函数 main.main
    // runtime/proc.go
    
    // init 方法会率先执行
    func init() {
    	go forcegchelper()
    }
    
    func main() {
      // 1. 执行 runtime.init
    	doInit(&runtime_inittask) 
    	// 2. 启动 gc
    	gcenable()
      ...
      // 3. 执行用户包依赖的 init 方法
      doInit(&main_inittask)
    	...
      // 4.执行用户主函数 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()
    	...
    }
    

image-20220830223037462

上次更新: 8/30/2022, 10:33:29 PM