当前位置:首页>Linux>Linux信号处理:通过 SIGSEGV 回溯 Go 程序堆栈

Linux信号处理:通过 SIGSEGV 回溯 Go 程序堆栈

  • 2026-10-11 07:45:10
Linux信号处理:通过 SIGSEGV 回溯 Go 程序堆栈

通过 SIGSEGV 回溯 Go 程序堆栈

本文方法通过 kill -11(SIGSEGV) 强制终止 Go 进程获取堆栈,执行后进程直接退出!仅限测试环境、离线调试、非核心业务使用,严禁在生产在线服务随意执行!

原理

SIGSEGV(信号 11)是段错误信号,内存非法访问时内核会发送给进程,默认行为是直接终止进程。Go 运行时默认注册了该信号的处理函数:收到 SIGSEGV 后,会先打印完整 goroutine 堆栈,再退出进程。利用这个特性,我们可以手动向 Go 进程发送 SIGSEGV,强制触发堆栈打印(注意:进程会被杀死,仅限调试使用)

有时候 golang 出现卡死、死循环、协程泄露等问题,但又没开启 pprof、没有其他调试办法时可以使用这种方式。

准备

先要准备一个 golang 测试代码

package mainimport (    "fmt"    "time")funcmain() {    go func() {        for {            time.Sleep(time.Second)            fmt.Println("goroutine running")        }    }()    for {        time.Sleep(time.Second)        fmt.Println("main running")    }}
# 编译go build -o myapp myapp.go

调试核心逻辑

Go 程序触发 SIGSEGV 打印的所有堆栈信息,只会输出到标准错误流 stderr(文件描述符 2)。 我们日常启动程序时,日志输出方式不同,stderr 挂载的目标就不一样,堆栈存放位置也随之改变,因此划分出三种场景,只需先查看 /proc/<PID>/fd/2 确认 stderr 去向,就能对应选择读取方式。

1. 查看 stderr 挂载位置

ps -elf | grep myapp# 替换为真实进程PID查询ls -l /proc/<PID>/fd/2

作用:快速判定堆栈输出位置,决定后续怎么抓取堆栈。

2. 触发堆栈打印

kill -11 <PID>

三种场景区分与实操

场景一:stderr 挂载终端(前台直接运行程序)

•启动方式:./myapp

      •特征:fd/2 指向 /dev/pts/xxx 终端设备
ls -l /proc/<PID>/fd/2lrwx------ 1 mars mars 64 May 17 11:32 2 -> /dev/pts/23

•特点:堆栈直接打印在当前运行窗口,直观可见

      •查看方式:直接观察程序运行终端输出即可
# 新窗口上发送信号kill -11 <PID># myapp窗口可以看到./myapp......SIGSEGV: segmentation violationPC=0x40332e m=0 sigcode=0 addr=0x3e8000591c5goroutine 1 gp=0xc000006380 m=nil [sleep]:runtime.gopark(0x194c923fb1714b?, 0x1?, 0x0?, 0x0?, 0x46?)        /usr/lib/go-1.22/src/runtime/proc.go:402 +0xce fp=0xc00006fea0 sp=0xc00006fe80 pc=0x4362aetime.Sleep(0x3b9aca00)        /usr/lib/go-1.22/src/runtime/time.go:195 +0x115 fp=0xc00006fee0 sp=0xc00006fea0 pc=0x4600f5main.main()        /home/mars/test/myapp.go:17 +0x28 fp=0xc00006ff50 sp=0xc00006fee0 pc=0x48d7e8runtime.main()        /usr/lib/go-1.22/src/runtime/proc.go:271 +0x29d fp=0xc00006ffe0 sp=0xc00006ff50 pc=0x435e7druntime.goexit({})        /usr/lib/go-1.22/src/runtime/asm_amd64.s:1695 +0x1 fp=0xc00006ffe8 sp=0xc00006ffe0 pc=0x462e81

场景二:stderr 重定向至日志文件

• 启动方式:./myapp > myapp.log 2>&1

      •特征:fd/2 指向本地日志文件路径
ls /proc/<PID>/fd/2 -ll-wx------ 1 mars mars 64 May 17 11:03 2 -> /home/mars/test/myapp.log
     •特点:标准输出、错误输出统一存入日志文件,方便事后复盘
kill -11 <PID># 当手动发11信号可以看到:cat myapp.log | tail -n 50runtime.gcenable.gowrap2()        /usr/lib/go-1.22/src/runtime/mgc.go:204 +0x25 fp=0xc00003dfe0 sp=0xc00003dfc8 pc=0x417145runtime.goexit({})        /usr/lib/go-1.22/src/runtime/asm_amd64.s:1695 +0x1 fp=0xc00003dfe8 sp=0xc00003dfe0 pc=0x462e81created by runtime.gcenable in goroutine 1        /usr/lib/go-1.22/src/runtime/mgc.go:204 +0xa5

场景三:stderr 重定向至 /dev/null(后台静默运行)

•启动方式:./myapp >/dev/null 2>&1 &

      •特征:fd/2 指向 /dev/null
ls /proc/<PID>/fd -ll-wx------ 1 mars mars 64 May 17 10:19 2 -> /dev/null

•特点:所有日志、错误信息都会被系统丢弃,常规方式看不到任何堆栈

      •解决办法:借助 strace 抓取系统调用,拦截写入 stderr 的堆栈数据
#先跟踪进程strace -p <PID> -y -s 500 -o myapp.strace.txt#发送信号kill -11 <PID>#查看堆栈cat myapp.strace.txt--- SIGSEGV {si_signo=SIGSEGV, si_code=SI_USER, si_pid=364997, si_uid=1000} ---nanosleep({tv_sec=0, tv_nsec=1000000}, NULL) = 0nanosleep({tv_sec=0, tv_nsec=1000000}, NULL) = 0write(2</dev/null>, "SIGSEGV: segmentation violation", 31) = 31write(2</dev/null>, "\n", 1)            = 1write(2</dev/null>, "PC=", 3)           = 3write(2</dev/null>, "0x464c21", 8)      = 8write(2</dev/null>, " m=", 3)           = 3write(2</dev/null>, "0", 1)             = 1write(2</dev/null>, " sigcode=", 9)     = 9write(2</dev/null>, "0", 1)             = 1write(2</dev/null>, " addr=", 6)        = 6write(2</dev/null>, "0x3e8000591c5", 13) = 13write(2</dev/null>, "\n", 1)            = 1write(2</dev/null>, "\n", 1)            = 1write(2</dev/null>, "goroutine ", 10)   = 10write(2</dev/null>, "0", 1)             = 1

进程退出后 strace 会自动结束;如果长时间没有输出,请检查进程是否还在运行,或手动 Ctrl+C 终止 strace。

上面的格式化看起来不好看,把标准错误输出的信息搜索出来,然后使用perl提取堆栈,做下格式化:

cat myapp.strace.txt | grep 'write(2' | perl -ne 'if($_=~/"\([^"]+)"/) { $line=$1;$line =~ s/\\n/\n/g;$line =~ s/\\t/\t/g;print $line;}'goroutine 4 gp=0xc000007500 m=nil [sleep]:runtime.gopark(0x195065a41e1ecd?, 0x1?, 0x0?, 0x0?, 0x4c?)        /usr/lib/go-1.22/src/runtime/proc.go:402 +0xce fp=0xc000066f30 sp=0xc000066f10 pc=0x4362aetime.Sleep(0x3b9aca00)        /usr/lib/go-1.22/src/runtime/time.go:195 +0x115 fp=0xc000066f70 sp=0xc000066f30 pc=0x4600f5main.main.func1()        /home/mars/test/myapp.go:11 +0x1c fp=0xc000066fe0 sp=0xc000066f70 pc=0x48d87cruntime.goexit({})        /usr/lib/go-1.22/src/runtime/asm_amd64.s:1695 +0x1 fp=0xc000066fe8 sp=0xc000066fe0 pc=0x462e81created by main.main in goroutine 1        /home/mars/test/myapp.go:9 +0x1erax    0xcarbx    0x0rcx    0x464c23rdx    0x0

命令说明

cat myapp.strace.txt | grep 'write(2' | perl -ne 'if($_=~/"([^"]+)"/) { $line=$1;$line =~ s/\\n/\n/g;$line =~ s/\\t/\t/g;print $line;}'

1)先搜索出标准错误的系统调用。

cat myapp.strace.txt | grep 'write(2'write(2</dev/null>, " addr=", 6)        = 6write(2</dev/null>, "0x3e8000591c5", 13) = 13write(2</dev/null>, "\n", 1)            = 1

2) 然后使用perl脚本提取引号里的字符串,并把\n \t打印成回车和tab键。

if($_=~/"([^"]+)"/) {              # 匹配引号里的内容    $line=$1;                      # 引号里的内容提取出来    $line =~ s/\\n/\n/g;           # 把字符串\n替换成回车    $line =~ s/\\t/\t/g;           # 把字符串\t替换成tab    print $line;                   # 输出替换过程的内容}

总结

适用场景

1.Go 进程长时间卡死无响应2.业务逻辑死循环阻塞3.协程堆积、goroutine 泄露排查4.服务未开启 pprof 调试端口5.无本地日志、无监控告警、无远程调试入口

重要注意事项

1.kill -11 属于强杀调试手段,执行后进程直接退出,仅可使用一次2.务必精准核对进程 PID,禁止批量操作,避免误杀线上正常业务进程3.该方式破坏性较强,正式生产环境严格禁止使用4.仅作为无任何调试手段时的兜底排查方案

生产环境优先推荐安全调试方案

1.内置 pprof:线上实时抓取协程、堆栈、性能指标,不中断服务2.gcore 生成 core 转储文件:离线分析进程现场,不影响业务运行3.dlv 远程断点调试:线上无损动态调试,精准定位代码阻塞点

接上一篇
Linux信号处理:kill使用与避坑

最新文章

随机文章