返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

6.2 panic

发布于 2024-10-12 19:16:07 字数 6338 浏览 0 评论 0 收藏 0

编译器将 panic 翻译成 gopanic 调用。

package main

func test() {
	recover()
}

func main() {
	defer test()
	panic("xxx")
}
$ go tool objdump -s "main\.main" test

TEXT main.main(SB) test.go
  test.go:9   0x1056fd6    CALL runtime.gopanic(SB)            


$ go tool objdump -s "main\.test" test

TEXT main.test(SB) test.go
  test.go:4   0x1056f66    CALL runtime.gorecover(SB)

gopanic

和 defer 类似,panic 也会保存到 G 里。

// runtime2.go

type _panic struct {
    argp      unsafe.Pointer  // pointer to arguments of deferred call run during panic
    arg       interface{}     // argument to panic
    link      *_panic         // link to earlier panic
    pc        uintptr         // where to return to in runtime if this panic is bypassed
    sp        unsafe.Pointer  // where to return to in runtime if this panic is bypassed
    recovered bool            // whether this panic is over
    aborted   bool            // the panic was aborted
    goexit    bool
}
type g struct {
    _panic  *_panic
}

引发错误前,需确保 G 已注册的延迟函数得以执行。

仅最后一个 panic 抛出。

但所有 panic 都保存在 G._panic 链表,以便输出跟踪(traceback)信息。

// panic.go

// The implementation of the predeclared function panic.

func gopanic(e interface{}) {
    gp := getg()
    
    // 创建,添加到 G._panic 链表头部。
    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. An earlier panic will not continue running, but we will make sure below that an
        // earlier Goexit does continue running.
        
        // 延迟函数(defer fn)可再次调用 panic,那么 gopanic 会 “递归嵌套”。
        // 在内层,d 已被外层 panic 或 goexit 启动。跳过,并将其从链表移除。
        // 由内层处理掉剩余的,且不会返回(not return)外层。
        // 结果要么崩溃(fatalpanic),要么 recover 回到 caller 函数。
        
        
        // 如果当前 defer 已经执行,则清理掉。
        if d.started {
            
            if d._panic != nil {
                d._panic.aborted = true
            }
            
            d._panic = nil
            
            // 释放当前 defer,继续执行下一个。
            if !d.openDefer {
                d.fn = nil
                gp._defer = d.link
                
                freedefer(d)
                continue
            }
        }
        
        // 启动当前 defer,并绑定(供内层检查)。
        d.started = true
        d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
        done := true
        
        // 执行延迟调用。
        p.argp = unsafe.Pointer(getargp(0))   // gopainc sp
        reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))

        p.argp = nil
        d._panic = nil
        
        // 延迟调用(deferproc)保存的 caller 数据。
        pc := d.pc
        sp := unsafe.Pointer(d.sp)
        
        // 丛链表删除。
        if done {
            d.fn = nil
            gp._defer = d.link
            freedefer(d)
        }
        
        // 如果延迟调用内执行了 recover 函数。
        // 那么 gopainc 执行到此结束,由 recover 恢复 caller 调用状态。
        // 剩余工作就交给 caller deferreturn 了。
        if p.recovered {
         
            // 本次 panic 被捕获,从链表移除。
            gp._panic = p.link
         
            // 将放弃的 panic 从链表移除。
            for gp._panic != nil && gp._panic.aborted {
                gp._panic = gp._panic.link
            }
         
            // 恢复 caller 调用。
            // 当前 gopanic 被抛弃,不再返回。
            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
}

fatalpanic 调用 os.exit 终止进程,不会等待其他 goroutine,也不会执行其他 G.defers。

因为 panic 代表不可修复错误,如果要等待其他 goroutine,则可能永远无法终止进程。那就失去了 panic 的设计初衷。

gorecover

在延迟函数内调用 recover,被编译器转换为 gorecover 调用。

仅是标记,还是由 gopanic 调用 recovery 处理。

参数 argp 由延迟函数传入,指向 defer caller.SP。

// The implementation of the predeclared function recover.

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
    
    // 通过比对 caller.SP 地址,确保在 topmost 内调用。
    if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
        p.recovered = true
        return p.arg
    }
    
    return nil
}

recovery

将调用堆栈恢复到 deferproc 下一条指令。

前面 gopanic 用 G.sigcode 传递 defer 保存的 caller SP、PC 值。

但要知道,这两个值是调用 deferproc 时候的状态,也就是说 PC 并非指向函数尾部。

那么,gogo 实际执行的是 deferproc 下一条指令。

在 recovery 里,设置 sched.ret = 1 ,gogo 先将其放到 AX。

编译器在 deferproc 调用下方生成:

CALL runtime.deferproc(SB)

TESTL AX, AX // if AX == 0 then ZF = 1 else ZF = 0.

JNE 83 // if ZF == 0 then jmp.

...

XCHGL AX, AX // 83

CALL runtime.deferreturn(SB)

如此,可以确保后续 defer 被 deferreturn 执行。

func recovery(gp *g) {
    sp := gp.sigcode0
    pc := gp.sigcode1
    
    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) { // arguments of fn follow fn
 
    ...
    
    // deferproc returns 0 normally.
    // a deferred func that stops a panic
    // makes the deferproc return 1.
    // the code the compiler generates always
    // checks the return value and jumps to the
    // end of the function if deferproc returns != 0.
    
    return0()
    
    // No code can go here - the C return register has
   // been set and must not be clobbered.
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文