好了,我们继续 Go 调度器旅程。

今天,我们讲讲 G 结构。

g 结构的源码

 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
// src/runtime/runtime2.go
type g struct {
	// 当前 goroutine 的栈内存范围
	stack       stack   // offset known to runtime/cgo
	stackguard0 uintptr // offset known to liblink
	stackguard1 uintptr // offset known to liblink

	_panic    *_panic // g 中 panic 相关的处理
	_defer    *_defer // g 中 defer 相关的处理
	m         *m      // 当前 g 绑定的 m
	sched     gobuf   // 存储当前 g 调度相关的数据,上下文切换时会把当前信息保存到这里
  // g 发生系统调用相关的参数
	syscallsp uintptr // if status==Gsyscall, syscallsp = sched.sp to use during gc
	syscallpc uintptr // if status==Gsyscall, syscallpc = sched.pc to use during gc
	stktopsp  uintptr // expected sp at top of stack, to check in traceback
	// param is a generic pointer parameter field used to pass
	// values in particular contexts where other storage for the
	// parameter would be difficult to find. It is currently used
	// in three ways:
	// 1. When a channel operation wakes up a blocked goroutine, it sets param to
	//    point to the sudog of the completed blocking operation.
	// 2. By gcAssistAlloc1 to signal back to its caller that the goroutine completed
	//    the GC cycle. It is unsafe to do so in any other way, because the goroutine's
	//    stack may have moved in the meantime.
	// 3. By debugCallWrap to pass parameters to a new goroutine because allocating a
	//    closure in the runtime is forbidden.
	param        unsafe.Pointer
	atomicstatus atomic.Uint32 // g 的状态
	stackLock    uint32 // sigprof/scang lock; TODO: fold in to atomicstatus
	goid         uint64    // g 的 id
  ......
}

我们主要注释和解释了比较重要的几个字段,其他字段在需要时,我们再详细分析。

先理解下,为什么需要stack这个字段,可以参考这篇文章

G 的创建

G 的创建一般有两种。第一种就是 Go 程序启动时,主线程会创建第一个 goroutune 来执行 main 函数。其次是,我们在代码中使用 go 关键字创建新的goroutine

goroutine 的创建都是通过 runtime 中 newproc 函数。每个 goroutine 创建的时候仅会分配 2K 大小的栈内存,且 runtime 中没有对 goroutine 数量的限制,理论上可以很多。

但是 goroutine 的数量受系数资源的限制(cpu,内存,文件描述符等)。比如创建网络连接或者打开文件描述符等操作,goroutine 过多可能会出现 too many files openResource temporarily unavailable 等报错导致程序执行异常。

新创建的 G 会通过runqput函数放到当前 G 所在 P 的 队列中,其遵循以下原则:

1
2
1. 优先放在 p.runnext 上,如果 p.runnext 不为空,用新 G 替换原来的 G;
2. 原来的 G 放到 p.runq 上,如果 p.runq 队列满了,则将 p.runq 的一半(从头部开始取)移到全局队列

下面就是newproc函数的源码分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// src/runtime/proc.go#L4238
// Create a new g running fn.
// 创建一个新的 g,运行 fn 函数
// 将其放在 g 队列,等待运行
// 编译器会将 go 关键字的语句转化成此函数
func newproc(fn *funcval) {
	gp := getg()
	pc := getcallerpc()
	systemstack(func() {
		newg := newproc1(fn, gp, pc)

		pp := getg().m.p.ptr()
		runqput(pp, newg, true)  // next == true, 保证新 g 优先放在 p.runnext 上
		// main goroutine 创建的时候,立即唤醒p
		if mainStarted {
			wakep()
		}
	})
}

继续追踪,在newproc1函数中可以看到以下代码:

1
2
3
4
5
6
	newg := gfget(pp)
	if newg == nil {
		newg = malg(_StackMin)  // _StackMin
		casgstatus(newg, _Gidle, _Gdead)
		allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
	}

这是说,当创建新的 g 时,会先从空闲的 g 列表中取,如果取不到,直接创建,并为其分配 2KB 大小的栈。

接着,并将新创建的 g 状态修改为_Gdead,因为 g 还没有被初始化。

最后,将其加入全局变量 allg中。

G 的销毁

G 在退出时会执行goexit函数,状态从_Grunning变为_Gdead,G 对象并不会直接释放,而是通过gfput函数放入关联 P 的本地空闲列表,或者全局空闲列表,以便复用。

其原则是,放入P的本地空闲列表 p.gFree 中,如果p.gFree超过 64 个,仅会在本地保存 32 个,把超过的部分放入全局空闲G列表sched.gFree上。

具体见gfput源码分析:

 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
// src/runtime/proc.go#L4384
// Put on gfree list.
// If local list is too long, transfer a batch to the global list.
func gfput(pp *p, gp *g) {
  // G 状态检查
	if readgstatus(gp) != _Gdead {
		throw("gfput: bad status (not Gdead)")
	}
	// G 的栈内存检查,如果不是标准栈大小,释放栈空间
	stksize := gp.stack.hi - gp.stack.lo

	if stksize != uintptr(startingStackSize) {
		// non-standard stack size - free it.
		stackfree(gp.stack)
		gp.stack.lo = 0
		gp.stack.hi = 0
		gp.stackguard0 = 0
	}
	// 放入 p.gFree 列表, 如果列表中g个数超过64,仅会在P本地列表保存32个,超过的部分放到全局空闲列表
	pp.gFree.push(gp)
	pp.gFree.n++
	if pp.gFree.n >= 64 {
		var (
			inc      int32
			stackQ   gQueue
			noStackQ gQueue
		)
		for pp.gFree.n >= 32 {
			gp := pp.gFree.pop()
			pp.gFree.n--
			if gp.stack.lo == 0 {
				noStackQ.push(gp)
			} else {
				stackQ.push(gp)
			}
			inc++
		}
		lock(&sched.gFree.lock)
		sched.gFree.noStack.pushAll(noStackQ)
		sched.gFree.stack.pushAll(stackQ)
		sched.gFree.n += inc
		unlock(&sched.gFree.lock)
	}
}

G 的运行

G 的执行需要 M,而 M 的运行需要绑定 P,所以理论上同一时间处于运行状态的 G 数量等于 P 的数量。

G 的状态保存在其gobuf字段上,所以 G 可以跨 M 进行调度。

M 找到可运行的 G 后,会通过汇编函数gogog0栈切换到用户 G 的栈运行。

G 的状态

G 的状态在src/runtime/runtime2.go中进行了定义,主要分三类,一类是 G 正常运行的几个状态,一类是与GC 相关的状态,还有一些未使用的状态。

其状态描述如下:

G状态 状态描述
_Gidle 刚刚被创建申请,还没有被初始化
_Grunnable 已经存储在可运行队列中,还没有执行代码,也没有栈的所有权
_Grunning 可以执行代码,拥有栈的所有权,已经绑定了 M 和 P,所以肯定不在可运行队列中
_Gsyscall 正在执行系统调用,拥有栈的所有权。【所以,肯定不在可运行队列,绑定了M,M未必绑定P,因为一旦系统调用,M 可以移交P】
_Gwaiting 因为runtime而阻塞,没有执行代码,也不在可运行队列【比如在channel的等待队列】
_Gdead 当前未使用的G,也许已经退出,也许正在初始化
_Gcopystack G的栈正在被拷贝【没有执行代码,也不在可运行队列。但是拥有栈的所有权,比如占空间不够用,需要扩容】
_Gpreempted 由于抢占而阻塞,等待唤醒
_Gscan GC 正在扫描栈空间

下面是【G的状态转移图,还在整理确认中】。

g0的作用

还记得在将 M 的文章,我们有提过,每个 M 都有一个 g0。这个 g0 负责记录工作线程 M 所使用的栈信息,用于调度,在用户的 g 和 g0之间进行切换。

1
2
3
4
type m struct {
	g0      *g     // g0主要用来记录工作线程使用的栈信息,在执行调度代码时使用g0
  ...
}

但实际上,整个 runtime 中有两种 g0。一种是上面说的每个 M 上的 g0,还有一种是 m0 的 g0。m0, g0 都是以全局变量的方式存在。

1
2
3
4
5
6
// src/runtime/proc.go#L113
var (
	m0           m
	g0           g
  ......
)

newproc1分析

在 newproc1 函数中,我们可以看到如下代码:

1
2
3
4
5
6
	newg := gfget(pp)
	if newg == nil {
		newg = malg(_StackMin)  // _StackMin
		casgstatus(newg, _Gidle, _Gdead)
		allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
	}

这是说,当创建新的 g 时,会先从空闲的 g 列表中取,如果取不到,直接创建,并为其分配 2KB 大小的栈。

接着,并将新创建的 g 状态修改为_Gdead,因为 g 还没有被初始化。

最后,将其加入全局变量 allg中。