本文用于持续记录 Go 栈相关源码学习笔记。
Goroutine 执行栈结构
Goroutine 是一个 g
对象,g
对象的前三个字段描述了它的执行栈:
// stack 描述了 Goroutine 的执行栈,栈的区间为 [lo, hi),在栈两边没有任何隐式数据结构
// 因此 Go 的执行栈由运行时管理,本质上分配在堆中,比 ulimit -s 大
type stack struct {
lo uintptr
hi uintptr
}
// gobuf 描述了 Goroutine 的执行现场
type gobuf struct {
sp uintptr
pc uintptr
g guintptr
ctxt unsafe.Pointer
ret sys.Uintreg
lr uintptr
bp uintptr
}
type g struct {
// stack 字段描述了实际的栈内存:[stack.lo, stack.hi)
stack stack // offset known to runtime/cgo
// stackhuard0 是对比 Go 栈增长的 prologue 的栈指针
// 如果 sp 寄存器比 stackguard 小(由于栈忘低地址方向增长),会触发栈拷贝和调度
// 通常情况下:stackguard0 = stack.lo + StackGuard,但被抢占时会变成 StackPreempt
stackguard0 uintptr // offset known to liblink
// stackguard1 时对比 C 栈增长的 prologue 的栈指针
// 当位于 g0 的 gsignal 栈上时,值为 stack.lo + StackGuard
// 在其他栈上值为 ~0 用于触发 morestackc(并 crash)调用
stackguard1 uintptr // offset known to liblink
...
// sched 描述了执行现场
sched gobuf
}
Go 调用栈帧内存布局
栈帧布局
runtime/stack.go
中有 x86 架构下的栈帧布局示意图
// (x86)
// +------------------+
// | args from caller |
// +------------------+ <- frame->argp
// | return address |
// +------------------+
// | caller's BP (*) | (*) if framepointer_enabled && varp < sp
// +------------------+ <- frame->varp
// | locals |
// +------------------+
// | args to callee |
// +------------------+ <- frame->sp
在 x86架构下,golang栈帧布局从上(高地址)到下(低地址)依次为:这个函数帧的调用者传入的参数, 这个函数帧的返回地址,调用者调用时的BP快照(见上文FP
用法原理),该帧本地变量,该帧调用其它函数需要传递的参数。
完整的栈结构图
-----------------
current func arg0
----------------- <----------- FP(pseudo FP)
caller ret addr
+---------------+ <----------- 这里是 _g_.sched.hi 吗?
| caller BP(*) |
----------------- <----------- SP(pseudo SP,实际上是当前栈帧的 BP 位置)
| Local Var0 |
-----------------
| Local Var1 |
-----------------
| Local Var2 |
----------------- -
| ........ |
-----------------
| Local VarN |
-----------------
| |
| |
| temporarily |
| unused space |
| |
| |
-----------------
| call retn |
-----------------
| call ret(n-1)|
-----------------
| .......... |
-----------------
| call ret1 |
-----------------
| call argn |
-----------------
| ..... |
-----------------
| call arg3 |
-----------------
| call arg2 |
|---------------|
| call arg1 |
----------------- <------------ hardware SP 位置
| return addr |
+---------------+ <----------- 这里是 _g_.sched.lo 吗?
Go 栈管理机制
Go 使用连续栈机制进行管理栈空间。
在 Go1.3 以前使用分段栈:
在栈空间用完后,分配一块新的内存地址,在这个新栈中包含旧栈的地址。
问题:这种设计的缺陷很容易破坏缓存的局部性原理,从而降低程序的运行时性能。收缩栈的操作太过昂贵,在循环中重复分裂,收缩,释放的操作会付出很大的开销。这就是热点分裂问题(hot split problem)
从 Go1.4 之后的版本中,使用了连续栈机制,也叫栈拷贝。
栈拷贝的方式是创建一个新的栈,它的大小是旧栈的两倍,并把旧栈完全拷贝进去。收缩操作不做处理,再次增长时使用刚才的空间。
栈是如何拷贝的
由于栈中的变量在 Go 中能够获得其地址,因此最终会出现指向栈的指针,如果直接拷贝,任何指向旧栈的指针都会失效。
所以 Go 的内存安全机制规定,任何能够指向栈的指针都必须存在于栈中。
在编译器的逃逸分析中,所有有可能逃逸的变量,都会被分配在堆上。剩下栈中的指针,指向的都是栈里的数据。
Go 没有采用 x86-64 架构函数传参优化
在 x86-64 架构下,增加了许多通用寄存器,C 系语言为了优化,会将参数部分(最多6个)使用寄存器直接传递,但是在 Go 中,编译器强制规定函数的传参全部使用栈传递,不使用寄存器传参。
执行栈分配过程
从创建 goroutine 开始
// go 函数会被编译为 runtime.newproc 的调用
// 用 siz 字节的参数创建运行 fn 调用的 goroutine
// 这个调用的堆栈布局是特殊的,它假设传递给 fn 的参数是进阶在 &fn 之上的堆栈上的。
// 因此它们在逻辑上是 newproc 的参数框架的一部分
func newproc(siz int32, fn *funcval) {
// 从 fn 的地址增加一个指针的长度,从而获取第一参数地址
argp := add(unsafe.Pointer(&fn), sys.PtrSize)
// 获取 g 指针,编译器会编译为从 TLS 或其他专用寄存器中获取
// 获取到的是 caller g 吗?
gp := getg()
// 获取调用方 PC
pc := getcallerpc()
// 使用 g0 系统栈创建新的 goroutine
systemstack(func() {
// 创建 g 的函数,传入了 fn 函数的入口地址,argp 调用函数参数的起始位置,siz 参数长度,
// gp caller g,caller pc(创建 goroutine 语句的地址)
newg := newproc1(fn, argp, siz, gp, pc)
_p_ := getg().m.p.ptr()
runqput(_p_, newg, true)
if mainStarted {
wakep()
}
})
}
解析 newproc 调用前的过程
也就是 fn 在哪,fn 之上的参数是如何分配的。
有参数的情况
package main
func hello(msg string) {
println(msg)
}
func main() { // 7 行
go hello("hello world") // 8 行
}
"".main STEXT size=91 args=0x0 locals=0x28
......
0x000f 00015 (hello.go:7) SUBQ $40, SP // 栈扩大 40 字节
0x0013 00019 (hello.go:7) MOVQ BP, 32(SP) // caller BP,由 callee 存储
0x0018 00024 (hello.go:7) LEAQ 32(SP), BP // callee BP 的栈从 32(SP) 开始
......
0x001d 00029 (hello.go:8) MOVL $16, (SP) // 将 16 放到 SP 的位置,16 是第一个参数 siz,因为是 int32,所以是 MOVL。数字是 16 是因为有 string.data 和 string.len 两个参数加一起占 16 个字节
0x0024 00036 (hello.go:8) LEAQ "".hello·f(SB), AX // 将 hello 的调用地址传给 AX
0x002b 00043 (hello.go:8) MOVQ AX, 8(SP) // 将 hello 的调用地址放入 8(SP) 的位置
0x0030 00048 (hello.go:8) LEAQ go.string."hello world"(SB), AX // 将“hello world”的地址放入 AX
0x0037 00055 (hello.go:8) MOVQ AX, 16(SP) // 将字符串地址放在 16(SP) 的位置
0x003c 00060 (hello.go:8) MOVQ $11, 24(SP) // 将 $11 放在 24(SP) 的位置,11 是 string 的长度,string 是结构体,结构体在传参中会扁平化为多个参数
0x0045 00069 (hello.go:8) CALL runtime.newproc(SB) // call 指令 = push+jmp,所以会将 newproc 地址入栈
0x004a 00074 (hello.go:9) MOVQ 32(SP), BP // 复原 caller BP
0x004f 00079 (hello.go:9) ADDQ $40, SP // 缩小栈
特殊的栈布局
栈布局
40(SP)+-----------------+ 高地址
| caller BP |
32(SP)+-----------------+ <-- main.BP
| 11 string.len |
24(SP)+-----------------+
| &"hello world" |
16(SP)+-----------------+ <-- fn + sys.PtrSize
| hello |
8(SP) +-----------------+ <-- fn
| siz |
(SP) +-----------------+ <-- SP
| newproc PC |
+-----------------+ callerpc: 要运行的 Goroutine 的 PC
| |
| | 低地址
newproc1 调用
func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) *g {
// 在系统栈中得到的是 g0
_g_ := getg()
......
siz := narg
siz = (siz + 7) &^ 7 // 内存对齐
// 参数大小不能超过 2048-4*8-8(64位),可以分配更大的栈,但没必要
if siz >= _StackMin-4*sys.RegSize-sys.RegSize {
throw("newproc: function arguments too large for new goroutine")
}
_p_ := _g_.m.p.ptr()
// 尝试复用运行结束的 G
newg := gfget(_p_)
if newg == nil {
// 分配一个新的 g 结构, 包含一个 stacksize 字节的的栈
// 总是 2KB?
newg = malg(_StackMin)
casgstatus(newg, _Gidle, _Gdead)
allgadd(newg) // 将 _Gdead 状态的 newg 添加到 allg,防止被 GC 扫描到
}
......
}
func malg(stacksize int32) *g {
newg := new(g)
if stacksize >= 0 {
// 有些系统需要额外的栈空间
// 将 stacksize 舍入为 2 的指数,目的是为了消除 _StackSystem 对栈的影响
stacksize = round2(_StackSystem + stacksize)
systemstack(func() {
newg.stack = stackalloc(uint32(stacksize))
})
// 设置堆栈保护位置
newg.stackguard0 = newg.stack.lo + _StackGuard
newg.stackguard1 = ^uintptr(0)
......
}
return newg
}
执行栈的分配
前置部分知识:[[Go 内存分配器]]
栈可能从两个不同的位置被分配:小栈和大栈。小栈指大小为 2K/4K/8K/16K 的栈,大栈则是更大的栈。 stackalloc
基本上也就是在权衡应该从哪里分配出一个执行栈,返回所在栈的低位和高位。
func stackalloc(n uint32) stack {
// g0
thisg := getg()
......
// 小栈由固定大小的空闲链表分配器进行分配
// 大栈由专用的 span 分配
var v unsafe.Pointer
if n < _FixedStack<<_NumStackOrders && n < _StackCacheSize {
// 小栈分配
} else {
// 大栈分配
}
......
return stack{uintptr(v), uintptr(v) + uintptr(n)}
}
小栈
对于较小的栈可以从 stackpool
或者 stackcache
中分配,这取决于当产生栈分配时,goroutine 是否正处于 exitsyscall
或 procresize
,或是正在发生抢占 thisg.m.preemptoff != ""
。
order := uint8(0)
n2 := n
for n2 > _FixedStack {
order++
n2 >>= 1
}
var x gclinkptr
// 检查是否需要从全局池(stackpool)中分配栈
if stackNoCache != 0 || thisg.m.p == 0 || thisg.m.preemptoff != "" {
lock(&stackpool[order].item.mu)
x = stackpoolalloc(order)
unlock(&stackpool[order].item.mu)
} else {
// 如果不需要就从 mcache.stackcache 中分配
c := thisg.m.p.ptr().mcache
x = c.stackcache[order].list
if x.ptr() == nil { // 提取失败,扩容再重试
stackcacherefill(c, order)
x = c.stackcache[order].list
}
c.stackcache[order].list = x.ptr().next
c.stackcache[order].size -= uintptr(n)
}
v = unsafe.Pointer(x)
// mcache.stackcache 扩容
func stackcacherefill(c *mcache, order uint8) {
......
var list gclinkptr
var size uintptr
lock(&stackpool[order].item.mu)
// 从全局池(stackpool)中获取一些 stack
// 获取所允许的容量的一半来防止 thrashing
for size < _StackCacheSize/2 {
x := stackpoolalloc(order)
x.ptr().next = list
list = x
size += _FixedStack << order
}
unlock(&stackpool[order].item.mu)
c.stackcache[order].list = list
c.stackcache[order].size = size
}
// 从空闲池中分配一个栈,必须在持有 stackpool[order].item.mu 下调用
func stackpoolalloc(order uint8) gclinkptr {
list := &stackpool[order].item.span // mSpanList 存储了 mspan 的头部和尾部
s := list.first // 链表头
lockWithRankMayAcquire(&mheap_.lock, lockRankMheap)
// 证明没有缓存已空
if s == nil {
// 从 mheap 上申请,一次申请 32KB 内存即 4 页((32*1024) >> 13)
s = mheap_.allocManual(_StackCacheSize>>_PageShift, &memstats.stacks_inuse)
......
// OpenBSD 6.4+ 对栈内存有特殊的需求,所以只要我们从堆上申请栈内存,需要在申请后做一些额外处理
osStackAlloc(s)
s.elemsize = _FixedStack << order
for i := uintptr(0); i < _StackCacheSize; i += s.elemsize {
// gclinkptr 也是一个指针类型
// 作用是屏蔽gc扫描
x := gclinkptr(s.base() + i)
// 链表头插法
x.ptr().next = s.manualFreeList
s.manualFreeList = x
}
list.insert(s)
}
x := s.manualFreeList
if x.ptr() == nil {
throw("span has no free stacks")
}
s.manualFreeList = x.ptr().next
s.allocCount++
if s.manualFreeList.ptr() == nil {
// 所有内存已经分配完毕,删除节点 s
list.remove(s)
}
return x
}
大栈
大空间从 stackLarge
进行分配
var s *mspan
npage := uintptr(n) >> _PageShift
log2npage := stacklog2(npage)
// 尝试从 stackLarge 缓存中获取堆栈。
lock(&stackLarge.lock)
if !stackLarge.free[log2npage].isEmpty() {
s = stackLarge.free[log2npage].first
stackLarge.free[log2npage].remove(s)
}
unlock(&stackLarge.lock)
lockWithRankMayAcquire(&mheap_.lock, lockRankMheap)
if s == nil {
// 如果无法从缓存中获取,则从堆中分配一个新的栈
s = mheap_.allocManual(npage, &memstats.stacks_inuse)
if s == nil {
throw("out of memory")
}
osStackAlloc(s)
s.elemsize = uintptr(n)
}
v = unsafe.Pointer(s.base())
堆上分配
无论是大栈还是小栈的分配,都是使用从 mheap
上申请的缓存,通过 allocManual
方法:
func (h *mheap) allocManual(npages uintptr, stat *uint64) *mspan {
return h.allocSpan(npages, true, 0, stat)
}
[[Go 内存分配器]]
总结
栈管理
早期几个版本中发生过一些变化:
- v1.0 ~ v1.1 — 最小栈内存空间为 4KB;
- v1.2 — 将最小栈内存提升到了 8KB;
- v1.3 — 使用连续栈替换之前版本的分段栈;
- v1.4 — 将最小栈内存降低到了 2KB;
Goroutine 的初始栈内存在最初的几个版本中多次修改,从 4KB 提升到 8KB 是临时的解决方案,其目的是为了减轻分段栈中的栈分裂对程序的性能影响;在 v1.3 版本引入连续栈之后,Goroutine 的初始栈大小降低到了 2KB,进一步减少了 Goroutine 占用的内存空间。
分段栈
Go 会在编译时在每个 go 函数入口处增加一个栈空间检查代码,如果栈用光了,就会去调用 morestack
函数。morestack
函数会分配一段新内存用作栈空间,接下来它会将有关栈的各种数据信息写入栈底的一个 struct 中,包括上一段的堆栈地址。然后重启 goroutine 来重试导致栈用光的函数。这就是“栈分裂”。
+---------------+
| | <---+ 新栈
| unused |
| stack |
| space |
+---------------+
| Foobar |
| |
+---------------+
| |
| lessstack |
+---------------+
| Stack info |
| |-----+
+---------------+ |
|
|
+---------------+ |
| Foobar | |
| | <---+
+---------------+
| rest of stack | <---+ 旧栈
| |
分段栈回溯机理:在新栈的底部,我们插入了 lessstack
函数。当我们从导致栈分裂的函数返回时,我们会回到 lessstack
, lessstack
会查找 stack 底部的那个 struct,并调整栈指针(rsp),使得返回前一段的栈空间。这样,我们就将新的栈释放掉了。
分段栈也有瑕疵。这两个栈彼此没有连续。 这种设计的缺陷很容易破坏缓存的局部性原理,从而降低程序的运行时性能。
同时收缩栈是一个相对昂贵的操作。如果是在一个循环中分裂栈情况更明显。函数会增长栈,分裂栈,返回栈,并且释放栈分段。如果是在循环里面做这些操作,那么将会付出很大的开销。例如循环一次经历了这些过程,当下一次循环时栈又被耗尽,又得重新分配栈分段,然后又被释放掉,周而复始,循环往复,开销就会巨大。
这就是熟知的 hot split problem
(热点分裂问题)。这是Golang开发组切换到新的栈管理方式的主要原因,新方式称为栈拷贝。
连续栈(栈拷贝)
栈拷贝开始很像分段栈。协程运行,使用栈空间,当栈将要耗尽时,触发相同的栈溢出检测。但是,不像分段栈里有一个回溯链接,栈拷贝的方式则是创建了一个新的分段,它是旧栈的两倍大小,并且把旧栈完全拷贝进来。
但栈拷贝也没有想象中的那么简单。由于栈中的变量在 Golang 中能够获取其地址,因此最终会出现指向栈的指针。而如果轻易拷贝移动栈,任何指向旧栈的指针都会失效。
所以 Golang 的内存安全机制规定,任何能够指向栈的指针都必须存在于栈中。这就可以通过垃圾收集器协助栈拷贝,因为垃圾收集器需要知道哪些指针可以进行回收,所以可以查到栈上的哪些部分是指针,当进行栈拷贝时,会更新指针信息只相信目标,以及它相关的所有指针。
特殊的是 runtime
的大量核心调度函数和 GC 核心都是用 C 语言写的,这些函数都获取不到指针信息,那么它们就无法复制。这种都会在一个特殊的栈中执行(g0),并且由 runtime
开发者定义栈尺寸。
汇编中的连续栈
在机器架构层面,很多关于函数的公用操作都会被提取为固定代码,在函数运行时插入到代码片段的前后部分中,其中函数代码前插入汇编,称为prolog
,一般只会有一个prolog
。在函数代码后插入汇编,称为epilog
,一般可以有多个epilog
。这就是“序言”和“后记”。
golang就是用prolog + epilog
的方式来实现连续栈的检测和复制的。
"".main STEXT size=105 args=0x0 locals=0x20
0x0000 00000 (main.go:23) TEXT "".main(SB), ABIInternal, $32-0
0x0000 00000 (main.go:23) MOVQ (TLS), CX
0x0009 00009 (main.go:23) CMPQ SP, 16(CX)
0x000d 00013 (main.go:23) JLS 98
// main func body
0x0062 00098 (main.go:26) NOP
0x0062 00098 (main.go:23) PCDATA $1, $-1
0x0062 00098 (main.go:23) PCDATA $0, $-1
0x0062 00098 (main.go:23) CALL runtime.morestack_noctxt(SB)
0x0067 00103 (main.go:23) JMP 0
栈溢出检测实现
TLS(thred-local storage) 是伪寄存器,它表示 g
结构体的位置。并且只能被载入到另一个寄存器中(因为本质上不是寄存器,是内存位置?)。16(TLS) 指向的是 g.stackguard0
。g.stackguard0
在上面的源码中显示被设置为 g.stack.lo + _StackGuard
的位置,也就是保留栈顶的一段(_StackGuard
)位置。所以每一个不是 nosplit
的函数都会在编译后的函数中加入检查,比较 SP 和 g.stackguard0
的值。
这就表示:栈溢出发生在整个函数执行前就能被侦测到,而不是函数内某条语句执行时。
执行栈的伸缩
栈的扩张
经过溢出检测后,会跳转到汇编实现的函数上进行栈扩容,如果函数不需要 g.sched.ctxt
字段,则会调用 runtime.nirestack_noctxt
,否则会被编译为直接调用 runtime.morestack
。
TEXT runtime·morestack_noctxt(SB),NOSPLIT,$0
MOVL $0, DX // DX 中存储着 g.sched.ctxt 字段,置为0意为不需要保存。
JMP runtime·morestack(SB)
TEXT runtime·morestack(SB),NOSPLIT,$0-0
// 检查要增加的是否为 g0 栈,不能扩容 g0 栈
get_tls(CX)
MOVQ g(CX), BX
MOVQ g_m(BX), BX
MOVQ m_g0(BX), SI
CMPQ g(CX), SI
JNE 3(PC)
CALL runtime·badmorestackg0(SB)
CALL runtime·abort(SB)
// 不能扩容信号栈(gsignal stack)
MOVQ m_gsignal(BX), SI
CMPQ g(CX), SI
JNE 3(PC)
CALL runtime·badmorestackgsignal(SB)
CALL runtime·abort(SB)
// 从 f 调用
// 将 m->morebuf 设置为 f 的调用方
NOP SP // tell vet SP changed - stop checking offsets
MOVQ 8(SP), AX // f's caller's PC
MOVQ AX, (m_morebuf+gobuf_pc)(BX)
LEAQ 16(SP), AX // f's caller's SP
MOVQ AX, (m_morebuf+gobuf_sp)(BX)
get_tls(CX)
MOVQ g(CX), SI
MOVQ SI, (m_morebuf+gobuf_g)(BX)
// 设置当前的执行栈(g.sched)为 f
MOVQ 0(SP), AX // f's PC
MOVQ AX, (g_sched+gobuf_pc)(SI)
MOVQ SI, (g_sched+gobuf_g)(SI)
LEAQ 8(SP), AX // f's SP
MOVQ AX, (g_sched+gobuf_sp)(SI)
MOVQ BP, (g_sched+gobuf_bp)(SI)
MOVQ DX, (g_sched+gobuf_ctxt)(SI)
// 切换到 g0 栈上调用 newstack
MOVQ m_g0(BX), BX
MOVQ BX, g(CX)
MOVQ (g_sched+gobuf_sp)(BX), SP
CALL runtime·newstack(SB)
CALL runtime·abort(SB) // crash if newstack returns
RET
newstack
在前半部分承担了对 Goroutine 进行抢占的任务[[Go 协作与抢占]],在后半部分则是真正的扩张。
func newstack() {
// g0
thisg := getg()
......
// 寻找要执行的 g
gp := thisg.m.curg
......
morebuf := thisg.m.morebuf
thisg.m.morebuf.pc = 0
thisg.m.morebuf.lr = 0
thisg.m.morebuf.sp = 0
thisg.m.morebuf.g = 0
......
sp := gp.sched.sp
if sys.ArchFamily == sys.AMD64 || sys.ArchFamily == sys.I386 || sys.ArchFamily == sys.WASM {
// 对 morestack 的调用花费了一个字,是因为 call 指令吗?
sp -= sys.PtrSize
}
// 分配一个更大(2倍)的栈并移动
oldsize := gp.stack.hi - gp.stack.lo
newsize := oldsize * 2
// 需要的栈太大, 直接溢出
if newsize > maxstacksize {
print("runtime: goroutine stack exceeds ", maxstacksize, "-byte limit\n")
print("runtime: sp=", hex(sp), " stack=[", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "]\n")
throw("stack overflow")
}
// goroutine 必须是正在执行中才会来调用 newstack,所以状态一定是 Grunning(or Gscanrunning)
// 转为 Gcopystack
casgstatus(gp, _Grunning, _Gcopystack)
// 因为 goroutine 处于 Gcopystack 状态,所以我们在复制栈时不会被并发的 gc 影响。
copystack(gp, newsize)
......
// 继续执行
casgstatus(gp, _Gcopystack, _Grunning)
gogo(&gp.sched)
}
栈的拷贝
栈拷贝的难点在于 Go 栈上的变量会包含自己的地址,当我们拷贝了一个指向原栈的指针时,拷贝后的指针就会变为无效指针。所以 Go 的策略是只有栈上分配的指针才能指向栈上的地址,否则这个指针指向的对象会重新在堆中进行分配(逃逸)。
func copystack(gp *g, newsize uintptr) {
// 旧栈
old := gp.stack
used := old.hi - gp.sched.sp
// 获取新栈
new := stackalloc(uint32(newsize))
// 计算调节幅度
var adjinfo adjustinfo
adjinfo.old = old
adjinfo.delta = new.hi - old.hi
// 调整 sudog,必要时与 channel 操作同步
ncopy := used
if !gp.activeStackChans {
adjustsudogs(gp, &adjinfo)
} else {
adjinfo.sghi = findsghi(gp, old)
ncopy -= syncadjustsudogs(gp, used, &adjinfo)
}
// 复制栈
memmove(unsafe.Pointer(new.hi-ncopy), unsafe.Pointer(old.hi-ncopy), ncopy)
// 新栈替换旧栈
gp.stack = new
gp.stackguard0 = new.lo + _StackGuard // 注意:可能会破坏一个抢占请求
gp.sched.sp = new.hi - used
gp.stktopsp += adjinfo.delta
// 释放旧栈
if stackPoisonCopy != 0 {
fillstack(old, 0xfc)
}
stackfree(old)
}
栈的收缩
栈的收缩发生在 GC 时
func scanstack(gp *g, gcw *gcWork) {
......
switch readgstatus(gp) &^ _Gscan {
default:
print("runtime: gp=", gp, ", goid=", gp.goid, ", gp->atomicstatus=", readgstatus(gp), "\n")
throw("mark - bad status")
case _Gdead:
return
case _Grunning:
print("runtime: gp=", gp, ", goid=", gp.goid, ", gp->atomicstatus=", readgstatus(gp), "\n")
throw("scanstack: goroutine not stopped")
case _Grunnable, _Gsyscall, _Gwaiting:
// 只在这三种状态下才能收缩
}
......
// 检查是否能够安全的收缩栈,比如系统调用时不可以,因为可能有指向栈的指针。
if isShrinkStackSafe(gp) {
// Shrink the stack if not much of it is being used.
shrinkstack(gp)
} else {
// Otherwise, shrink the stack at the next sync safe point.
gp.preemptShrink = true
}
......
}
func shrinkstack(gp *g) {
oldsize := gp.stack.hi - gp.stack.lo
newsize := oldsize / 2 // 收缩幅度为减半
// 但不能小于最小栈大小
if newsize < _FixedStack {
return
}
// 仅当栈使用量小于四分之一时才会对栈进行收缩
avail := gp.stack.hi - gp.stack.lo
if used := gp.stack.hi - gp.sched.sp + _StackLimit; used >= avail/4 {
return
}
copystack(gp, newsize)
}
Goroutine 执行现场
在上面的 morestack
中可以看到一些对 g.sched(gobuf)
字段的处理:
// 设置当前的执行栈(g.sched)为 f
MOVQ 0(SP), AX // f's PC
MOVQ AX, (g_sched+gobuf_pc)(SI)
MOVQ SI, (g_sched+gobuf_g)(SI)
LEAQ 8(SP), AX // f's SP
MOVQ AX, (g_sched+gobuf_sp)(SI)
MOVQ BP, (g_sched+gobuf_bp)(SI)
MOVQ DX, (g_sched+gobuf_ctxt)(SI)
以及调用 gogo
函数时需要传入 gobuf
:
func newstack() {
gogo(&gp.sched)
}
剩余内容在 [[Go runtime]] 中也有体现。