18

Golang中panic与recover的实现原理

 3 years ago
source link: https://studygolang.com/articles/31290
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

今天我们讲讲golang中panic异常,以及recover对异常的捕获,由于panic、recover、defer之间非常亲密,所以今天就放在一起讲解,这里会涉及到一些defer的知识,有兴趣可以看我的另一篇关于defer的文章 Golang中defer的实现原理 .

Panic异常

Go的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、 空指针引用等。这些运行时错误会引起painc异常。

一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。

不是所有的panic异常都来自运行时,直接调用内置的panic函数也会引发panic异常

接下来,我们通过其汇编码尝试找出内置函数panic()的底层实现。

注意:我会把源码中每个方法的作用都注释出来,可以参考注释进行理解。

先编写一段简单的代码,并保存在panic.go文件中

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

然后使用以下命令编译代码:

go tool compile -S panic.go
0x0024 00036 (panic.go:10)      PCDATA  $2, $1
        0x0024 00036 (panic.go:10)      PCDATA  $0, $0
        0x0024 00036 (panic.go:10)      LEAQ    type.string(SB), AX
        0x002b 00043 (panic.go:10)      PCDATA  $2, $0
        0x002b 00043 (panic.go:10)      MOVQ    AX, (SP)
        0x002f 00047 (panic.go:10)      PCDATA  $2, $1
        0x002f 00047 (panic.go:10)      LEAQ    "".statictmp_0(SB), AX
        0x0036 00054 (panic.go:10)      PCDATA  $2, $0
        0x0036 00054 (panic.go:10)      MOVQ    AX, 8(SP)
        0x003b 00059 (panic.go:10)      CALL    runtime.gopanic(SB)

我们可以看到panic()函数调用被替换成了runtime.gopanic()函数

看函数之前,我们先来看一下panic的结构体

runtime\runtime2.go:_panic
type _panic struct {
    argp      unsafe.Pointer // 指向在panic下运行的defer的参数的指针
    arg       interface{}    // panic的参数
    link      *_panic        // 链接到更早的panic,新panic添加到表头
    recovered bool           // 该panic是否被recover
    aborted   bool           // 该panic是否强制退出
}

接着,我们再来分析runtime.gopanic()函数

runtime\panic.go
func gopanic(e interface{}) {
    //获取当前goroutine
    gp := getg()
    ...
    //生成一个新的panic结构
    var p _panic
    p.arg = e
    //指向更早的panic
    p.link = gp._panic
    //绑定到goroutine
    gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

    atomic.Xadd(&runningPanicDefers, 1)

    //循环goroutine中的defer链表
    for {
        d := gp._defer
        if d == nil {
            break
        }
        //如果defer已经被调用
        //如果该defer已经由较早的panic或者Goexit使用(表示引发了新的panic)
        //则从链表中去除这个panic,之前的panic或Goexit将不会继续运行。
        if d.started {
            if d._panic != nil {
                d._panic.aborted = true
            }
            d._panic = nil
            d.fn = nil
            gp._defer = d.link
            //释放该defer
            freedefer(d)
            //跳过循环,继续下一个defer
            continue
        }
        // 将defer标记已调用,但保留在列表中
        //这样 traceback 在栈增长或者 GC 的时候,能够找到并更新 defer 的参数栈帧
        // 并用 reflectcall 执行 d.fn
        d.started = true
        //记录在 defer 中发生的 panic
        //如果在 defer 的函数调用过程中又发生了新的 panic,那个 panic 会在链表中找到 d
        // 然后标记 d._panic(指向当前的 panic) 为 aborted 状态。
        d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

        p.argp = unsafe.Pointer(getargp(0))
        //执行defer后面的fn,如果有recover()函数会执行recover
        reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
        p.argp = nil

        // reflectcall 并没有 panic,移除 d
        if gp._defer != d {
            throw("bad defer entry in panic")
        }
        //清空defer
        d._panic = nil
        d.fn = nil
        //下一个defer
        gp._defer = d.link

        // trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic
        //GC()
        
        //defer语句下一条语句的地址
        pc := d.pc
        //获取rsp寄存器的值的指针
        //必须是指针,以便在堆栈复制期间进行调整
        sp := unsafe.Pointer(d.sp) 
        //释放defer
        freedefer(d)
        //如果panic被recover
        //会在gorecove 函数中已经修改为 true ,等会我们在讲
        if p.recovered {
            //统计
            atomic.Xadd(&runningPanicDefers, -1)
            
            //下一个panic
            gp._panic = p.link
            // 已标记已中止的panic,q且保留在g.panic列表中。
            //从列表中删除它们。
            for gp._panic != nil && gp._panic.aborted {
                gp._panic = gp._panic.link
            }
            //处理完所有panic
            if gp._panic == nil { // 必须用信号完成
                gp.sig = 0
            }
            // Pass information about recovering frame to recovery.
            //将有关恢复帧的信息传递给recovery函数
            //通过之前传入的 sp 和 pc 恢复
            gp.sigcode0 = uintptr(sp)
            gp.sigcode1 = pc
            mcall(recovery)
            throw("recovery failed") // mcall should not return
        }
    }

    // ran out of deferred calls - old-school panic now
    // Because it is unsafe to call arbitrary user code after freezing
    // the world, we call preprintpanics to invoke all necessary Error
    // and String methods to prepare the panic strings before startpanic.
    preprintpanics(gp._panic)
    
    //致命错误,终止程序
    fatalpanic(gp._panic) // should not return
    *(*int)(nil) = 0      // not reached
}

接着,我们再来看看它是如何通过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")
    }
    //让这个 defer 结构体的 deferproc 位置的调用重新返回
    // 这次将返回值修改为 1
    gp.sched.sp = sp
    gp.sched.pc = pc
    gp.sched.lr = 0
    gp.sched.ret = 1
    //直接跳回到deferreturn那里去
    gogo(&gp.sched)
}

我们再来总结一下整个流程:

  1. 先创建一个_panic结构体,加载到链表的表头
  2. 遍历当前goroutine的defer链表,
    • 如果defer被标记为已调用,跳出当前循环,进入下一个defer;
    • 否则,将当前defer标记为已调用,同时执行defer后面的函数,如果有recover,则会通过之前创建defer时传进来的deferproc 的下一条汇编指令的地址(pc),以及函数调用栈栈顶的位置(sp)返回到deferreturn的位置上去,否则,直接退出程序

Recover捕获异常

通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们 可以在程序崩溃前,做一些操作。比如说:当web服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭,服务器甚至可以将异常信息反馈到客户端,帮助调试。

如果在defer函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。

recover函数的使用

1.recover必须与defer配合使用

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

类似于下面这种情况是不可以的:

func main() {
    recover()
    panic("触发异常")

}
MjyYfm.png!mobile

在这里插入图片描述

2.必须在defer函数中直接调用recover,不能进行封装或者嵌套

func main() {
    defer func() {
        if r := MyRecover(); r != nil {
            fmt.Println(r)
        }
    }()
    panic("err")
}
func MyRecover() interface{} {
    fmt.Println("recover")
    return recover()
}
m2YnMji.png!mobile

在这里插入图片描述

同样,在defer中嵌套也不可以

func main() {
    defer func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println(r)
            }
        }()
    }()
    panic("err")
}

如果我们直接在 defer 语句中调用 MyRecover 函数又可以正常工作了:

func main() {
    //正常捕获
    defer MyRecover()
    panic("err")
}
func MyRecover() interface{} {
    fmt.Println("recover")
    return recover()
}

但是,如果 defer 语句直接调用 recover 函数,依然不能正常捕获异常:

func main() { 
    // 无法捕获异常
    defer recover()
    panic("err")
}

必须要和有异常的栈帧只隔一个栈帧, recover 函数才能正常捕获异常。换言之, recover 函数捕获的是祖父一级调用函数栈帧的异常(刚好可以跨越一层 defer 函数)!

同时,为了避免不加区分的panic被恢复,可能导致系统漏洞的问题,最安全的做饭,就是对不同的错误类型分别处理

recover函数的原理

接下来,我们通过底层源码来看看它是如何做到这些限制的:

runtime\panic.go
func gorecover(argp uintptr) interface{} {
    
    gp := getg()
    p := gp._panic
    //必须存在panic
    //非runtime.Goexit();
    //panic还未被恢复
    //argp == uintptr(p.argp)
    //p.argp是最顶层的延迟函数调用的参数指针,argp是调用recover函数的参数地址,通常是defer函数的参数地址
    //如果两者相等,说明可以被恢复,这也是为什么recover必须跟在defer后面且recover 函数捕获的是祖父一级调用函数栈帧的异常的原因
    if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
        //将recovered 标志改为true
        p.recovered = true
        return p.arg
    }
    return nil
}

gorecover函数比较简单,就是将recovered设为true,说明已经defer后面的函数包含recover

总结

  • recover函数在defer函数中
  • recover函数被defer函数直接调用
  • 如果包含多个defer函数,前面的defer通过recover()消除panic后,函数中剩余的defer仍然会执行,但不能再次recover()
  • 连续调用panic,仅最后一个会被recover捕获

参考

  1. Go语言圣经 .
  2. Go语言高级编程 .

有疑问加站长微信联系

iiUfA3j.png!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK