golang panic和recover 实现原理

2023年1月31日11:25:37

这篇文章是系列文章中的第二篇,系列文章主要包括:

  1. golang defer的原理
  2. golang panic和recover()函数的原理(包括golang对于错误处理方式)
  3. defer性能损耗的讨论以及最重要的应用场景
  4. defer在golang 1.13 上的性能

panic 能中断一个程序的执行,同时也能在一定情况下进行恢复(recover)。我们就来看一看 panic 和 recover 这对关键字的实现机制。根据我们对 Go 的实践,可以预见的是,他们的实现跟runtime调度器和 defer 关键字也紧密相关。

思考

1.为什么go 进程会终止

func main() {
	panic("sim lou.")
}

输出结果是:

panic: sim lou.

goroutine 1 [running]:
main.main()
	/Users/ytlou/Desktop/golang/golang_study/study/basic/panic/panic_test1.go:4 +0x39
Process finished with exit code 2

这里思考一下,为什么执行 panic 后会导致应用程序运行中止?或者说执行panic为什么导致进程终止了?

2. 为什么不会中止运行

func main() {
	defer func() {
		if err := recover(); err != nil {
			log.Printf("recover: %v", err)
		}
	}()
	
	panic("sim lou.")
}

输出结果是:

2019/10/26 22:19:33 recover: sim lou.

Process finished with exit code 0

思考一下为什么加上 defer + recover 组合就可以保护应用程序不会退出。

3.不设置 defer 行不

上面问题二是 defer + recover 组合,那我去掉 defer 是不是也可以呢?如下:

func main() {
	if err := recover(); err != nil {
		log.Printf("recover: %v", err)
	}
	panic("sim lou.")
}

运行结果:

panic: sim lou.

goroutine 1 [running]:
main.main()
	/Users/ytlou/Desktop/golang/golang_study/study/basic/panic/panic_test3.go:9 +0xa1

Process finished with exit code 2

不行!!!我们常说 defer + recover 组合 “万能” 捕获。但是为什么呢。去掉 defer 后为什么就无法捕获了?

思考一下,为什么需要设置 defer 后 recover 才能起作用?

同时你还需要仔细想想,我们设置 defer + recover 组合后就能无忧无虑了吗,各种 “乱” 写了吗?

4. 为什么起个 goroutine 就不行

func main() {
	go func() {
		defer func() {
			if err := recover(); err != nil {
				log.Printf("recover: %v", err)
			}
		}()
	}()
	panic("qwertyuiop.")
}

输出结果:

panic: qwertyuiop.

goroutine 1 [running]:
main.main()
	/Users/ytlou/Desktop/golang/golang_study/study/basic/panic/panic_test4.go:13 +0x51

请思考一下,为什么新起了一个 Goroutine 就无法捕获到异常了?到底发生了什么事…

但是我们改一下:

func main() {
	go func() {
		defer func() {
			if err := recover(); err != nil {
				log.Printf("recover: %v", err)
			}
		}()
		panic("aim lou 2.")
	}()
	time.Sleep(1* time.Second)
}

输出是:

2019/10/26 22:27:50 recover: aim lou 2.

为什么我们把panic放到自定义的协程里面recover又可以work了呢?

基于前面的四个问题,我们阅读源码,从源码找到root cause.

数据结构

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
}

在 panic 中是使用 _panic 作为其基础单元的,每执行一次 panic 语句,都会创建一个 _panic 对象。它包含了一些基础的字段用于存储当前的 panic 调用情况,涉及的字段如下

  • argp:指向 defer 延迟调用的参数的指针
  • arg:panic 的原因,也就是调用 panic 时传入的参数
  • link:指向上一个调用的 _panic,这里说明panic也是一个链表
  • recovered:panic 是否已经被处理过,也就是是否被 recover
  • aborted:panic 是否被中止

通过查看 link 字段,可得知其是一个链表的数据结构,如下图:

+-----------+          +-----------+          +-----------+
|  _panic   |     +---->  _panic   |    +----->  _panic   |
+-----------+     |    +-----------+    |     +-----------+
|  ......   |     |    |  ......   |    |     |  ......   |
+-----------+     |    +-----------+    |     +-----------+
|   link    |-----+    |   link    +----+     |   link    |
+-----------+          +-----------+          +-----------+
|  ......   |          |  ......   |          |  ......   |
+-----------+          +-----------+          +-----------+

panic

我们先看看panic生成的汇编代码:

func main() {
	panic("sim lou.")
}

汇编代码:

"".main STEXT size=65 args=0x0 locals=0x18
	0x0000 00000 (panic_test1.go:3)	TEXT	"".main(SB), ABIInternal, $24-0
......
	0x002f 00047 (panic_test1.go:4)	PCDATA	$0, $0
	0x002f 00047 (panic_test1.go:4)	MOVQ	AX, 8(SP)
	0x0034 00052 (panic_test1.go:4)	CALL	runtime.gopanic(SB)
.......

可以看到 panic 翻译成汇编代码主要是调用了 runtime.gopanic,我们一起来看看这个方法做了什么事,如下(省略部分)

func gopanic(e interface{}) {
	gp := getg()
	......
	var p _panic
	p.arg = e
	// 头插法
	p.link = gp._panic
	gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

	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.
		if d.started {
			if d._panic != nil {
				d._panic.aborted = true
			}
			d._panic = nil
			d.fn = nil
			gp._defer = d.link
			freedefer(d)
			continue
		}

		// Mark defer as started, but keep on list, so that traceback
		// can find and update the defer's argument frame if stack growth
		// or a garbage collection happens before reflectcall starts executing d.fn.
		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.
		d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

		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")
		}
		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)
		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.
			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
			mcall(recovery)
			throw("recovery failed") // mcall should not return
		}
	}

	preprintpanics(gp._panic)

	fatalpanic(gp._panic) // should not return
	*(*int)(nil) = 0      // not reached
}
  • 获取指向当前 Goroutine 的指针
  • 初始化一个 panic 的基本单位 _panic,并将这个panic头插入当前goroutine的panic链表中。
  • 获取当前 Goroutine 上挂载的 _defer(数据结构也是链表)
  • 若当前存在 defer 调用,则调用 reflectcall 方法去执行先前 defer 中延迟执行的代码。reflectcall方法若在执行过程中需要运行 recover 将会调用 gorecover 方法。
  • 结束前,使用 preprintpanics 方法打印出所涉及的 panic 消息
  • 最后调用 fatalpanic 中止应用程序,实际是执行 exit(2) 进行最终退出行为的。

通过对上述代码的执行分析,可得知 panic 方法实际上就是处理当前 Goroutine(g) 上所挂载的 ._panic 链表(所以无法对其他 Goroutine 的异常事件响应),然后对其所属的 defer 链表和 recover 进行检测并处理,最后调用退出命令中止应用程序。

恢复 recover panic

func main() {
	defer func() {
		if err := recover(); err != nil {
			log.Printf("recover: %v", err)
		}
	}()
	
	panic("sim lou.")
}

输出结果:

2019/10/27 12:39:30 recover: sim lou.

Process finished with exit code 0

我们看汇编代码,panic是怎么被recover的:

"".main STEXT size=118 args=0x0 locals=0x50
	......
	0x003a 00058 (panic_test2.go:6)	CALL	runtime.deferprocStack(SB)
	......
	0x005a 00090 (panic_test2.go:12)	CALL	runtime.gopanic(SB)
	......
	0x0060 00096 (panic_test2.go:6)	CALL	runtime.deferreturn(SB)
	......
"".main.func1 STEXT size=151 args=0x0 locals=0x40
	0x0000 00000 (panic_test2.go:6)	TEXT	"".main.func1(SB), ABIInternal, $64-0
	......
	0x0026 00038 (panic_test2.go:7)	CALL	runtime.gorecover(SB)
	......
	0x0092 00146 (panic_test2.go:6)	JMP	0

通过分析底层调用,可得知主要是如下几个方法:

  • runtime.deferprocStack
  • runtime.gopanic
  • runtime.deferreturn
  • runtime.gorecover

前面我们说了简单的流程,gopanic 方法会遍历调用当前 Goroutine 下的 defer 链表,若 reflectcall 执行中遇到 recover 就会调用 gorecover 进行处理,该方法代码如下:

func gorecover(argp uintptr) interface{} {
	// Must be in a function running as part of a deferred call during the panic.
	// Must be called from the topmost function of the call
	// (the function used in the defer statement).
	// p.argp is the argument pointer of that topmost deferred function call.
	// Compare against argp reported by caller.
	// If they match, the caller is the one who can recover.
	gp := getg()
	p := gp._panic
	if p != nil && !p.recovered && argp == uintptr(p.argp) {
		p.recovered = true
		return p.arg
	}
	return nil
}

这代码,看上去挺简单的,核心就是修改 recovered 字段。该字段是用于标识当前 panic 是否已经被 recover 处理。但是这和我们想象的并不一样啊,程序是怎么从 panic 流转回去的呢?是不是在核心方法里处理了呢?我们再看看 gopanic 的代码,如下:

func gopanic(e interface{}) {
    ...
    for {
        // defer...
        ...
        pc := d.pc
        sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
        freedefer(d)

        // recover...
        if p.recovered {
            atomic.Xadd(&runningPanicDefers, -1)

            gp._panic = p.link
            for gp._panic != nil && gp._panic.aborted {
                gp._panic = gp._panic.link
            }
            if gp._panic == nil { 
                gp.sig = 0
            }

            gp.sigcode0 = uintptr(sp)
            gp.sigcode1 = pc
            mcall(recovery)
            throw("recovery failed") 
        }
    }
    ...
}

我们回到 gopanic 方法中再仔细看看,发现实际上是包含对 recover 流转的处理代码的。恢复流程如下:

  • 判断当前 _panic 中的 recover 是否已标注为处理
  • 从 _panic 链表中删除已标注中止的 panic 事件,也就是删除已经被恢复的 panic 事件
  • 将相关需要恢复的栈帧信息传递给 recovery 方法的 gp 参数(每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量)
  • 执行 recovery 进行恢复动作
  • 从流程来看,最核心的是 recovery 方法。它承担了异常流转控制的职责。代码如下:
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)
}

粗略一看,似乎就是很简单的设置了一些值?但实际上设置的是编译器中伪寄存器的值,常常被用于维护上下文等。在这里我们需要结合 gopanic 方法一同观察 recovery 方法。它所使用的栈指针 sp 和程序计数器 pc 是由当前 defer 在调用流程中的 deferproc 传递下来的,因此实际上最后是通过 gogo 方法跳回了 deferproc 方法。另外我们注意到:

gp.sched.ret = 1

在底层中程序将 gp.sched.ret 设置为了 1,也就是没有实际调用 deferproc 方法,直接修改了其返回值。意味着默认它已经处理完成。直接转移到 deferproc 方法的下一条指令去。至此为止,异常状态的流转控制就已经结束了。接下来就是继续走 defer 的流程了.

panic 抛出

当然如果所有的 defer 都没有指明显式的 recover,那么这时候则直接在运行时抛出 panic 信息:

// 消耗完所有的 defer 调用,保守地进行 panic
// 因为在冻结之后调用任意用户代码是不安全的,所以我们调用 preprintpanics 来调用
// 所有必要的 Error 和 String 方法来在 startpanic 之前准备 panic 字符串。
preprintpanics(gp._panic)

fatalpanic(gp._panic) // 不应该返回
*(*int)(nil) = 0      // 无法触及

总结:

从 panic 和 recover 这对关键字的实现上可以看出,可恢复的 panic 必须要 recover 的配合。 而且,这个 recover 必须位于同一 goroutine 的直接调用链上(例如,如果 A 依次调用了 B 和 C,而 B 包含了 recover,而 C 发生了 panic,则这时 B 的 panic 无法恢复 C 的 panic; 又例如 A 调用了 B 而 B 又调用了 C,那么 C 发生 panic 时,如果 A 要求了 recover 则仍然可以恢复), 否则无法对 panic 进行恢复。

当一个 panic 被恢复后,调度并因此中断,会重新进入调度循环,进而继续执行 recover 后面的代码, 包括比 recover 更早的 defer(因为已经执行过得 defer 已经被释放,而尚未执行的 defer 仍在 goroutine 的 defer 链表中), 或者 recover 所在函数的调用方。

  • 作者:惜暮
  • 原文链接:https://louyuting.blog.csdn.net/article/details/102761955
    更新时间:2023年1月31日11:25:37 ,共 8474 字。