深度解析 Golang Panic 的細節

一、介紹

當數組越界、訪問非法空間或者主動調用 panic 時,panic 會停掉當前正在執行的程序,包括所有協程,比起 exit 直接退出,panic 的退出更有秩序,它會先處理完當前 goroutine 已經 defer 掛上去的任務,執行完畢後再退出整個程序。

二、執行 panic 底層到底發生什麼?

接下來我們通過一個案例彙編執行得出 panic 底層執行哪些操作,如下:

func main() {
  panic("hello panic")
}

執行輸出:

➜  go tool compile -S main.go
"".main STEXT size=66 args=0x0 locals=0x18
        0x0000 00000 (main.go:108)      TEXT    "".main(SB), ABIInternal, $24-0
        0x0000 00000 (main.go:108)      MOVQ    (TLS), CX
        0x0009 00009 (main.go:108)      CMPQ    SP, 16(CX)
        ...
        0x002f 00047 (main.go:120)      PCDATA  $2, $0
        0x002f 00047 (main.go:120)      MOVQ    AX, 8(SP)
        0x0034 00052 (main.go:120)      CALL    runtime.gopanic(SB)
        0x0039 00057 (main.go:120) 
              ...
        0x0024 00036 (main.go:111)      LEAQ    "".main.func1·f(SB), AX
        0x002b 00043 (main.go:111)      PCDATA  $2, $0
        0x002b 00043 (main.go:111)      MOVQ    AX, 8(SP)
        0x0030 00048 (main.go:111)      CALL    runtime.deferproc(SB)
        0x0035 00053 (main.go:111)      TESTL   AX, AX
        ...
        0x004b 00075 (main.go:116)      MOVQ    AX, 8(SP)
        0x0050 00080 (main.go:116)      CALL    runtime.gopanic(SB)
        0x0055 00085 (main.go:116)      UNDEF
        0x0057 00087 (main.go:111)      XCHGL   AX, AX
        0x0058 00088 (main.go:111)      CALL    runtime.deferreturn(SB)
        0x005d 00093 (main.go:111)      MOVQ    16(SP), BP
       ...
        0x0026 00038 (main.go:112)      MOVQ    AX, (SP)
        0x002a 00042 (main.go:112)      CALL    runtime.gorecover(SB)
        ... 
        0x0072 00114 ($GOROOT/src/fmt/print.go:208)     LEAQ    go.string."recovered:%v\n"(SB), AX
        ...
        0x00a3 00163 ($GOROOT/src/fmt/print.go:208)     CALL    fmt.Fprintf(SB)

2.1 panic 結構

type _panic struct {
  argp      unsafe.Pointer  //一個指針,指向defer調用的參數的指針
  arg       interface{}   //panic傳入的參數
  link      *_panic   //是一個鏈表結構,指向上一個調用的_panic
  recovered bool  //是否已被recover 恢復
  aborted   bool   //panic是否被強行中止
}

說明:

再看一下 goroutine 的兩個重要字段:

type g struct {
    // ...
    _panic         *_panic // panic 鏈表,這是最裏的一個
    _defer         *_defer // defer 鏈表,這是最裏的一個;
    // ...
}

從這裏我們看出:_defer 和 _panic 鏈表都是掛在 goroutine 之上的

通過彙編得知 panic 的底層實現是 gopanic 的函數,位於 runtime/panic.go 文件。panic 機制最重要的就是 gopanic 函數了,所有的 panic 細節盡在此。爲什麼 panic 會顯得晦澀,主要有兩個點:

  1. 嵌套 panic 的時候,gopanic 會有遞歸執行的場景

  2. 程序指令跳轉並不是常規的函數壓棧,彈棧,在 recovery 的時候,是直接修改指令寄存器的結構體,從而直接越過了 gopanic 後面的邏輯,甚至是多層 gopanic 遞歸的邏輯

2.2 gopanic 實現

// The implementation of the predeclared function panic.
func gopanic(e interface{}) {
  gp := getg() //獲取指向當前goroutine的指針
  ...
  //初始化一個panic的基本單元_panic
  var p _panic
  p.arg = e
   // 把當前最新的 _panic 掛到鏈表最前面
  p.link = gp._panic
  gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
  //遍歷當前goroutinue的defer鏈表
  for {
    //獲取當前goroutinue上掛的defer
    d := gp._defer
    if d == nil {
      break
    }
    //下面是一些對此defer的判斷和處理,是否開放,是否可執行等
    ...
    if d.started {
      if d._panic != nil {
        d._panic.aborted = true
      }
      d._panic = nil
      if !d.openDefer {
        // For open-coded defers, we need to process the
        // defer again, in case there are any other defers
        // to call in the frame (not including the defer
        // call that caused the panic).
        d.fn = nil
        gp._defer = d.link
        freedefer(d)
        continue
      }
    }
    ...
    d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
    //當前goroutinue若存在有效的defer調用,
    //就會調下面的reflectcall方法來處理defer後面的操作,
    //當然,若defer裏面進行了recover處理,則會調用gorecover函數,
    p.argp = unsafe.Pointer(getargp(0))
    reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
    }
    p.argp = nil
    //在pc、sp中記錄當前defer的pc、sp
    pc := d.pc
    sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
    ...
    //已經有recover被調用
    if p.recovered {
         // 摘掉當前的 _panic
         gp._panic = p.link
         // 如果前面還有 panic,並且是標記了 aborted 的,那麼也摘掉;
         for gp._panic != nil && gp._panic.aborted {
             gp._panic = gp._panic.link
         }
         // panic 的流程到此爲止,恢復到業務函數堆棧上執行代碼;
         gp.sigcode0 = uintptr(sp)
         gp.sigcode1 = pc
         // 注意:恢復的時候 panic 函數將從此處跳出,本 gopanic 調用結束,後面的代碼永遠都不會執行。
         mcall(recovery)
         throw("recovery failed") // mcall should not return
    }    
  }
  //準備panic程序退出時要打印的參數
  preprintpanics(gp._panic)
  //遞歸打印所有panic中的參數信息,並執行exit(2)來退出程序
  fatalpanic(gp._panic) // should not return
  *(*int)(nil) = 0      // not reached
}

上面邏輯可以拆分爲循環內和循環外兩部分去理解:

  1. 遍歷 goroutine 的 defer 鏈表,獲取到一個 _defer 延遲函數;

  2. 獲取到 _defer 延遲函數,設置標識 d.started,綁定當前 d._panic(用以在遞歸的時候判斷);

  3. 執行 _defer 延遲函數;

  4. 摘掉執行完的 _defer 函數;

  5. 判斷 _panic.recovered 是否設置爲 true,進行相應操作;

  6. 如果是 true 那麼重置 pc,sp 寄存器(一般從 deferreturn 指令前開始執行),goroutine 投遞到調度隊列,等待執行;

  7. 重複以上步驟;

調用 gopanic 函數,接着遍歷當前 goroutinue 上掛載的 defer 鏈表,在執行 reflectcall 時碰到 recover,就執行 gorecover 函數處理,接下來看下 gorecover。

2.3 gorecover 實現以及如何恢復 panic

func gorecover(argp uintptr) interface{} {
  gp := getg()
  p := gp._panic
  if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
    p.recovered = true
    return p.arg
  }
  return nil
}

通過如上代碼,得知 gorecover 得知當前 goroutinue 是否在 panic 的流程中,在的話就將 panic 的 recovered 字段置微爲 true,否則直接返回 nil,所以說 recover 在普通的流程被調用時時沒有任何作用的。

gorecover 函數僅僅改了 recovered 標記,那麼 gouroutine 是怎麼從 panic 返回的呢?

func recovery(gp *g) {
  // Info about defer passed in G struct.
  sp := gp.sigcode0
  pc := gp.sigcode1
  // d's arguments need to be in the stack.
  if sp != 0 && (sp < gp.stack.lo || gp.stack.hi < sp) {
    print("recover: ", hex(sp), " not in [", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "]\n")
    throw("bad recovery")
  }
  // Make the deferproc for this d return again,
  // this time returning 1. The calling function will
  // jump to the standard return epilogue.
  gp.sched.sp = sp
  gp.sched.pc = pc
  gp.sched.lr = 0
  gp.sched.ret = 1
  //切換上下文
  gogo(&gp.sched)
}
func deferproc(siz int32, fn *funcval) {
    ...
  sp := getcallersp()
  argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
  callerpc := getcallerpc()
  //newdefer從_defer池中找是否有可複用的_defer基本單元,若沒有,就會malloc一個新的
  //並且將當前goroutinue的defer指向這個d,  新的defer總是會出現在最前面
  //這就是defer先入後出屬性的原因所在,
  d := newdefer(siz)
    ...
  d.fn = fn
  d.pc = callerpc
  d.sp = sp
  switch siz {
  case 0:
    // Do nothing.
  case sys.PtrSize:
    *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
  default:
    memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
  }
  return0()
}

三、Q&A

3.1 爲什麼 recover 一定要放在 defer 才生效?

因爲 _panic.recovered 字段的的修改時機,只能在 for 循環內的第三步執行 _defer 延遲函數。

3.2 爲什麼 recover 只能當前協程有效?

上面我們提到 gopanic 函數,裏面 for 循環有如下代碼:

    //獲取當前goroutinue上掛的defer
    d := gp._defer
    // 步驟:執行 defer 函數
    reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
    // 步驟:執行完成,把這個 defer 從鏈表裏摘掉;
    gp._defer = d.link

在 gopanic 裏,只遍歷執行當前 goroutine 上的 _defer 函數鏈條。所以,如果掛在其他 goroutine 的 defer 函數做了 recover ,那麼沒有絲毫用途。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/byouCtNulnjYDluDaO9wIw