golang 棧擴容和棧轉移原理


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函數會遍歷新棧上雖有的棧幀信息,並遍歷其中所有可能指針的位置,一旦發現指針指向舊棧,就會調整當前的指針使其指向新棧

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM