Golang强制死锁检测器输出跟踪日志的方法

Golang强制死锁检测器输出跟踪日志的方法 我想使用执行跟踪包来通过研究收集到的跟踪信息调试死锁。然而,当发生死锁时,系统会崩溃而不产生任何跟踪信息。

死锁是通过checkdead()检测的,该函数检查正在运行的 goroutine 数量是否为 0,如果是则抛出致命错误。致命错误(与 panic 不同)是不可捕获和恢复的。它们是一条输出消息,后跟一个 sys.exit(1)。因此,在死锁情况下使用 recover() 调用 StopTrace()(以刷新跟踪缓冲区)的想法是不可行的。

所以我在想,也许我可以修改 checkdead() 函数,使其在抛出致命错误之前先调用 StopTrace()。我的意思是把下面这段代码(来自 checkdead() 声明):

...
getg().m.throwing = -1                   // do not dump full stacks
unlock(&sched.lock)                      // unlock so that GODEBUG=scheddetail=1 doesn't hang
throw("all goroutines are asleep - deadlock!")
...

改成:

...
getg().m.throwing = -1                   // do not dump full stacks
unlock(&sched.lock)                      // unlock so that GODEBUG=scheddetail=1 doesn't hang
StopTrace()                              // to flush trace buffers before exit
throw("all goroutines are asleep - deadlock!")
...

然而,由于编译指令(#pragmas)强制某些函数不能有写屏障,当我想重新构建运行时库时,上面的代码片段会导致崩溃。例如,runtime 包中的函数 templateThread() 在执行期间会调用 checkdead(),并且它带有 //go:nowritebarriersrec 编译指令,这意味着该函数及其调用的任何函数都不能有写屏障。

综上所述,我有两个问题:

  • 有没有人知道更好的方法,如何强制 checkdead() 在退出程序之前刷新跟踪缓冲区?
  • 有没有办法绕过编译指令以避免构建崩溃?虽然这可能会导致运行时行为异常。我只是好奇。

附注:我不确定我把问题解释得是否清楚。如果有任何部分不清楚,请在评论中告诉我。


更多关于Golang强制死锁检测器输出跟踪日志的方法的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang强制死锁检测器输出跟踪日志的方法的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


要强制死锁检测器输出跟踪日志,可以通过修改运行时源码实现。以下是具体方法:

  1. 修改 runtime/proc.go 中的 checkdead() 函数
// 在抛出死锁错误前停止跟踪
func checkdead() {
    // ... 原有检查逻辑 ...
    
    getg().m.throwing = -1
    unlock(&sched.lock)
    
    // 添加跟踪停止逻辑
    if trace.enabled {
        traceStop()
    }
    
    throw("all goroutines are asleep - deadlock!")
}
  1. 编译自定义运行时
# 克隆Go源码
git clone https://go.googlesource.com/go
cd go/src

# 应用修改后编译
./make.bash
  1. 使用修改后的运行时编译程序
// main.go - 示例死锁程序
package main

import (
    "runtime/trace"
    "sync"
)

func main() {
    trace.Start(os.Stderr)
    defer trace.Stop()
    
    var mu sync.Mutex
    mu.Lock()
    mu.Lock() // 第二次锁定导致死锁
}

编译命令:

# 使用自定义运行时
GOROOT=/path/to/custom/go ./go/bin/go build main.go
  1. 处理写屏障限制的替代方案
// 通过runtime包直接调用内部函数
// 在checkdead()中添加:
if trace.enabled {
    // 直接调用内部停止函数,避免写屏障
    stopTrace()
}

其中 stopTrace() 需要在 runtime 包中实现:

// runtime/trace.go
func stopTrace() {
    // 简化的停止逻辑,避免写屏障
    traceLock()
    if trace.enabled {
        traceShutdown()
    }
    traceUnlock()
}
  1. 使用信号处理作为备选方案
// 在程序启动时注册信号处理
import (
    "os"
    "os/signal"
    "runtime/trace"
    "syscall"
)

func main() {
    trace.Start(os.Stderr)
    
    // 捕获退出信号
    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGABRT, syscall.SIGQUIT)
    
    go func() {
        <-c
        trace.Stop()
        os.Exit(1)
    }()
    
    // ... 程序逻辑 ...
}

这种方法通过修改运行时源码,在 checkdead() 检测到死锁时强制停止跟踪并输出日志。需要重新编译Go工具链,但可以确保在死锁发生时获取跟踪信息。

回到顶部