最新版本查看 https://blog.haohtml.com/archives/30403
當(dāng)我們執(zhí)行一個(gè) go func() 語句的時(shí)候,runtime 會(huì)通過調(diào)用 newproc() 函數(shù)來創(chuàng)建G。而內(nèi)部真正創(chuàng)建創(chuàng)建G的函數(shù)為 newproc1(),在沒有可以復(fù)用的G的情況下,會(huì)通過 newg = malg(_StackMin) 語句創(chuàng)建一個(gè)包含stack的G。
// Allocate a new g, with a stack big enough for stacksize bytes.
func malg(stacksize int32) *g {
newg := new(g)
if stacksize >= 0 {
stacksize = round2(_StackSystem + stacksize)
systemstack(func() {
newg.stack = stackalloc(uint32(stacksize))
})
newg.stackguard0 = newg.stack.lo + _StackGuard
newg.stackguard1 = ^uintptr(0)
// Clear the bottom word of the stack. We record g
// there on gsignal stack during VDSO on ARM and ARM64.
*(*uintptr)(unsafe.Pointer(newg.stack.lo)) = 0
}
return newg
}
對(duì)新創(chuàng)建的g,需要通過調(diào)用 stackalloc() 函數(shù)為其分配 stacksize 大小stack,那么分配操作它又是如何工作的呢?
stack的申請(qǐng)
根據(jù)申請(qǐng)stack的大小,又分為兩種情況,一種是 small stack,另一種是 large stack,兩者采用不同的申請(qǐng)策略。主要涉及了內(nèi)存申請(qǐng)策略,如果對(duì)golang 的內(nèi)存管理比較了解的話,這塊理解起來就顯的太過于簡(jiǎn)單了。建議先閱讀一下這篇文章《Golang 內(nèi)存組件之mspan、mcache、mcentral 和 mheap 數(shù)據(jù)結(jié)構(gòu)》。
func stackalloc(n uint32) stack {
...
var v unsafe.Pointer
if n < _FixedStack<<_NumStackOrders && n < _StackCacheSize {
order := uint8(0)
n2 := n
for n2 > _FixedStack {
order++
n2 >>= 1
}
var x gclinkptr
if stackNoCache != 0 || thisg.m.p == 0 || thisg.m.preemptoff != "" {
// thisg.m.p == 0 can happen in the guts of exitsyscall
// or procresize. Just get a stack from the global pool.
// Also don't touch stackcache during gc
// as it's flushed concurrently.
lock(&stackpool[order].item.mu)
x = stackpoolalloc(order)
unlock(&stackpool[order].item.mu)
} else {
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)
} else {
...
}
return stack{uintptr(v), uintptr(v) + uintptr(n)}
}
對(duì)于small stack,會(huì)直接通過從G綁定的P中的 mcache 字段申請(qǐng),這個(gè)字段可以理解為內(nèi)存資源中心,里面包含有多種不同規(guī)格大小的內(nèi)存塊,根據(jù)申請(qǐng)大小找到一個(gè)可以滿足其大小的最小規(guī)格的內(nèi)存區(qū)域。
這里重點(diǎn)關(guān)注下 stackcacherefill() 函數(shù)。
func stackalloc(n uint32) stack {
...
var v unsafe.Pointer
if n < _FixedStack<<_NumStackOrders && n < _StackCacheSize {
...
} else {
var s *mspan
npage := uintptr(n) >> _PageShift
log2npage := stacklog2(npage)
// Try to get a stack from the large stack cache.
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 {
// Allocate a new stack from the heap.
s = mheap_.allocManual(npage, spanAllocStack)
if s == nil {
throw("out of memory")
}
osStackAlloc(s)
s.elemsize = uintptr(n)
}
v = unsafe.Pointer(s.base())
}
...
return stack{uintptr(v), uintptr(v) + uintptr(n)}
}
對(duì)于 large stack 而言,先從 stackLarge 全局對(duì)象 large stack spans 池中查找,如果未找到的話,則調(diào)用 osStackAlloc() 和 mheap_.allocManual() 函數(shù)直接從 heap 中直接申請(qǐng)并注冊(cè)這塊內(nèi)存。
最后根據(jù)找到內(nèi)存的起始地址(低地址)和 申請(qǐng)大小n,返回 stack 結(jié)構(gòu)體。
stack的釋放
對(duì)于stack的釋放,實(shí)現(xiàn)函數(shù)為 stackfree()。
在資源申請(qǐng)時(shí)根據(jù)其申請(qǐng)大小分為兩種情況,同樣對(duì)于stack的釋放也是一樣。對(duì)于small stack 來講,從哪里申請(qǐng)的,用完以后再放回去即可。操作對(duì)象仍是 P下面的 mcache 字段。
這里重點(diǎn)關(guān)注下 stackcacherelease() 函數(shù)。
對(duì)于 large stack 來講,則將再放在 stackLarge 全局變量里,以便可以復(fù)用,避免重復(fù)申請(qǐng)產(chǎn)生的開銷;但如果當(dāng)前GC正處于清掃階段,則直接調(diào)用 osStackFree() 和 mheap_.freeManual() 來釋放內(nèi)存。
對(duì)于g中stack的釋放觸發(fā)條件有:
- 當(dāng)一個(gè)g運(yùn)行結(jié)束的時(shí)候,可能會(huì)釋放stack(只是有可能)參考:https://blog.haohtml.com/archives/23437;
- GC期間會(huì)清理所有包含stack的g,直接釋放完所有g(shù)的stack, 然后再將這些g放在不包含stack的
sched.gFree.noStack列表中,參考:https://blog.haohtml.com/archives/27003; - 其它