golang 栈的扩大和收缩

本文深入探讨了Go语言中栈的动态调整机制,包括栈扩大与收缩的过程,以及如何通过runtime·morestack_noctxt等函数实现栈的安全性检查与goroutine的抢占式调度。

栈扩大

stack.go中的常量,用于检查goroutinue的状态

 uintptrMask = 1<<(8*sys.PtrSize) - 1
    // Goroutine preemption request.
    // Stored into g->stackguard0 to cause split stack check failure.
    // Must be greater than any real sp.
    // 0xfffffade in hex.
stackPreempt = uintptrMask & -1314
    // Thread is forking.
    // Stored into g->stackguard0 to cause split stack check failure.
    // Must be greater than any real sp.
stackFork = uintptrMask & -1234

当检查到栈不足够的时候,调用runtime·morestack_noctxt,检查完成之后将重新从函数其实处运行,再进行一次栈check,因为进入newstack有可能并不会增长栈。

0x0000 00000 (x.go:19)  MOVQ    TLS, CX
0x0009 00009 (x.go:19)  MOVQ    (CX)(TLS*2), CX
0x0010 00016 (x.go:19)  LEAQ    -160(SP), AX
0x0018 00024 (x.go:19)  CMPQ    AX, 16(CX)
0x001c 00028 (x.go:19)  JLS 637
 ...
0x027d 00637 (x.go:19)  CALL    runtime.morestack_noctxt(SB)
0x0282 00642 (x.go:19)  JMP 0

这个函数调用runtime·morestack

这个函数中:
- 检查是否是从g或者是gsignal中调用的这个函数,如果是,则报错
- 假设调用调用morestack_noctx的函数为f,
- 首先保存f的caller’s SP到m的morebuf 还有PC还有当前g
- 然后保存f的SP PC和BP以及g到当前goroutinue的gobuf
- morebuf主要用于在morestack中出现throw的时候可以正常打印调用栈
- 切换到m.g0的栈上调用runtime.newstack

runtime.newstack

栈扩容安全性检查

if thisg.m.morebuf.g.ptr().stackguard0 == stackFork {
    throw("stack growth after fork")
}

不能再fork之后调用morestack,fork就是栈还没有初始化完的时候,会把栈顶设置为stackFork。

如果morebuf中的g不等于m.curg,报错

gp := thisg.m.curg
// Write ctxt to gp.sched. We do this here instead of in
// morestack so it has the necessary write barrier.
gp.sched.ctxt = ctxt


if thisg.m.curg.throwsplit {
    // Update syscallsp, syscallpc in case traceback uses them.
    morebuf := thisg.m.morebuf
    gp.syscallsp = morebuf.sp
    gp.syscallpc = morebuf.pc
    print("runtime: newstack sp=", hex(gp.sched.sp), " stack=[", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "]\n",
        "\tmorebuf={pc:", hex(morebuf.pc), " sp:", hex(morebuf.sp), " lr:", hex(morebuf.lr), "}\n",
        "\tsched={pc:", hex(gp.sched.pc), " sp:", hex(gp.sched.sp), " lr:", hex(gp.sched.lr), " ctxt:", gp.sched.ctxt, "}\n")

    traceback(morebuf.pc, morebuf.sp, morebuf.lr, gp)
    throw("runtime: stack split at bad time")
}

不能在throwsplit为ture的时候调用morestack,这个标志表示现在不能进行栈的扩容

判断是否是真的需要扩容

接下来清空m.morebuf

这里判断是不是因为设置了stackPreempt而进去的morestack,如果是,直接把g.stackguard0重新设置为gp.stack.lo + _StackGuard

然后切换到goroutinue继续执行,这里是因为如果一个goroutinue需要被抢占,会设置两个状态 g.stackguard0 = stackPreempt和g.preempt = true
这里当g.stackguard0 == stackpreempt的时候,
如果有锁,内存分配,条件的时候,不进行goroutinue切换

后面如果gp.preemptscan为true,也不进行切换

否则调用gopreempt_m进行切换

这个函数最终调用schedule和execute来切换到一个新的goroutinue上运行。

扩容

copystack(gp, uintptr(newsize), true)
casgstatus(gp, _Gcopystack, _Grunning)
gogo(&gp.sched)

调用copystack分配并拷贝栈,然后继续gogo切换到goroutinue继续执行

也就是newstack不但实现了栈的扩容,同时还实现了go的抢占式调度算法

一个小小的疑问及答案不

如果刚好在goroutinue的栈即将不够用的时候设置了stackpreempt,那么是不是有可能因为stackpreempt而没有进行栈扩容而导致栈溢出呢?

关于这个疑问的答案是不会导致栈溢出,请看这里:

    0x027d 00637 (x.go:19)  CALL    runtime.morestack_noctxt(SB)
    0x0282 00642 (x.go:19)  JMP 0

在runtime.morestack_noctext调用之后会JMP0,也就是如果是因为stackpreempt而进入newstack,那么当goroutinue继续运行之后,会返回函数起始处,重新检查sp和栈顶,这时候栈顶已经被newstack恢复。

栈收缩

收缩栈是在mgcmark.go中触发的,主要是在scanstack和markrootFreeGStacks函数中,也就是垃圾回收的时候会根据情况收缩栈

// Maybe shrink the stack being used by gp.
// Called at garbage collection time.
// gp must be stopped, but the world need not be.
shrinkstack 收缩栈在必要的时候

  • 如果这个g是Gdead状态,则会释放栈空间
  • 如果已经使用的栈空间大于总栈空间的1/4,则不进行栈收缩,如果是在正在进行系统调用也不能进行栈缩放,因为system使用的参数可能在栈上面。
  • 缩小栈的空间为原来的一半
### Golang的数据结构实现 #### 定义结构体 为了创建一个基于数组的,在 Go 语言中可以定义如下结构体: ```go type Stack struct { data []interface{} } ``` 此结构体 `Stack` 包含了一个接口类型的切片 `data`,用于存储中的元素[^1]。 #### 初始化方法 初始化一个新的实例可以通过下面的方法完成: ```go func New() *Stack { return &Stack{ data: make([]interface{}, 0), } } ``` 这段代码返回一个指向新创建并已初始化好的对象的指针。 #### 基本操作:压入(Push) 向顶添加元素的操作称为“推”,即 Push 操作。以下是具体的实现方式: ```go func (s *Stack) Push(value interface{}) { s.data = append(s.data, value) } ``` 这里通过调用内置函数 `append()` 来增加新的数据项到底端[^2]。 #### 基本操作:弹出(Pop) 从顶端移除最上面的一个元素的过程叫做 Pop 操作。考虑到性能优化的需求,当内剩余空间较大时会适当收缩其内部使用的内存大小以节省资源。具体逻辑如下所示: ```go func (s *Stack) Pop() interface{} { if s.IsEmpty() { fmt.Println("The stack is empty.") return nil } topIndex := len(s.data) - 1 element := s.data[topIndex] s.data = s.data[:topIndex] // 如果当前长度远小于容量,则缩小容量 if cap(s.data)-len(s.data) >= 65536 || len(s.data)*2 < cap(s.data) { newData := make([]interface{}, len(s.data)) copy(newData, s.data) s.data = newData } return element } ``` 上述代码实现了安全检查以及动态调整底层存储机制的功能[^4]。 #### 判断是否为空(IsEmpty) 提供一种简单的方式来判断是否为空是非常有用的。这可以通过比较内的元素数量来轻松做到: ```go func (s *Stack) IsEmpty() bool { return len(s.data) == 0 } ``` 如果没有任何元素,则认为它是空的状态;反之亦然。 #### 获取顶元素(Top) 有时可能只需要查看而不必取出顶元素,这时就可以使用 Top 方法: ```go func (s *Stack) Top() interface{} { if !s.IsEmpty() { return s.data[len(s.data)-1] } return nil } ``` 这个功能允许访问但不会改变的内容。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值