Глубокое понимание принципа анализа Go-defer

Go

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

Поскольку отложенная тревога привязана к работающей g, вот атрибуты, связанные с отложенной тревогой в g.

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

В соответствии с функцией, определенной после ключевого слова deferfnи размер параметра для создания отложенной функции выполнения, и повесить эту отложенную функцию на текущую 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
}

Получить сайзкласс по размеру, классифицировать и кешировать сайзкласс, в этом и заключается идея выделения памяти

Сначала перейдите к p, чтобы выделить, а затем получить локальный кеш из глобального планирования в пакетах, Идея этого кеша второго уровня действительно повсюду в различных частях исходного кода 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

Функция, используемая для освобождения отсрочки, должна быть такой же, как планировщик и выделение памяти.

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 и анализа планировщика,Глубокое понимание принципов 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 и выполните recovery().

    В это время он восстановит панику2, вызваннуюpanicPANIC2.Recovered = true, следует продолжить обработку следующей PANIC по списку G._Panic Link, но мы можем найтиpanic1Он был выполнен, что является логикой следующего кода, удалите панику, которая была выполнена

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

Логику паники можно разобрать:

Когда программа сталкивается с паникой, она не будет продолжать выполняться, сначала поместите текущуюpanicподняться наg._panicВ связанном списке начните обход текущего gg._deferсвязанный список, затем выполнить_deferФункция, определенная объектом, и т. д. Если функция отсрочки снова сработает во время вызывающего процесса, она будет выполнена снова.gopanicНаконец, функция печатает всю информацию о тревоге в цикле и выходит из текущего g. Однако если во время вызова для отсрочки встречается восстановление, планирование продолжается (mcall(recovery)).

2.4.1. recovery

Возобновить панический 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 и помещает текущую панику в связанный список g._panic
  4. L14: Из-за паники1 извлеките из заголовка связанного списка g._defer отложенный2 и начните выполнение
  5. L12: выполнить defer2, еще одну команду panic, и смонтировать ее в связанный список g._panic.
  6. L12: из-за panic2 извлеките defer2 из заголовка связанного списка g._defer и обнаружите, что defer2 был выполнен и перемещен из связанного списка, а defer2 запускается с помощью panic1, пропустите defer2 и прервите panic1.
  7. L12: Продолжайте извлекать следующий связанный список g._defer для defer1.
  8. L5: defer1 выполняет восстановление, восстанавливает панику2, удаляет связанный список, определяет следующую панику, а именно панику1, паника1 была прервана отсрочкой2, удалить панику1
  9. defer1 закончен, удалите defer1

3. Сопутствующие документы

4. Справочная документация

  • "Перейти к изучению языка" - знаки дождя