惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

SecWiki News
SecWiki News
I
InfoQ
The Cloudflare Blog
人人都是产品经理
人人都是产品经理
博客园 - Franky
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
博客园_首页
罗磊的独立博客
V
V2EX
李成银的技术随笔
大猫的无限游戏
大猫的无限游戏
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
True Tiger Recordings
Vercel News
Vercel News
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
F
Fox-IT International blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
M
Microsoft Research Blog - Microsoft Research
Know Your Adversary
Know Your Adversary
爱范儿
爱范儿
The Register - Security
The Register - Security
G
Google Developers Blog
The Hacker News
The Hacker News
Malwarebytes
Malwarebytes
S
Securelist
博客园 - 三生石上(FineUI控件)
Jina AI
Jina AI
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
S
SegmentFault 最新的问题
博客园 - 叶小钗
F
Fortinet All Blogs
Apple Machine Learning Research
Apple Machine Learning Research
宝玉的分享
宝玉的分享
博客园 - 聂微东
T
Threatpost
博客园 - 【当耐特】
D
Docker
P
Privacy & Cybersecurity Law Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
G
GRAHAM CLULEY
V
Visual Studio Blog
C
Cisco Blogs
IT之家
IT之家
S
Security Archives - TechRepublic
Latest news
Latest news
阮一峰的网络日志
阮一峰的网络日志

蛮荆

如何获取更多的免费服务器 Kubernetes 调度器队列 - 设计与实现 Kubernetes 调度器 - 核心流程 Kubernetes Networking Model & CNI Kubernetes 控制器管理总结 Kubernetes CronJob 设计与实现 Kubernetes Job 设计与实现 Kubernetes HPA 设计与实现 Kubernetes Deployment 滚动更新实现原理 Kubernetes GC 设计与实现 Kubernetes Pod 驱逐 - 设计与实现 Kubernetes Daemonset 设计与实现 Kubernetes ReplicaSet 设计与实现 Kubernetes EndPoint 设计与实现 Kubernetes Informer 设计与实现 降本增效之应用优化 (三) 日志存储与检索 Kubernetes Pod 设计与实现 - 创建流程 Kubernetes 探针设计与实现 Unix 编程艺术名句摘录 Kubernetes - CRI 概述 Golang 编译速度为什么这么快? Kubernetes Pod 设计与实现 - Pause 容器 Kubernetes - kube-proxy 代理模式工程优化 Kubernetes 应用最佳实践 - 优雅关闭长连接 Kubernetes Service 类型和会话亲和性 Kubernetes 为什么需要 Ingress Kubernetes 架构 - 控制平面和数据平面 降本增效之应用优化 (二) 大报表 Go 语言如何获取 CPU 利用率 降本增效之应用优化 (一) Redis 业务规则引擎演变过程简述 微服务中的熔断算法 漏桶算法和令牌桶算法 jsonparser 为什么比标准库的 encoding/json 快 10 倍 ? zap 高性能设计与实现 HTTP Router 算法演进 布谷鸟过滤器 fastcache 高性能设计与实现 Web 常见的三个安全问题 ants Code Reading 布谷鸟过滤器 Go 线程安全 map 方案选型 布隆过滤器 死锁、活锁、饥饿、自旋锁 sync.Pool Code Reading Go 内存管理概述 Go netpoll Code Reading goroutine 泄漏与检测 time/Timer Code Reading GMP Scheduler Code Reading Go channel 的 15 条规则和底层实现 为什么 Linux “一切皆文件” context.Context Code Reading runtime/HACKING.md Goland 最佳实践 互联网开发与金庸武学 为什么 Redis 6.0 引入多线程模型? Kubernetes 应用最佳实践 - 金丝雀发布 容器中如何正确配置 GOMAXPROCS ? singleflight Code Reading sync.Map Code Reading sync.Cond Code Reading sync.WaitGroup Code Reading sync.RWMutex Code Reading sync.Mutex Code Reading sync.Once Code Reading Go 无锁编程 sync/atomic Code Reading goroutine 交替打印奇偶数 GODEBUG Go 并发模式 Go 汇编 UUID 通用技术选型 Kubernetes 应用最佳实践 - 水平自动伸缩 Go 高性能 Tips fasthttp 为什么比标准库 net/http 快 10 倍 ? 技术文章配图指南 ChatGPT 初体验 Docker 网络原理概览 iptables 的五表五链 Kubernetes 应用最佳实践 - 亲和性和污点容忍度 Go 的反射与三大定律 Docker 官方提供的最佳实践 Go 语言内置的设计模式 HTTP1 到 HTTP3 的工程优化 Kubernetes 应用最佳实践 - Sidecar 模式 Kubernetes 应用最佳实践 - init 容器和钩子函数 为什么 defer 的执行顺序和注册顺序不同? Go map 设计与实现 Go 切片扩容底层实现 Go 语言中的零拷贝 Go Delve 云原生和边缘计算简介 Kubernetes Pod 服务质量等级 Kubernetes 应用最佳实践 - 探针 Kubernetes 应用最佳实践 - 资源请求和限制 CDN 原理 Kubernetes 应用最佳实践 - 开篇 缓存策略和模式 Go 内存模型
为什么 recover 必须在 defer 中调用?
2023-03-13 · via 蛮荆

2023-03-13 Golang Go 源码分析 读代码

前言

开始正文之前,先来看看几个有趣的小问题:

  1. 为什么 panic 可以让程序崩溃?
  2. 为什么 recover 可以捕获 panic 的消息并终止程序崩溃?
  3. 为什么 recover 必须在 defer 中调用?
  4. 为什么 recover 必须在 defer 中直接调用 (不能嵌套)?

内部实现

带着上面的几个小问题,我们从源代码的角度来探究一下, panicrecover 的实现相关文件目录为 $GOROOT/src/runtime,笔者的 Go 版本为 go1.19 linux/amd64

_panic 对象

_panic 对象表示 panic 语句 语句的运行时。

// runtime2.go

type _panic struct {
	argp      unsafe.Pointer // 指向调用 defer 时参数的指针
	arg       any            // 指向调用 panic 时传入的参数
	link      *_panic        // _panic 链表
	pc        uintptr        // panic 被捕获后,继续执行的程序 sp (栈底) 寄存器
	sp        unsafe.Pointer // panic 被捕获后,继续执行的程序 pc (程序计数器) 寄存器 (下一条汇编指令的地址)
	recovered bool           // 当前 panic 是否被捕获 
	aborted   bool           // 当前 panic 是否被终止
	
	// pc、sp 和 goexit 三个字段都是为了修复 runtime.Goexit 带来的问题引入的 
	// runtime.Goexit 能够只结束调用该函数的 goroutine 而不影响其他的 goroutine 
	//  但是该函数会被 defer 中的 panic 和 recover 取消
	// 引入这三个字段就是为了保证 runtime.Goexit 函数一定会执行
	goexit    bool
}

_panic 对象


gopanic 方法

gopanic 方法对应 panic 函数,编译器会将 panic 语句 转换为 gopanic 函数调用。

// panic.go

func gopanic(e any) {
    // 获取当前 G
	gp := getg()

	// 生成一个新的 _panic 对象
	var p _panic        
	p.arg = e
	// 将 _panic 对象放在链表头部
	p.link = gp._panic  
	gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

	for {
		// 获取 _defer 链表头节点
		d := gp._defer
		// 没有 _defer 对象, 自然也就没有 recover,直接跳出循环
		// 意味着程序没有捕获 panic, 然后崩溃
		if d == nil {
			break
		}

		// defer 语句已经执行过了
		// 如果 defer 是由之前的 panic 或 runtime.Goexit 执行的
		// 并且触发了新的 panic, 也就是 defer 函数里再次 panic
		// 将 defer 从列表中删除,之前的 panic 不会继续运行
		// 但需要确保之前的 runtime.Goexit 继续运行
		if d.started {
			if d._panic != nil {
				d._panic.aborted = true
			}
			d._panic = nil
			
			if !d.openDefer {
				d.fn = nil
				// 从 defer 链表中删除当前 defer 对象
				gp._defer = d.link 
				freedefer(d)
				continue
			}
		}

		// 标记 defer 已经执行
		d.started = true
		
		// 如果在 defer 调用期间发生了新的 panic
		// 新的 panic 将在链表中找到当前 _defer
		// 标记 d._panic (指向当前的 panic) 终止
		d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
		d._panic = nil

		// 获取 pc, sp 寄存器的值
		pc := d.pc
		sp := unsafe.Pointer(d.sp)
		
		if p.recovered {
			// p.recovered 字段已经在 gorecover 函数中被修改为 true
			// 说明当前 panic 被捕获了
            // 从 panic 链表中删除当前 panic 对象
			gp._panic = p.link  
			
			if gp._panic != nil && gp._panic.goexit && gp._panic.aborted {
				// 正常的恢复将绕过 Goexit
				// 非正常的情况交给 Goexit 处理
			}

			gp._panic = p.link
			// 如果链表的当前节点后面还有 _panic 对象
			// 并且被标记为终止了,将它们从链表中删除
			for gp._panic != nil && gp._panic.aborted {
				gp._panic = gp._panic.link
			}
			
			// 将捕获到的 panic 消息传递给 recover 函数
			gp.sigcode0 = uintptr(sp)
			gp.sigcode1 = pc
			// 恢复的时候 panic 函数将从此处跳出 (编译器实现)
			// gopanic 调用结束,下面的两行代码不会执行
			mcall(recovery)            
			throw("recovery failed")
		}
	}

	fatalpanic(gp._panic)
}

fatalpanic 方法

fatalpanic 方法表示当 panic 没有被捕获时要执行的操作 (也就是结束程序)。

//go:nosplit
func fatalpanic(msgs *_panic) {
	systemstack(func() {
		// 程序结束错误码为 2 
		exit(2)
	})
}

gorecover 方法

gorecover 方法对应 recover 函数,编译器会将 recover 语句转换为 gorecover 函数调用。

//go:nosplit
func gorecover(argp uintptr) any {
    // recover 函数必须在 defer 函数中调用
    // recover 函数必须从最顶层函数 (直接在 defer 语句或函数体中) 调用,也就是说不能出现 defer 嵌套
	// p.argp 是最顶层的 defer 函数调用的参数指针,与 panic 函数的调用方的参数进行比较
	// 如果匹配,调用方就可以 recover
	gp := getg()
	p := gp._panic

    // 只处理一个 _panic, 标记完就返回
    // 具体的捕获恢复处理逻辑在 gopanic 函数实现
	if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
		// 将 panic 标记为已捕获
		p.recovered = true
		// 返回 panic 的参数 
		return p.arg
	}
	return nil
}

recovery 方法

recovery 方法用于恢复 goroutine 的继续执行,通过重置寄存器并将 goroutine 重新加入调度队列。

func recovery(gp *g) {
	// 恢复之前传入的 sp 和 pc 
	sp := gp.sigcode0   // 取出栈 sp
	pc := gp.sigcode1   // 取出栈 pc
	
	gp.sched.sp = sp    // 重置栈 sp
	gp.sched.pc = pc    // 重置栈 pc
	gp.sched.lr = 0
	gp.sched.ret = 1    
    gogo(&gp.sched)     // 加入调度队列
}

函数中的这句代码可以简单解释下:

gp.sched.ret = 1

这里并没有调用 deferproc 函数,但是直接修改了返回值,所以调度再次执行时会跳转到 deferproc 函数的下一条指令位置,设置为 1 是模拟 deferproc 函数返回值。

在上一篇分析 defer 源代码的时候,我们提到过:

如果 deferproc 返回值不等于 0, 说明 panic 被捕获到了

如果 deferproc 返回值等于 0, 说明 panic 没有被捕获

小结

panic + recover 的实现由编译器和运行时共同完成,通过对内部实现源代码的学习,我们可以更加深入理解 defer + panic+ recover 的内部实现, 现在来回答本文开头提到的几个小问题。

  1. panic 如果没有被捕获,最终会调用 exit(2) 终止程序运行 (也就是程序崩溃)
  2. recover 最终会调用 recovery 方法恢复程序的继续执行
  3. gorecover 会进行参数校验,只有在 defer 语句中调用 recover, 才能通过参数校验 (详情见 gorecover 函数代码注释)
  4. 同上