深入理解Godefer的原理剖析

35次阅读

共计 9267 个字符,预计需要花费 24 分钟才能阅读完成。

defer 也是 Go 里面比较特别的一个关键字了,主要就是用来保证在程序执行过程中,defer 后面的函数都会被执行到,一般用来关闭连接、清理资源等。

1. 结构概览

1.1. defer

type _defer struct {
   siz     int32   // 参数的大小
   started bool    // 是否执行过了
   sp      uintptr // sp at time of defer
   pc      uintptr
   fn      *funcval 
   _panic  *_panic // defer 中的 panic
   link    *_defer // defer 链表,函数执行流程中的 defer,会通过 link 这个 属性进行串联
}

1.2. panic

type _panic struct {
   argp      unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
   arg       interface{}    // argument to panic
   link      *_panic        // link to earlier panic
   recovered bool           // whether this panic is over
   aborted   bool           // the panic was aborted
}

1.3. g

因为 defer panic 都是绑定在 运行的 g 上的,所以这里说明一下 g 中与 defer panic 相关的属性

type g struct {
   _panic         *_panic // panic 组成的链表
   _defer         *_defer // defer 组成的先进后出的链表,同栈
}

2. 源码分析

2.1. main

最开始,还是通过go tool 来分析一下,底层是通过什么函数来实现的吧

func main() {defer func() {recover()
    }()
    panic("error")
}

go build -gcflags=all=”-N -l” main.go

go tool objdump -s “main.main” main

▶ go tool objdump -s "main\.main" main | grep CALL
  main.go:4             0x4548d0                e81b00fdff              CALL runtime.deferproc(SB)              
  main.go:7             0x4548f2                e8b90cfdff              CALL runtime.gopanic(SB)                
  main.go:4             0x4548fa                e88108fdff              CALL runtime.deferreturn(SB)            
  main.go:3             0x454909                e85282ffff              CALL runtime.morestack_noctxt(SB)       
  main.go:5             0x4549a6                e8d511fdff              CALL runtime.gorecover(SB)              
  main.go:4             0x4549b5                e8a681ffff              CALL runtime.morestack_noctxt(SB)

综合反编译结果可以看出,defer 关键字首先会调用 runtime.deferproc 定义一个延迟调用对象,然后再函数结束前,调用 runtime.deferreturn 来完成 defer 定义的函数的调用

panic 函数就会调用 runtime.gopanic 来实现相关的逻辑

recover 则调用 runtime.gorecover 来实现 recover 的功能

2.2. deferproc

根据 defer 关键字后面定义的函数 fn 以及 参数的 size,来创建一个延迟执行的 函数,并将这个延迟函数,挂在到当前 g 的 _defer 的链表上

func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
   sp := getcallersp()
   argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
   callerpc := getcallerpc()
   // 获取一个_defer 对象,并放入 g._defer 链表的头部
   d := newdefer(siz)
     // 设置 defer 的 fn pc sp 等,后面调用
   d.fn = fn
   d.pc = callerpc
   d.sp = sp
   switch siz {
   case 0:
      // Do nothing.
   case sys.PtrSize:
      // _defer 后面的内存 存储 argp 的地址信息
      *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
   default:
      // 如果不是指针类型的参数,把参数拷贝到 _defer 的后面的内存空间
      memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
   }
   return0()}

这个函数看起来比较简答,通过 newproc 获取一个 _defer 的对象,并加入到当前 g 的 _defer 链表的头部,然后再把参数或参数的指针拷贝到 获取到的 _defer 对象的 后面的内存空间

2.2.1. newdefer

newdefer 的作用是获取一个 _defer 对象,并推入 g._defer链表的头部

func newdefer(siz int32) *_defer {
   var d *_defer
   // 根据 size 通过 deferclass 判断应该分配的 sizeclass,就类似于 内存分配预先确定好几个 sizeclass,然后根据 size 确定 sizeclass,找对应的缓存的内存块
   sc := deferclass(uintptr(siz))
   gp := getg()
   // 如果 sizeclass 在既定的 sizeclass 范围内,去 g 绑定的 p 上找
   if sc < uintptr(len(p{}.deferpool)) {pp := gp.m.p.ptr()
      if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
         // 当前 sizeclass 的缓存数量 ==0,且不为 nil,从 sched 上获取一批缓存
         systemstack(func() {lock(&sched.deferlock)
            for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {d := sched.deferpool[sc]
               sched.deferpool[sc] = d.link
               d.link = nil
               pp.deferpool[sc] = append(pp.deferpool[sc], d)
            }
            unlock(&sched.deferlock)
         })
      }
      // 如果从 sched 获取之后,sizeclass 对应的缓存不为空,分配
      if n := len(pp.deferpool[sc]); n > 0 {d = pp.deferpool[sc][n-1]
         pp.deferpool[sc][n-1] = nil
         pp.deferpool[sc] = pp.deferpool[sc][:n-1]
      }
   }
   // p 和 sched 都没有找到 或者 没有对应的 sizeclass,直接分配
   if d == nil {
      // Allocate new defer+args.
      systemstack(func() {total := roundupsize(totaldefersize(uintptr(siz)))
         d = (*_defer)(mallocgc(total, deferType, true))
      })
   }
   d.siz = siz
   // 插入到 g._defer 的链表头
   d.link = gp._defer
   gp._defer = d
   return d
}

根据 size 获取 sizeclass,对 sizeclass 进行分类缓存,这是内存分配时的思想

先去 p 上分配,然后批量从全局 sched 上获取到本地缓存,这种二级缓存的思想真的是遍布在 go 源码的各个部分啊

2.3. deferreturn

func deferreturn(arg0 uintptr) {gp := getg()
   // 获取 g defer 链表的第一个 defer,也是最后一个声明的 defer
   d := gp._defer
   // 没有 defer,就不需要干什么事了
   if d == nil {return}
   sp := getcallersp()
   // 如果 defer 的 sp 与 callersp 不匹配,说明 defer 不对应,有可能是调用了其他栈帧的延迟函数
   if d.sp != sp {return}
   // 根据 d.siz,把原先存储的参数信息获取并存储到 arg0 里面
   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
   // defer 用过了就释放了,gp._defer = d.link
   freedefer(d)
   // 跳转到执行 defer
   jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}

2.3.1.freedefer

释放 defer 用到的函数,应该跟调度器、内存分配的思想是一样的

func freedefer(d *_defer) {
   // 判断 defer 的 sizeclass
   sc := deferclass(uintptr(d.siz))
   // 超出既定的 sizeclass 范围的话,就是直接分配的内存,那就不管了
   if sc >= uintptr(len(p{}.deferpool)) {return}
   pp := getg().m.p.ptr()
   // p 本地 sizeclass 对应的缓冲区满了,批量转移一半到全局 sched
   if len(pp.deferpool[sc]) == cap(pp.deferpool[sc]) {
      // 使用 g0 来转移
      systemstack(func() {
         var first, last *_defer
         for len(pp.deferpool[sc]) > cap(pp.deferpool[sc])/2 {n := len(pp.deferpool[sc])
            d := pp.deferpool[sc][n-1]
            pp.deferpool[sc][n-1] = nil
            pp.deferpool[sc] = pp.deferpool[sc][:n-1]
            // 先将需要转移的那批 defer 对象串成一个链表
            if first == nil {first = d} else {last.link = d}
            last = d
         }
         lock(&sched.deferlock)
         // 把这个链表放到 sched.deferpool 对应 sizeclass 的链表头
         last.link = sched.deferpool[sc]
         sched.deferpool[sc] = first
         unlock(&sched.deferlock)
      })
   }
   // 清空当前要释放的 defer 的属性
   d.siz = 0
   d.started = false
   d.sp = 0
   d.pc = 0
   d.link = nil

   pp.deferpool[sc] = append(pp.deferpool[sc], d)
}

二级缓存的思想,在 深入理解 Go-goroutine 的实现及 Scheduler 分析,深入理解 go-channel 和 select 的原理,深入理解 Go- 垃圾回收机制 已经分析过了,就不再过多分析了

2.4. gopanic

func gopanic(e interface{}) {gp := getg()

   var p _panic
   p.arg = e
   p.link = gp._panic
   gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

   atomic.Xadd(&runningPanicDefers, 1)
   // 依次执行 g._defer 链表的 defer 对象
   for {
      d := gp._defer
      if d == nil {break}

      // If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),
      // take defer off list. The earlier panic or Goexit will not continue running.
      // 正常情况下,defer 执行完成之后都会被移除,既然这个 defer 没有移除,原因只有两种:1. 这个 defer 里面引发了 panic 2. 这个 defer 里面引发了 runtime.Goexit,但是这个 defer 已经执行过了,需要移除,如果引发这个 defer 没有被移除是第一个原因,那么这个 panic 也需要移除,因为这个 panic 也执行过了,这里给 panic 增加标志位,以待后续移除
      if d.started {
         if d._panic != nil {d._panic.aborted = true}
         d._panic = nil
         d.fn = nil
         gp._defer = d.link
         freedefer(d)
         continue
      }
      d.started = true

      // Record the panic that is running the defer.
      // If there is a new panic during the deferred call, that panic
      // will find d in the list and will mark d._panic (this panic) aborted.
      // 把当前的 panic 绑定到这个 defer 上面,defer 里面有可能 panic,这种情况下就会进入到 上面 d.started 的逻辑里面,然后把当前的 panic 终止掉,因为已经执行过了 
      d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
      // 执行 defer.fn
      p.argp = unsafe.Pointer(getargp(0))
      reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
      p.argp = nil

      // reflectcall did not panic. Remove d.
      if gp._defer != d {throw("bad defer entry in panic")
      }
      // 解决 defer 与 panic 的绑定关系,因为 defer 函数已经执行完了,如果有 panic 或 Goexit 就不会执行到这里了
      d._panic = nil
      d.fn = nil
      gp._defer = d.link

      // trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic
      //GC()

      pc := d.pc
      sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
      freedefer(d)
      // panic 被 recover 了,就不需要继续 panic 了,继续执行剩余的代码
      if p.recovered {atomic.Xadd(&runningPanicDefers, -1)

         gp._panic = p.link
         // Aborted panics are marked but remain on the g.panic list.
         // Remove them from the list.
         // 从 panic 链表中移除 aborted 的 panic,下面解释
         for gp._panic != nil && gp._panic.aborted {gp._panic = gp._panic.link}
         if gp._panic == nil { // must be done with signal
            gp.sig = 0
         }
         // Pass information about recovering frame to recovery.
         gp.sigcode0 = uintptr(sp)
         gp.sigcode1 = pc
         // 调用 recovery,恢复当前 g 的调度执行
         mcall(recovery)
         throw("recovery failed") // mcall should not return
      }
   }
     // 打印 panic 信息
   preprintpanics(gp._panic)
     // panic
   fatalpanic(gp._panic) // should not return
   *(*int)(nil) = 0      // not reached
}

这里解释一下 gp._panic.aborted 的作用,以下面为例

func main() {defer func() { // defer1
      recover()}()
   panic1()}

func panic1() {defer func() {  // defer2
      panic("error1") // panic2
   }()
   panic("error")  // panic1
}
  1. 当执行到 panic("error")

    g._defer 链表:g._defer->defer2->defer1

    g._panic 链表:g._panic->panic1

  2. 当执行到 panic("error1")

    g._defer 链表:g._defer->defer2->defer1

    g._panic 链表:g._panic->panic2->panic1

  3. 继续执行到 defer1 函数内部,进行 recover()

    此时会去恢复 panic2 引起的 panic,panic2.recovered = true,应该顺着 g._panic 链表继续处理下一个 panic 了,但是我们可以发现 panic1 已经执行过了,这也就是下面的代码的逻辑了,去掉已经执行过的 panic

    for gp._panic != nil && gp._panic.aborted {gp._panic = gp._panic.link}

panic 的逻辑可以梳理一下:

程序在遇到 panic 的时候,就不再继续执行下去了,先把当前 panic 挂载到 g._panic 链表上,开始遍历当前 g 的g._defer 链表,然后执行 _defer 对象定义的函数等,如果 defer 函数在调用过程中又发生了 panic,则又执行到了 gopanic函数,最后,循环打印所有 panic 的信息,并退出当前 g。然而,如果调用 defer 的过程中,遇到了 recover,则继续进行调度(mcall(recovery))。

2.4.1. recovery

恢复一个被 panic 的 g,重新进入并继续执行调度

func recovery(gp *g) {
   // Info about defer passed in G struct.
   sp := gp.sigcode0
   pc := gp.sigcode1
   // Make the deferproc for this d return again,
   // this time returning 1.  The calling function will
   // jump to the standard return epilogue.
   // 记录 defer 返回的 sp pc
   gp.sched.sp = sp
   gp.sched.pc = pc
   gp.sched.lr = 0
   gp.sched.ret = 1
   // 重新恢复执行调度
   gogo(&gp.sched)
}

2.5. gorecover

gorecovery 仅仅只是设置了 g._panic.recovered 的标志位

func gorecover(argp uintptr) interface{} {gp := getg()
   p := gp._panic
   // 需要根据 argp 的地址,判断是否在 defer 函数中被调用
   if p != nil && !p.recovered && argp == uintptr(p.argp) {
      // 设置标志位,上面 gopanic 中会对这个标志位做判断
      p.recovered = true
      return p.arg
   }
   return nil
}

2.6. goexit

我们还忽略了一个点,当我们手动调用 runtime.Goexit() 退出的时候,defer 函数也会执行,我们分析一下这种情况

func Goexit() {
    // Run all deferred functions for the current goroutine.
    // This code is similar to gopanic, see that implementation
    // for detailed comments.
    gp := getg()
  // 遍历 defer 链表
    for {
        d := gp._defer
        if d == nil {break}
    // 如果 defer 已经执行过了,与 defer 绑定的 panic 终止掉
        if d.started {
            if d._panic != nil {
                d._panic.aborted = true
                d._panic = nil
            }
            d.fn = nil
      // 从 defer 链表中移除
            gp._defer = d.link
      // 释放 defer
            freedefer(d)
            continue
        }
    // 调用 defer 内部函数
        d.started = true
        reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
        if gp._defer != d {throw("bad defer entry in Goexit")
        }
        d._panic = nil
        d.fn = nil
        gp._defer = d.link
        freedefer(d)
        // Note: we ignore recovers here because Goexit isn't a panic
    }
  // 调用 goexit0,清除当前 g 的属性,重新进入调度
    goexit1()}

2.7. 图示解析

源码这一块阅读起来难度并不是很大,如果还有什么疑惑,希望下面的一副动图能解开你的疑惑

作图作的略拙劣,见谅

步骤解析:

  1. L3: 生成一个 defer1,放到 g._defer 链表上
  2. L11: 生成一个 defer2,挂载到 g._defer 链表上
  3. L14: panic1 调用 gopanic,将当前 panic 放到 g._panic 链表上
  4. L14: 因为 panic1,从 g._defer 链表头部提取到 defer2,开始执行
  5. L12: 执行 defer2,又一个 panic,挂载到 g._panic 链表上
  6. L12: 因为 panic2,从 g._defer 链表头部提取到 defer2,发现 defer2 已经执行过了移出链表,,且 defer2 是因为 panic1 而触发的,跳过 defer2,并 abort panic1
  7. L12: 继续提取 g._defer 链表的下一个,提取到 defer1
  8. L5: defer1 执行 recover,recover 掉 panic2,移除链表,判断下一个 panic,即 panic1,panic1 已经被 defer2 aborted 掉了,移除 panic1
  9. defer1 执行完了,移除 defer1

3. 关联文档

  • 二级缓存,sizeclass: 深入理解 Go- 垃圾回收机制
  • gogo goexit0 调度: 深入理解 Go-goroutine 的实现及 Scheduler 分析

4. 参考文档

  • 《Go 语言学习笔记》– 雨痕

正文完
 0