An in-depth understanding of Go-runtime.SetFinalizer principles

Posted by usawh on Sun, 08 Sep 2019 06:09:04 +0200

A finalizer is a function associated with an object, set by runtime.SetFinalizer, that is called when the object is GC to complete the last step in the object's life.The existence of finalizer makes it impossible for an object to be marked as white in a trichrome marker, i.e. garbage, so that the life of the object can continue for a GC cycle.Just like defer, Finalizer allows us to do something similar to resource release

1. Overview of the structure

1.1. heap

type mspan struct {
    // A special ly concatenated list of all objects on the current span
    // There is an offset in the specialty, which is the offset of the data object on the space, through which the data object is associated with the specialty
    specials    *special   // linked list of special records sorted by offset.
}

1.2. special

type special struct {
    next   *special // linked list in span
    // offset of data object on space
    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.
    // The size of the return ed data
    nret    uintptr
    // Type of the first parameter
    fint    *_type   // May be a heap pointer, but always live.
    // Pointer type of data object associated with 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. Global variables

var finlock mutex  // protects the following variables
// Run finalizer's g, only one g, sleep when not in use, wake up when needed
var fing *g        // goroutine that runs finalizers
// finalizer's global queue, here is the list of finalizers that have been set up
var finq *finblock // list of finalizers that are to be executed
// The list of finblock s that have been released, cached in finc, can be removed directly when needed, avoiding memory allocation again
var finc *finblock // cache of free blocks
var finptrmask [_FinBlockSize / sys.PtrSize / 8]byte
var fingwait bool  // The token bit of fing, which uses fingwait and fingwake to determine if fing needs to be waked up
var fingwake bool
// Chain List of All blocks
var allfin *finblock // list of all blocks

2. Source Code Analysis

2.1. Create finalizer

2.1.1. main

func main() {
    // i is the data object that follows
    var i = 3
    // Here the func is the finalizer
    runtime.SetFinalizer(&i, func(i *int) {
        fmt.Println(i, *i, "set finalizer")
    })
    time.Sleep(time.Second * 5)
}

2.1.2. SetFinalizer

Generate a special object from the data object, bind it to the span in which the data object resides, concatenate it to span.specials, and ensure fing exists

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
    // -- Omit the logic of data validation--
    ot := (*ptrtype)(unsafe.Pointer(etyp))

    // find the containing object
    // base==0 when no allocated address is found in memory, setFinalizer is called when memory is reclaimed, and will not be reclaimed without allocation
    base, _, _ := findObject(uintptr(e.data), 0, 0)

    f := efaceOf(&finalizer)
    ftyp := f._type
    // If finalizer type == nil, try removing (if not, you don't need to)
    if ftyp == nil {
        // switch to system stack and remove finalizer
        systemstack(func() {
            removefinalizer(e.data)
        })
        return
    }
    // --Verify the number and type of finalizer parameters--
    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
    // Calculate and align the size of the 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
    // Make sure finalizer has a goroutine
    createfing()

    systemstack(func() {
        // Instead, switch to g0, add finalizer, and do not repeat the settings
        if !addfinalizer(e.data, (*funcval)(f.data), nret, fint, ot) {
            throw("runtime.SetFinalizer: finalizer already set")
        }
    })
}

There is nothing complicated in the logic here, but the difficulty in comparing parameters, types, etc.

2.1.3. removefinalizer

Through removespecial, find the special object corresponding to data object p, if found, release the corresponding memory on mheap

func removefinalizer(p unsafe.Pointer) {
    // Find the corresponding special object based on data p
    s := (*specialfinalizer)(unsafe.Pointer(removespecial(p, _KindSpecialFinalizer)))
    if s == nil {
        return // there wasn't a finalizer to remove
    }
    lock(&mheap_.speciallock)
    // Release the memory corresponding to the special s found
    mheap_.specialfinalizeralloc.free(unsafe.Pointer(s))
    unlock(&mheap_.speciallock)
}

The function here, although called removefinalizer, has nothing to do with the finalizer structure for the time being, it is all dealing with the special structure, and so is the addfinalizer after it.

2.1.4. removespecial

Traverses through the specials of the span in which the data resides, removes the specials from the specified data p, and returns

func removespecial(p unsafe.Pointer, kind uint8) *special {
    // Find the span where data p is located
    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()
    // Make sure the space is cleaned
    span.ensureSwept()
    // Get the offset of the data P and find the corresponding special p according to the offset
    offset := uintptr(p) - span.base()

    lock(&span.speciallock)
    t := &span.specials
    // Traverse the span.specials list
    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 {
            // Found, modify pointer, remove currently found Specials
            *t = s.next
            unlock(&span.speciallock)
            releasem(mp)
            return s
        }
        t = &s.next
    }
    unlock(&span.speciallock)
    releasem(mp)
    // If not found, return to nil
    return nil
}

2.1.5. addfinalizer

As opposed to removefinalizer, this is to create a corresponding special based on the data object p and add it to the span.specials list

func addfinalizer(p unsafe.Pointer, f *funcval, nret uintptr, fint *_type, ot *ptrtype) bool {
    lock(&mheap_.speciallock)
    // Allocate a block of memory for finalizer to use
    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
    // Not added successfully because p already has a special object
    lock(&mheap_.speciallock)
    mheap_.specialfinalizeralloc.free(unsafe.Pointer(s))
    unlock(&mheap_.speciallock)
    return false
}

2.1.6. addspecial

Here is the main logic for adding Specials

func addspecial(p unsafe.Pointer, s *special) bool {
    span := spanOfHeap(uintptr(p))
    if span == nil {
        throw("addspecial on invalid pointer")
    }
    // As with removerspecial, make sure the space has been cleaned
    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 {
            // Already exists, can't add, a data object, can only bind a 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.
    // Add to end of specials queue
    s.offset = uint16(offset)
    s.next = *t
    *t = s
    unlock(&span.speciallock)
    releasem(mp)

    return true
}

2.1.7. createfing

This function guarantees that after the finalizer is created, a goroutine runs, which runs only once and is recorded by the global variable fing

func createfing() {
    // start the finalizer goroutine exactly once
    // Go ahead and create a goroutine to monitor the operation at all times
    if fingCreate == 0 && atomic.Cas(&fingCreate, 0, 1) {
        // Open a goroutine run
        go runfinq()
    }
}

2.2. Execute finalizer

createfing above tries to create a goroutine to execute, so let's analyze the execution process

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

    for {
        lock(&finlock)
        // Get finq global queue and empty global queue
        fb := finq
        finq = nil
        if fb == nil {
            // If the global queue is empty, sleep on the current g and wait for wakeup
            gp := getg()
            fing = gp
            // Set fing's status flag bit
            fingwait = true
            goparkunlock(&finlock, waitReasonFinalizerWait, traceEvGoBlock, 1)
            continue
        }
        unlock(&finlock)
        // Loop through fin arrays in runq chains
        for fb != nil {
            for i := fb.cnt; i > 0; i-- {
                f := &fb.fin[i-1]
                // Gets the size of the returned data that stores the current finalizer and, if larger than before, allocates it
                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.
                // Clear frame memory storage
                *(*[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")
                }
                // Call finalizer function
                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.
                // Empty finalizer properties
                f.fn = nil
                f.arg = nil
                f.ot = nil
                atomic.Store(&fb.cnt, i-1)
            }
            // Put completed finalizer s in finc for caching to avoid reallocating memory
            next := fb.next
            lock(&finlock)
            fb.next = finc
            finc = fb
            unlock(&finlock)
            fb = next
        }
    }
}

When I finished the above process, I suddenly found a little confused

  1. When was the data finalizer inserted in the global queue finq?
  2. g If I sleep, how can I be awakened?

Start with the first question:

Inserting the queue goes back to the GC we analyzed earlier Understanding Go-Garbage Recycling Yes, there is a function below in 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
            // The entry to the global finq queue is here
            freespecial(y, unsafe.Pointer(p), size)
        }
        ....
    }
    ....
}

2.2.2. freespecial

When gc, not only do you need to free the memory for the specials, but you also clean up the specials to create the corresponding dinalizer object and insert it into the finq queue

func freespecial(s *special, p unsafe.Pointer, size uintptr) {
    switch s.kind {
    case _KindSpecialFinalizer:
        // Join this finalizer to the global queue
        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)
    // The following two cases are not within the scope of analysis, omitted
    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)
    // If finq is empty or finq's internal array is full, get the block from finc or reassign and insert it into finq's chain header
    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)]
                }
            }
        }
        // Remove and get the chain header from finc
        block := finc
        finc = block.next
        // Mount the list of chains obtained from finc to the queue header of finq, which points to the new block
        block.next = finq
        finq = block
    }
    // Get the block corresponding to the index from finq.cnt
    f := &finq.fin[finq.cnt]
    atomic.Xadd(&finq.cnt, +1) // Sync with markroots
    // Set related properties
    f.fn = fn
    f.nret = nret
    f.fint = fint
    f.ot = ot
    f.arg = p
    // Set Wake-up Flag
    fingwake = true
    unlock(&finlock)
}

Now you can see how runq global queues are populated

So, the second question, how can fing wake up when it is dormant?

It goes back here, Deeply Understanding the Implementation of Go-goroutine and Scheduler Analysis This article

2.2.4. findrunnable

There is a piece of code in findrunnable as follows:

func findrunnable() (gp *g, inheritTime bool) {
    // wakefing to wake fing and return fing
    if fingwait && fingwake {
        if gp := wakefing(); gp != nil {
            // Wake up g and continue from hibernation
            ready(gp, 0, true)
        }
    }
}

2.2.5. wakefing

Not only will the state bit fingwait fingwake be judged twice here, but if it meets the wake-up requirements, the two state bits need to be reset

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

3. Reference Documents

  • Go Language Learning Notes--Rain Tracks

Topics: Go