好了,我们继续 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 open 或 Resource 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 后,会通过汇编函数gogo
从g0
栈切换到用户 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
中。