深入理解Go-runtime.SetFinalizer原理剖析

finalizer是與對象關聯的一個函數,通過runtime.SetFinalizer 來設置,它在對象被GC的時候,這個finalizer會被調用,以完成對象生命中最後一程。由於finalizer的存在,導致了對象在三色標記中,不可能被標爲白色對象,也就是垃圾,所以,這個對象的生命也會得以延續一個GC週期。正如defer一樣,我們也可以通過 Finalizer 完成一些類似於資源釋放的操作

1. 結構概覽

1.1. heap

type mspan struct {
    // 當前span上所有對象的special串成鏈表
    // special中有個offset,就是數據對象在span上的offset,通過offset,將數據對象和special關聯起來
    specials    *special   // linked list of special records sorted by offset.
}

1.2. special

type special struct {
    next   *special // linked list in span
    // 數據對象在span上的offset
    offset uint16   // span offset of object
    kind   byte     // kind of special
}

1.3. specialfinalizer

type specialfinalizer struct {
    special special
    fn      *funcval // May be a heap pointer.
    // return的數據的大小
    nret    uintptr
    // 第一個參數的類型
    fint    *_type   // May be a heap pointer, but always live.
    // 與finalizer關聯的數據對象的指針類型
    ot      *ptrtype // May be a heap pointer, but always live.
}

1.4. finalizer

type finalizer struct {
    fn   *funcval       // function to call (may be a heap pointer)
    arg  unsafe.Pointer // ptr to object (may be a heap pointer)
    nret uintptr        // bytes of return values from fn
    fint *_type         // type of first argument of fn
    ot   *ptrtype       // type of ptr to object (may be a heap pointer)
}

1.5. 全局變量

var finlock mutex  // protects the following variables
// 運行finalizer的g,只有一個g,不用的時候休眠,需要的時候再喚醒
var fing *g        // goroutine that runs finalizers
// finalizer的全局隊列,這裏是已經設置的finalizer串成的鏈表
var finq *finblock // list of finalizers that are to be executed
// 已經釋放的finblock的鏈表,用finc緩存起來,以後需要使用的時候可以直接取走,避免再走一遍內存分配了
var finc *finblock // cache of free blocks
var finptrmask [_FinBlockSize / sys.PtrSize / 8]byte
var fingwait bool  // fing的標誌位,通過 fingwait和fingwake,來確定是否需要喚醒fing
var fingwake bool
// 所有的blocks串成的鏈表
var allfin *finblock // list of all blocks

2. 源碼分析

2.1. 創建finalizer

2.1.1. main

func main() {
    // i 就是後面說的 數據對象
    var i = 3
    // 這裏的func 就是後面一直說的 finalizer
    runtime.SetFinalizer(&i, func(i *int) {
        fmt.Println(i, *i, "set finalizer")
    })
    time.Sleep(time.Second * 5)
}

2.1.2. SetFinalizer

根據 數據對象 ,生成一個special對象,並綁定到 數據對象 所在的span,串聯到span.specials上,並且確保fing的存在

func SetFinalizer(obj interface{}, finalizer interface{}) {
    if debug.sbrk != 0 {
        // debug.sbrk never frees memory, so no finalizers run
        // (and we don't have the data structures to record them).
        return
    }
    e := efaceOf(&obj)
    etyp := e._type
    // ---- 省略數據校驗的邏輯 ---
    ot := (*ptrtype)(unsafe.Pointer(etyp))

    // find the containing object
    // 在內存中找不到分配的地址時 base==0,setFinalizer 是在內存回收的時候調用,沒有分配就不會回收
    base, _, _ := findObject(uintptr(e.data), 0, 0)

    f := efaceOf(&finalizer)
    ftyp := f._type
    // 如果 finalizer type == nil,嘗試移除(沒有的話,就不需要移除了)
    if ftyp == nil {
        // switch to system stack and remove finalizer
        systemstack(func() {
            removefinalizer(e.data)
        })
        return
    }
    // --- 對finalizer參數數量及類型進行校驗 --
    if ftyp.kind&kindMask != kindFunc {
        throw("runtime.SetFinalizer: second argument is " + ftyp.string() + ", not a function")
    }
    ft := (*functype)(unsafe.Pointer(ftyp))
    if ft.dotdotdot() {
        throw("runtime.SetFinalizer: cannot pass " + etyp.string() + " to finalizer " + ftyp.string() + " because dotdotdot")
    }
    if ft.inCount != 1 {
        throw("runtime.SetFinalizer: cannot pass " + etyp.string() + " to finalizer " + ftyp.string())
    }
    fint := ft.in()[0]
    switch {
    case fint == etyp:
        // ok - same type
        goto okarg
    case fint.kind&kindMask == kindPtr:
        if (fint.uncommon() == nil || etyp.uncommon() == nil) && (*ptrtype)(unsafe.Pointer(fint)).elem == ot.elem {
            // ok - not same type, but both pointers,
            // one or the other is unnamed, and same element type, so assignable.
            goto okarg
        }
    case fint.kind&kindMask == kindInterface:
        ityp := (*interfacetype)(unsafe.Pointer(fint))
        if len(ityp.mhdr) == 0 {
            // ok - satisfies empty interface
            goto okarg
        }
        if _, ok := assertE2I2(ityp, *efaceOf(&obj)); ok {
            goto okarg
        }
    }
    throw("runtime.SetFinalizer: cannot pass " + etyp.string() + " to finalizer " + ftyp.string())
okarg:
    // compute size needed for return parameters
    // 計算返回參數的大小並進行對齊
    nret := uintptr(0)
    for _, t := range ft.out() {
        nret = round(nret, uintptr(t.align)) + uintptr(t.size)
    }
    nret = round(nret, sys.PtrSize)

    // make sure we have a finalizer goroutine
    // 確保 finalizer 有一個 goroutine
    createfing()

    systemstack(func() {
        // 卻換到g0,添加finalizer,並且不能重複設置
        if !addfinalizer(e.data, (*funcval)(f.data), nret, fint, ot) {
            throw("runtime.SetFinalizer: finalizer already set")
        }
    })
}

這裏邏輯沒什麼複雜的,只是在參數、類型的判斷等上面,比較的麻煩

2.1.3. removefinalizer

通過removespecial,找到數據對象p所對應的special對象,如果找到的話,釋放mheap上對應的內存

func removefinalizer(p unsafe.Pointer) {
    // 根據數據p找到對應的special對象
    s := (*specialfinalizer)(unsafe.Pointer(removespecial(p, _KindSpecialFinalizer)))
    if s == nil {
        return // there wasn't a finalizer to remove
    }
    lock(&mheap_.speciallock)
    // 釋放找到的special所對應的內存
    mheap_.specialfinalizeralloc.free(unsafe.Pointer(s))
    unlock(&mheap_.speciallock)
}

這裏的函數,雖然叫removefinalizer, 但是這裏暫時跟finalizer結構體沒有關係,都是在跟special結構體打交道,後面的addfinalizer也是一樣的

2.1.4. removespecial

遍歷數據所在的span的specials,如果找到了指定數據p的special的話,就從specials中移除,並返回

func removespecial(p unsafe.Pointer, kind uint8) *special {
    // 找到數據p所在的span
    span := spanOfHeap(uintptr(p))
    if span == nil {
        throw("removespecial on invalid pointer")
    }

    // Ensure that the span is swept.
    // Sweeping accesses the specials list w/o locks, so we have
    // to synchronize with it. And it's just much safer.
    mp := acquirem()
    // 保證span被清掃過了
    span.ensureSwept()
    // 獲取數據p的偏移量,根據偏移量去尋找p對應的special
    offset := uintptr(p) - span.base()

    lock(&span.speciallock)
    t := &span.specials
    // 遍歷span.specials這個鏈表
    for {
        s := *t
        if s == nil {
            break
        }
        // This function is used for finalizers only, so we don't check for
        // "interior" specials (p must be exactly equal to s->offset).
        if offset == uintptr(s.offset) && kind == s.kind {
            // 找到了,修改指針,將當前找到的special移除
            *t = s.next
            unlock(&span.speciallock)
            releasem(mp)
            return s
        }
        t = &s.next
    }
    unlock(&span.speciallock)
    releasem(mp)
    // 沒有找到,就返回nil
    return nil
}

2.1.5. addfinalizer

正好跟removefinalizer相反,這個就是根據數據對象p,創建對應的special,然後添加到span.specials鏈表上面

func addfinalizer(p unsafe.Pointer, f *funcval, nret uintptr, fint *_type, ot *ptrtype) bool {
    lock(&mheap_.speciallock)
    // 分配出來一塊內存供finalizer使用
    s := (*specialfinalizer)(mheap_.specialfinalizeralloc.alloc())
    unlock(&mheap_.speciallock)
    s.special.kind = _KindSpecialFinalizer
    s.fn = f
    s.nret = nret
    s.fint = fint
    s.ot = ot
    if addspecial(p, &s.special) {

        return true
    }

    // There was an old finalizer
    // 沒有添加成功,是因爲p已經有了一個special對象了
    lock(&mheap_.speciallock)
    mheap_.specialfinalizeralloc.free(unsafe.Pointer(s))
    unlock(&mheap_.speciallock)
    return false
}

2.1.6. addspecial

這裏是添加special的主邏輯

func addspecial(p unsafe.Pointer, s *special) bool {
    span := spanOfHeap(uintptr(p))
    if span == nil {
        throw("addspecial on invalid pointer")
    }
    // 同 removerspecial一樣,確保這個span已經清掃過了
    mp := acquirem()
    span.ensureSwept()

    offset := uintptr(p) - span.base()
    kind := s.kind

    lock(&span.speciallock)

    // Find splice point, check for existing record.
    t := &span.specials
    for {
        x := *t
        if x == nil {
            break
        }
        if offset == uintptr(x.offset) && kind == x.kind {
            // 已經存在了,不能在增加了,一個數據對象,只能綁定一個finalizer
            unlock(&span.speciallock)
            releasem(mp)
            return false // already exists
        }
        if offset < uintptr(x.offset) || (offset == uintptr(x.offset) && kind < x.kind) {
            break
        }
        t = &x.next
    }

    // Splice in record, fill in offset.
    // 添加到 specials 隊列尾
    s.offset = uint16(offset)
    s.next = *t
    *t = s
    unlock(&span.speciallock)
    releasem(mp)

    return true
}

2.1.7. createfing

這個函數是保證,創建了finalizer之後,有一個goroutine去運行,這裏只運行一次,這個goroutine會由全局變量 fing 記錄

func createfing() {
    // start the finalizer goroutine exactly once
    // 進創建一個goroutine,進行時刻監控運行
    if fingCreate == 0 && atomic.Cas(&fingCreate, 0, 1) {
        // 開啓一個goroutine運行
        go runfinq()
    }
}

2.2. 執行finalizer

在上面的 createfing 的會嘗試創建一個goroutine去執行,接下來就分析一下執行流程吧

func runfinq() {
    var (
        frame    unsafe.Pointer
        framecap uintptr
    )

    for {
        lock(&finlock)
        // 獲取finq 全局隊列,並清空全局隊列
        fb := finq
        finq = nil
        if fb == nil {
            // 如果全局隊列爲空,休眠當前g,等待被喚醒
            gp := getg()
            fing = gp
            // 設置fing的狀態標誌位
            fingwait = true
            goparkunlock(&finlock, waitReasonFinalizerWait, traceEvGoBlock, 1)
            continue
        }
        unlock(&finlock)
        // 循環執行runq鏈表裏的fin數組
        for fb != nil {
            for i := fb.cnt; i > 0; i-- {
                f := &fb.fin[i-1]
                // 獲取存儲當前finalizer的返回數據的大小,如果比之前大,則分配
                framesz := unsafe.Sizeof((interface{})(nil)) + f.nret
                if framecap < framesz {
                    // The frame does not contain pointers interesting for GC,
                    // all not yet finalized objects are stored in finq.
                    // If we do not mark it as FlagNoScan,
                    // the last finalized object is not collected.
                    frame = mallocgc(framesz, nil, true)
                    framecap = framesz
                }

                if f.fint == nil {
                    throw("missing type in runfinq")
                }
                // frame is effectively uninitialized
                // memory. That means we have to clear
                // it before writing to it to avoid
                // confusing the write barrier.
                // 清空frame內存存儲
                *(*[2]uintptr)(frame) = [2]uintptr{}
                switch f.fint.kind & kindMask {
                case kindPtr:
                    // direct use of pointer
                    *(*unsafe.Pointer)(frame) = f.arg
                case kindInterface:
                    ityp := (*interfacetype)(unsafe.Pointer(f.fint))
                    // set up with empty interface
                    (*eface)(frame)._type = &f.ot.typ
                    (*eface)(frame).data = f.arg
                    if len(ityp.mhdr) != 0 {
                        // convert to interface with methods
                        // this conversion is guaranteed to succeed - we checked in SetFinalizer
                        *(*iface)(frame) = assertE2I(ityp, *(*eface)(frame))
                    }
                default:
                    throw("bad kind in runfinq")
                }
                // 調用finalizer函數
                fingRunning = true
                reflectcall(nil, unsafe.Pointer(f.fn), frame, uint32(framesz), uint32(framesz))
                fingRunning = false

                // Drop finalizer queue heap references
                // before hiding them from markroot.
                // This also ensures these will be
                // clear if we reuse the finalizer.
                // 清空finalizer的屬性
                f.fn = nil
                f.arg = nil
                f.ot = nil
                atomic.Store(&fb.cnt, i-1)
            }
            // 將已經完成的finalizer放入finc以作緩存,避免再次分配內存
            next := fb.next
            lock(&finlock)
            fb.next = finc
            finc = fb
            unlock(&finlock)
            fb = next
        }
    }
}

看完上面的流程的時候,突然發現有點懵逼

  1. 全局隊列finq中是什麼時候被插入數據 finalizer的?
  2. g如果休眠了,那怎麼被喚醒呢?

先針對第一個問題分析:

插入隊列的操作,要追溯到我們之前分析的GC 深入理解Go-垃圾回收機制 了,在sweep 中有下面一段函數

2.2.1. sweep

func (s *mspan) sweep(preserve bool) bool {
    ....
    specialp := &s.specials
    special := *specialp
    for special != nil {
        ....
        if special.kind == _KindSpecialFinalizer || !hasFin {
            // Splice out special record.
            y := special
            special = special.next
            *specialp = special
            // 加入全局finq隊列的入口就在這裏了
            freespecial(y, unsafe.Pointer(p), size)
        }
        ....
    }
    ....
}

2.2.2. freespecial

在gc的時候,不僅要把special對應的內存釋放掉,而且把specials整理創建對應dinalizer對象,並插入到 finq隊列裏面

func freespecial(s *special, p unsafe.Pointer, size uintptr) {
    switch s.kind {
    case _KindSpecialFinalizer:
        // 把這個finalizer加入到全局隊列
        sf := (*specialfinalizer)(unsafe.Pointer(s))
        queuefinalizer(p, sf.fn, sf.nret, sf.fint, sf.ot)
        lock(&mheap_.speciallock)
        mheap_.specialfinalizeralloc.free(unsafe.Pointer(sf))
        unlock(&mheap_.speciallock)
    // 下面兩種情況不在分析範圍內,省略
    case _KindSpecialProfile:
        sp := (*specialprofile)(unsafe.Pointer(s))
        mProf_Free(sp.b, size)
        lock(&mheap_.speciallock)
        mheap_.specialprofilealloc.free(unsafe.Pointer(sp))
        unlock(&mheap_.speciallock)
    default:
        throw("bad special kind")
        panic("not reached")
    }
}

2.2.3. queuefinalizer

func queuefinalizer(p unsafe.Pointer, fn *funcval, nret uintptr, fint *_type, ot *ptrtype) {
    lock(&finlock)
    // 如果finq爲空或finq的內部數組已經滿了,則從finc或重新分配 來獲取block並插入到finq的鏈表頭
    if finq == nil || finq.cnt == uint32(len(finq.fin)) {
        if finc == nil {
            finc = (*finblock)(persistentalloc(_FinBlockSize, 0, &memstats.gc_sys))
            finc.alllink = allfin
            allfin = finc
            if finptrmask[0] == 0 {
                // Build pointer mask for Finalizer array in block.
                // Check assumptions made in finalizer1 array above.
                if (unsafe.Sizeof(finalizer{}) != 5*sys.PtrSize ||
                    unsafe.Offsetof(finalizer{}.fn) != 0 ||
                    unsafe.Offsetof(finalizer{}.arg) != sys.PtrSize ||
                    unsafe.Offsetof(finalizer{}.nret) != 2*sys.PtrSize ||
                    unsafe.Offsetof(finalizer{}.fint) != 3*sys.PtrSize ||
                    unsafe.Offsetof(finalizer{}.ot) != 4*sys.PtrSize) {
                    throw("finalizer out of sync")
                }
                for i := range finptrmask {
                    finptrmask[i] = finalizer1[i%len(finalizer1)]
                }
            }
        }
        // 從finc中移除並獲取鏈表頭
        block := finc
        finc = block.next
        // 將從finc獲取到的鏈表掛載到finq的隊列頭,finq指向新的block
        block.next = finq
        finq = block
    }
    // 根據finq.cnt獲取索引對應的block
    f := &finq.fin[finq.cnt]
    atomic.Xadd(&finq.cnt, +1) // Sync with markroots
    // 設置相關屬性
    f.fn = fn
    f.nret = nret
    f.fint = fint
    f.ot = ot
    f.arg = p
    // 設置喚醒標誌
    fingwake = true
    unlock(&finlock)
}

至此,也就明白了,runq全局隊列是怎麼被填充的了

那麼,第二個問題,當fing被休眠後,怎麼被喚醒呢?

這裏就需要追溯到,深入理解Go-goroutine的實現及Scheduler分析 這篇文章了

2.2.4. findrunnable

在 findrunnable 中有一段代碼如下:

func findrunnable() (gp *g, inheritTime bool) {
    // 通過狀態位判斷是否需要喚醒 fing, 通過wakefing來判斷並返回fing
    if fingwait && fingwake {
        if gp := wakefing(); gp != nil {
            // 喚醒g,並從休眠出繼續執行
            ready(gp, 0, true)
        }
    }
}

2.2.5. wakefing

這裏不僅會對狀態位 fingwait fingwake做二次判斷,而且,如果狀態位符合喚醒要求的話,需要重置兩個狀態位

func wakefing() *g {
    var res *g
    lock(&finlock)
    if fingwait && fingwake {
        fingwait = false
        fingwake = false
        res = fing
    }
    unlock(&finlock)
    return res
}

3. 參考文檔

  • 《Go語言學習筆記》--雨痕
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章