一文教你搞懂 Go 中栈操作 (3)

在进行栈分配之前我们先来看看栈初始化的时候会做什么,需要注意的是,stackinit 是在调用 runtime·schedinit初始化的,是在调用 runtime·newproc之前进行的。

在执行栈初始化的时候会初始化两个全局变量 stackpool 和 stackLarge。stackpool 可以分配小于 32KB 的内存,stackLarge 用来分配大于 32KB 的栈空间。

栈的分配

从初始化的两个两个全局变量我们也可以知道,栈会根据大小的不同从不同的位置进行分配。

小栈内存分配

文件位置:src/runtime/stack.go

func stackalloc(n uint32) stack { // 这里的 G 是 G0 thisg := getg() ... var v unsafe.Pointer // 在 Linux 上,_FixedStack = 2048、_NumStackOrders = 4、_StackCacheSize = 32768 // 如果申请的栈空间小于 32KB if n < _FixedStack<<_NumStackOrders && n < _StackCacheSize { order := uint8(0) n2 := n // 大于 2048 ,那么 for 循环 将 n2 除 2,直到 n 小于等于 2048 for n2 > _FixedStack { // order 表示除了多少次 order++ n2 >>= 1 } var x gclinkptr //preemptoff != "", 在 GC 的时候会进行设置,表示如果在 GC 那么从 stackpool 分配 // thisg.m.p = 0 会在系统调用和 改变 P 的个数的时候调用,如果发生,那么也从 stackpool 分配 if stackNoCache != 0 || thisg.m.p == 0 || thisg.m.preemptoff != "" { lock(&stackpool[order].item.mu) // 从 stackpool 分配 x = stackpoolalloc(order) unlock(&stackpool[order].item.mu) } else { // 从 P 的 mcache 分配内存 c := thisg.m.p.ptr().mcache x = c.stackcache[order].list if x.ptr() == nil { // 从堆上申请一片内存空间填充到stackcache中 stackcacherefill(c, order) x = c.stackcache[order].list } // 移除链表的头节点 c.stackcache[order].list = x.ptr().next c.stackcache[order].size -= uintptr(n) } // 获取到分配的span内存块 v = unsafe.Pointer(x) } else { ... } ... return stack{uintptr(v), uintptr(v) + uintptr(n)} }

stackalloc 会根据传入的参数 n 的大小进行分配,在 Linux 上如果 n 小于 32768 bytes,也就是 32KB ,那么会进入到小栈的分配逻辑中。

小栈指大小为 2K/4K/8K/16K 的栈,在分配的时候,会根据大小计算不同的 order 值,如果栈大小是 2K,那么 order 就是 0,4K 对应 order 就是 1,以此类推。这样一方面可以减少不同 Goroutine 获取不同栈大小的锁冲突,另一方面可以预先缓存对应大小的 span ,以便快速获取。

thisg.m.p == 0可能发生在系统调用 exitsyscall 或改变 P 的个数 procresize 时,thisg.m.preemptoff != ""会发生在 GC 的时候。也就是说在发生在系统调用 exitsyscall 或改变 P 的个数在变动,亦或是在 GC 的时候,会从 stackpool 分配栈空间,否则从 mcache 中获取。

如果 mcache 对应的 stackcache 获取不到,那么调用 stackcacherefill 从堆上申请一片内存空间填充到stackcache中。

主要注意的是,stackalloc 由于切换到 G0 进行调用,所以 thisg 是 G0,我们也可以通过《如何编译调试 Go runtime 源码 https://www.luozhiyun.com/archives/506 》这一篇文章的方法来进行调试:

func stackalloc(n uint32) stack { thisg := getg() // 添加一行打印 if debug.schedtrace > 0 { print("stackalloc runtime: gp: gp=", thisg, ", goid=", thisg.goid, ", gp->atomicstatus=", readgstatus(thisg), "\n") } ... }

下面我们分别看一下 stackpoolalloc 与 stackcacherefill 函数。

runtime.stackpoolalloc

func stackpoolalloc(order uint8) gclinkptr { list := &stackpool[order].item.span s := list.first lockWithRankMayAcquire(&mheap_.lock, lockRankMheap) if s == nil { // no free stacks. Allocate another span worth. // 从堆上分配 mspan // _StackCacheSize = 32 * 1024 s = mheap_.allocManual(_StackCacheSize>>_PageShift, &memstats.stacks_inuse) if s == nil { throw("out of memory") } // 刚分配的 span 里面分配对象个数肯定为 0 if s.allocCount != 0 { throw("bad allocCount") } if s.manualFreeList.ptr() != nil { throw("bad manualFreeList") } //OpenBSD 6.4+ 系统需要做额外处理 osStackAlloc(s) // Linux 中 _FixedStack = 2048 s.elemsize = _FixedStack << order //_StackCacheSize = 32 * 1024 // 这里是将 32KB 大小的内存块分成了elemsize大小块,用单向链表进行连接 // 最后 s.manualFreeList 指向的是这块内存的尾部 for i := uintptr(0); i < _StackCacheSize; i += s.elemsize { x := gclinkptr(s.base() + i) x.ptr().next = s.manualFreeList s.manualFreeList = x } // 插入到 list 链表头部 list.insert(s) } x := s.manualFreeList // 代表被分配完毕 if x.ptr() == nil { throw("span has no free stacks") } // 将 manualFreeList 往后移动一个单位 s.manualFreeList = x.ptr().next // 统计被分配的内存块 s.allocCount++ // 因为分配的时候第一个内存块是 nil // 所以当指针为nil 的时候代表被分配完毕 // 那么需要将该对象从 list 的头节点移除 if s.manualFreeList.ptr() == nil { // all stacks in s are allocated. list.remove(s) } return x }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpsjdd.html