当前位置: 首页 > 文档资料 > 深入解析 Go >

4.1 系统初始化

优质
小牛编辑
127浏览
2023-12-01

整个程序启动是从_rt0_amd64_darwin开始的,然后JMP到main,接着到_rt0_amd64。前面只有一点点汇编代码,做的事情就是通过参数argc和argv等,确定栈的位置,得到寄存器。下面将从_rt0_amd64开始分析。

这里首先会设置好m->g0的栈,将当前的SP设置为stackbase,将SP往下大约64K的地方设置为stackguard。然后会获取处理器信息,放在全局变量runtime·cpuid_ecx和runtime·cpuid_edx中。接着,设置本地线程存储。本地线程存储是依赖于平台实现的,比如说这台机器上是调用操作系统函数thread_fast_set_cthread_self。设置本地线程存储之后还会立即测试一下,写入一个值再读出来看是否正常。

本地线程存储

这里解释一下本地线程存储。比如说每个goroutine都有自己的控制信息,这些信息是存放在一个结构体G中。假设我们有一个全局变量g是结构体G的指针,我们希望只有唯一的全局变量g,而不是g0,g1,g2...但是我们又希望不同goroutine去访问这个全局变量g得到的并不是同一个东西,它们得到的是相对自己线程的结构体G,这种情况下就需要本地线程存储。g确实是一个全局变量,却在不同线程有多份不同的副本。每个goroutine去访问g时,都是对应到自己线程的这一份副本。

设置好本地线程存储之后,就可以为每个goroutine和machine设置寄存器了。这样设置好了之后,每次调用get_tls(r),就会将当前的goroutine的g的地址放到寄存器r中。你可以在源代码中看到一些类似这样的汇编:

	get_tls(CX)
	MOVQ	g(CX), AX //get_tls(CX)之后,g(CX)得到的就是当前的goroutine的g

不同的goroutine调用get_tls,得到的g是本地的结构体G的,结构体中记录goroutine的相关信息。

初始化顺序

接下来的事情就非常直白,可以直接上代码:

	CLD				// convention is D is always left cleared
	CALL	runtime·check(SB) //检测像int8,int16,float等是否是预期的大小,检测cas操作是否正常
	MOVL	16(SP), AX		// copy argc
	MOVL	AX, 0(SP)
	MOVQ	24(SP), AX		// copy argv
	MOVQ	AX, 8(SP)
	CALL	runtime·args(SB)	//将argc,argv设置到static全局变量中了
	CALL	runtime·osinit(SB)	//osinit做的事情就是设置runtime.ncpu,不同平台实现方式不一样
	CALL	runtime·hashinit(SB)	//使用读/dev/urandom的方式从内核获得随机数种子
	CALL	runtime·schedinit(SB)	//内存管理初始化,根据GOMAXPROCS设置使用的procs等等

proc.c中有一段注释,也说明了bootstrap的顺序:

// The bootstrap sequence is:
//
// call osinit
// call schedinit
// make & queue new G
// call runtime·mstart
//
// The new G calls runtime·main.

先调用osinit,再调用schedinit,创建就绪队列并新建一个G,接着就是mstart。这几个函数都不太复杂。

调度器初始化

让我们看一下runtime.schedinit函数。该函数其实是包装了一下其它模块的初始化函数。有调用mallocinit,mcommoninit分别对内存管理模块初始化,对当前的结构体M初始化。

接着调用runtime.goargs和runtime.goenvs,将程序的main函数参数argc和argv等复制到了os.Args中。

也是在这个函数中,根据环境变量GOMAXPROCS决定可用物理线程数目的:

	procs = 1;
	p = runtime·getenv("GOMAXPROCS");
	if(p != nil && (n = runtime·atoi(p)) > 0) {
		if(n > MaxGomaxprocs)
			n = MaxGomaxprocs;
		procs = n;
	}

回到前面的汇编代码继续看:

	// 新建一个G,当它运行时会调用main.main
	PUSHQ	$runtime·main·f(SB)		// entry
	PUSHQ	$0			// arg size
	CALL	runtime·newproc(SB)
	POPQ	AX
	POPQ	AX

	// start this M
	CALL	runtime·mstart(SB)

还记得前面章节讲的go关键字的调用协议么?先将参数进栈,再被调函数指针和参数字节数进栈,接着调用runtime.newproc函数。所以这里其实就是新开个goroutine执行runtime.main。

runtime.newproc会把runtime.main放到就绪线程队列里面。本线程继续执行runtime.mstart,m意思是machine。runtime.mstart会调用到调度函数schedule

schedule函数绝不返回,它会根据当前线程队列中线程状态挑选一个来运行。由于当前只有这一个goroutine,它会被调度,然后就到了runtime.main函数中来,runtime.main会调用用户的main函数,即main.main从此进入用户代码。前面已经写过helloworld了,用gdb调试,一步一步的跟踪观察这个过程。

links

  • 目录
  • 上一节: Go语言程序初始化过程
  • 下一节: main.main之前的准备