ゴランでのパニックと回復の実現原理

今日は、golangでのパニック例外とrecoverによる例外のキャプチャについて説明します。パニック、リカバリ、および延期は非常に近いため、本日一緒に説明します。延期に関する知識がいくつか含まれます。興味がある場合は、私を参照してください。延期に関する別の記事は、Golangでの延期の実装原則です

パニック異常

Goのタイプシステムは、コンパイル時に多くのエラーをキャッチしますが、範囲外の配列アクセス、nullポインター参照など、一部のエラーは実行時にのみチェックできます。これらのランタイムエラーは、痛みを伴う例外を引き起こす可能性があります。
一般的に、パニック例外が発生すると、プログラムは操作を中断し、ゴルーチンで遅延した機能(遅延メカニズム)を即座に実行します。その後、プログラムがクラッシュし、ログ情報が出力されました。
すべてのパニック例外がランタイムから発生するわけではありません。組み込みのパニック関数を直接呼び出すと、パニック例外も発生します。
次に、アセンブリコードを使用して、組み込み関数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()関数に置き換えられていることがわかります
関数を見る前に、パニックの構造を見てみましょう。

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
}

次に、リカバリ機能を介してどのように返信されるかを見てみましょう。

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. 現在のゴルーチンの延期リストをトラバースし、
    • 延期が呼び出されたとマークされている場合は、現在のループからジャンプして次の延期に入ります。
    • それ以外の場合は、現在の遅延を呼び出し済みとしてマークし、同時に遅延の背後にある関数を実行します。リカバリがある場合は、遅延が以前に作成されたときに渡されたdeferprocの次のアセンブリ命令(pc)のアドレスと、関数呼び出しスタックの最上位を渡します。位置(sp)はdeferreturnの位置に戻ります。それ以外の場合は、プログラムを直接終了します。

キャッチ例外の回復

一般的に言って、パニック例外の処理を行うべきではありませんが、場合によっては、例外から回復できる可能性があります。少なくとも、プログラムがクラッシュする前にいくつかの操作を実行できます。例:Webサーバーで予期しない重大な問題が発生した場合、クラッシュする前にすべての接続を閉じる必要があります。サーバーは、デバッグに役立つ異常な情報をクライアントにフィードバックすることもできます。
組み込み関数recoverがdefer関数で呼び出され、deferステートメントを定義する関数でパニック例外が発生した場合、recoverはプログラムをパニックから回復させ、パニック値を返します。パニック例外の原因となった関数は実行を継続しませんが、正常に戻ることができます。パニックが発生していないときにrecoverを呼び出すと、recoverはnilを返します。

回復機能の使用

1.recoverはdeferと組み合わせて使用​​する必要があります
func main() {
    
    
	defer func() {
    
    
			recover()
			 }()
	panic("err")
}

次のような状況は受け入れられません。

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

}

ここに写真の説明を挿入

2.リカバリはdefer関数で直接呼び出す必要があり、カプセル化またはネストすることはできません
func main() {
    
    
	defer func() {
    
    
		if r := MyRecover(); r != nil {
    
    
			fmt.Println(r)
		}
	}()
	panic("err")
}
func MyRecover() interface{
    
    } {
    
    
	fmt.Println("recover")
	return recover()
}

ここに写真の説明を挿入
同様に、延期でのネストはできません

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")
}

回復機能が例外を正常にキャッチする前に、例外を除いて1つのスタックフレームのみをスタックフレームから分離する必要があります。言い換えると、recover関数は、祖父の第1レベルの呼び出し関数のスタックフレームの例外をキャッチします(defer関数のレイヤーにまたがることができます)。

同時に、システムの脆弱性を引き起こす可能性のある無差別なパニックの回復を回避するために、調理する最も安全な方法は、さまざまなタイプのエラーを個別に処理することです。

回復機能の原理

次に、基礎となるソースコードを使用して、これらの制限がどのように達成されるかを確認します。

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関数は比較的単純です。つまり、recoverをtrueに設定し、deferの背後にある関数にrecoverが含まれていることを示します。

総括する

  • 回復機能は延期機能にあります
  • 回復関数は、遅延関数によって直接呼び出されます
  • 複数の延期関数が含まれている場合、前の延期がrecover()によってパニックを解消した後、関数内の残りの延期は引き続き実行されますが、再度recover()することはできません。
  • パニックを継続的に呼び出し、最後の1つだけがrecoverによってキャプチャされます

参照

  1. 言語聖書に行きなさい
  2. Go言語での高度なプログラミング

おすすめ

転載: blog.csdn.net/xzw12138/article/details/109256075