go语言-深入defer特性-遍历调用[4]

395次阅读  |  发布于2年以前

「最傷人的是這一切的野蠻侵略、掠奪破壞都是以神的名義做的,而狂熱的人類只是玩具。」

正如之前在汇编代码中看到的,当函数正常结束时,其递归调用了runtime.deferreturn函数遍历defer链,并调用存储在defer中的函数。

func deferreturn(arg0 uintptr) {
    gp := getg()
    //当前协程defer函数链表
    d := gp._defer
    if d == nil {
        return
    }
    // 获取调用deferreturn函数时的栈顶位置
    sp := getcallersp()
    if d.sp != sp {
        return
    }
    // 内联defer调用规则
    if d.openDefer {
        done := runOpenDeferFrame(gp, d)
        gp._defer = d.link
        freedefer(d)
        return
    }
    //把保存在_defer对象中的fn函数需要用到的参数复制到栈上,准备调用fn
    switch d.siz {
    case 0:
        // Do nothing.
    case sys.PtrSize:
        *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
    default:
        memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
    }
    fn := d.fn
    d.fn = nil
    //使gp._defer指向下一个_defer结构体
    gp._defer = d.link
    //释放_defer结构体
    freedefer(d)
    // 调用fn函数
    jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}

在遍历defer链表的过程中,有两个重要的终止条件。一个是当遍历到链表的末尾时,最终链表指针变为nil,这时需要终止链表。除此之外,当defer结构中存储的SP地址与当前deferreturn的调用者SP地址不同时,仍然需要终止执行。原因是协程的链表中放入了当前函数调用链所有函数的defer结构,但是在执行时只能执行当前函数的defer结构。例如当前函数的执行链为a()→b()→c(),在执行函数c正常返回后,当前三个函数的defer结构都存储在链表中,但是当前只能够执行函数c中的fc函数。如果发现defer是其他函数的内容,则立即返回。

func a(){
    defer fa()
    b()  
}
func b(){
    defer fb()
    c()  
}
func c(){
    defer fc()
  ...  
}

deferreturn获取需要执行的defer函数,需要将当前defer函数的参数重新转移到栈中,调用freedefer销毁当前的结构体,并将链表指向下一个_defer结构体。现在有了函数指针,也有了参数,可以调用jmpdefer完成函数的调用。jmpdefer是一段汇编代码,其在amd64下的汇编代码如下所示。

Jmpdefer函数了比较巧妙的方式实现了对deferreturn函数反复调用。其核心思想是调整了deferreturn函数的SP、BP地址,使deferreturn函数退出之后再次调用deferreturn函数,从而实现循环调用。

TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
  // jmpdefer函数的第一个参数fn的地址放入DX寄存器
    MOVQ    fv+0(FP), DX
    // jmpdefer函数的第二个参数放入 BX 寄存器
    MOVQ    argp+8(FP), BX
    // 调整SP、BP位置
    LEAQ    -8(BX), SP  
    MOVQ    -8(SP), BP
    // 调整返回地址
    SUBQ    $5, (SP)
  // 继续执行deferreturn
    MOVQ    0(DX), BX
    JMP BX

上例执行SUBQ $5, (SP) 语句后,对应的栈帧结构如图所示。从图中可以看出,jmpdefer函数通过调整SP、BP寄存器值,已经抛弃了deferreturn的栈帧供后续使用。并且,由于调整了返回地址,jmpdefer函数在执行完毕返回时可以递归调用deferreturn函数,复用了栈空间,不会因为大量调用导致栈溢出。这种策略在协程调用循环中也会使用。

图 递归defer调用的栈帧结构

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8