go在線程的基礎上實現了用戶態更加輕量級的寫成,線程棧為了防止stack overflow,默認大小一般是2MB,而在go中,協程棧在初始化時是2KB
go中的棧是可以擴容的,在64位操作系統上最大為1GB
1. newstack()函數
在函數序言階段如果判斷出需要擴容,則會跳轉調用運行時morestack_noctxt函數,函數調用鏈為:
morestack_noctxt() -> morestack() -> newstack()
核心代碼位於 newstack() 函數中,newstack()函數不僅會處理擴容,還會處理協程的搶占
下面看一下newstack()函數的核心實現:
func newstack() {
oldsize := gp.stack.hi - gp.stack.lo
// 兩倍於原來大小
newsize := oldsize * 2
// 需要的棧太大,直接溢出
if newsize > maxstacksize {
throw( "stack overflow" )
}
// goroutine必須是正在執行過程中才會調用newstack
// 所以這個狀態一定是Grunning或者Gscanrunning
casgstatus(gp, _Grunning, _Gcopystack)
// gp的處於Gcopystack狀態,當我們對棧進行復制時並發GC不會掃描此棧
// 棧的復制
copystack(gp, newsize)
casgstatus(gp, _Gcopystack, _Grunning)
// 繼續執行
gogo(&gp.sched)
}
什么是gp?
gp就是當前協程的結構體:
type g struct {
stack stack
stackguard0 uintptr
stackguard1 uintptr
...
}
type stack struct {
lo uintptr // 8 bytes
hi uintptr
}
gp.stack.hi - gp.stack.lo就是在計算當前協程棧的大小
newstack()函數首先通過棧底地址與棧頂地址計算出舊棧的大小,並計算新棧的大小,新棧大小為舊棧的兩倍大。在64為操作系統中,如果棧大小超過了1GB(maxstacksize)則直接報錯stack overflow
2. 棧轉移
棧擴容的重要一步就是將舊棧的內容轉移到新棧中,棧擴容首先將協程的狀態設置為 _Gcopystack,以便在垃圾回收時不會掃描該棧帶來錯誤
棧復制並不是向內存復制一樣簡單,需要處理很多其他地址的指針轉移的問題,同時為了應對頻繁的棧調整,linux操作系統下,會對2/4/8/16KB的小棧進行專門的優化
在全局以及每個邏輯處理器中預先分配這些小棧的緩存池,避免頻繁申請堆內存
對於大棧,其大小不確定,孫然也有一個全局的緩存池,但不會預先放入多個棧,當棧被銷毀時,如果被銷毀的棧為大棧則放入全局緩存池中
在分配到棧后,如果有指針指向舊棧,那么需要將其調整到新棧中
在調整時有一個額外的步驟是調整sudog,由於通道在阻塞的情況下存儲的元素可能指向了站上的指針,因此需要調整
接着需要將舊棧的大小復制到新棧中,這涉及借助memmove函數進行內存復制
擴容最關鍵的一步是在新棧中調整指針,因為新棧中的指針可能指向舊棧,舊棧一旦釋放后會出現問題。
在棧擴容的時候,copystack函數會遍歷新棧上雖有的棧幀信息,並遍歷其中所有可能指針的位置,一旦發現指針指向舊棧,就會調整當前的指針使其指向新棧