chikaku

且听风吟

永远是深夜有多好。
github
email

Go 运行时黑魔法

Go 程序启动 里提到过,当前 (被调度为 running) 协程的数据结构指针即 *g 是放在 TLS 上的。可以用一点汇编自行取到 *g (当然只是一个 uintptr 并非带类型的指针),以访问运行时协程数据。且 g/p/m 数据有关联,还可以做很多其他事情。

事实上现在有很多类似的 repo 比如 goidpid 都用了这个办法。但类似的库都有一个现象,我们可以在 repo 的代码里看到各种 *_go1.13.go, *_go1.14.go, *_go1.15.go 等等文件,原因是各个版本的 runtime 数据结构可能会修改,不同版本对应一个字段的偏移量会不同,需要在每个版本写一个同 runtime 一致的结构体,然后用 unsafe.Offsetof 取得偏移量。

在一次用反射的时候想到 Go ELF 中应该有地方保存了这些数据结构的偏移量信息。后续查找 Go ELF 相关的资料,发现 Go 将 debuginfo 以 DWARF 格式保存,而且标准库提供了对应的接口 debug/dwarf 但是有些问题,首先 go test 的时候编译出的二进制似乎是不包含 debuginfo 导致读 dwarf 出错,其次 darwin 下的编译的 ELF 读取 dwarf 会报 bad magic number,只有在 Linux 下能愉快的使用

后续就比较简单了,通过 os.Args[0] 获取二进制路径,读取出 dwarf, 根据结构体名和字段名查找符号信息,就可以在运行时读取任意数据结构的任意字段偏移量 (包括 runtime 的 g/p/m 和其他非导出结构的非导出字段)。现在,我们就拥有了获取运行时数据的魔法!

具体的实现可以查看 tools-go 仓库下的 rt package

使用方式#

下面给出 rt package 实现的一些工具

任意结构体任意字段偏移量 rt.Offsetof#

类似于 unsafe.Offsetof 这里结构体名字一定要写对,如果是第三方库结构体,可以通过符号表查

// 获取运行时 g 的 goid 字段的偏移量
offset, _ := rt.Offsetof("runtime.g", "goid")
fmt.Println(offset)
// 152

当前协程栈大小 rt.GoStackSize#

因为 g 上面有两个标识栈范围的字段,可以通过差值获取栈大小

func main() {
    done := make(chan int)
    go func() { done <- demo() }()
    <-done
}

func demo() int {
    size, _ := rt.GoStackSize()
    fmt.Println("init stack size", size)

    rec(10000, size)
    return 0
}

func rec(n, size0 int) {
    size, _ := rt.GoStackSize()
    if size != size0 {
        fmt.Println("stack size")
    }

    if n > 0 { rec(n-1, size) }
}

通过输出可以看到初始栈大小 4K 后续每次 morestack 增加一倍

init stack size 4096
stack size 8192
stack size 16384
stack size 32768
stack size 65536
stack size 131072
stack size 262144
stack size 524288
stack size 1048576
stack size 2097152

定时器总量 rt.NumTimers#

由于所有的定时器都注册在 p 上可以通过 p.numTimers 获取单个 p 的定时器数量,而且运行时刚好也提供了一个 allp 的全局变量,用 linkname 链接过来就可以遍历所有的 p 啦!

当然这个方法由于 timer 状态的修改以及延迟删除等,数据不会完全准确,但是能够估出一个相对值,在进行压测的时候还是很有用的。

func main() {
    time.AfterFunc(time.Minute, nil)
    time.AfterFunc(time.Minute, nil)
    time.AfterFunc(time.Minute, nil)
    time.AfterFunc(time.Minute, nil)
    time.AfterFunc(time.Minute, nil)

    cnt, _ := rt.NumTimers()
    fmt.Println("timer count", cnt)
    // timer count 5
}

注意事项 ⚠️⚠️⚠️#

  • package rt 目前只在 Linux 下可用
  • 需要 build! 直接 go run 或者 go test 编译出的二进制不包含 debuginfo 会报错 decoding dwarf section info at offset 仓库内部在 test 的时候预先编译了一个二进制来读取 dwarf
  • 危险! 这个库里面直接操作了 *g 以及 allp (虽然也把锁链接过来了用上了但是感觉更危险了) 不确定会对运行时造成何种影响,可以在测试环境和压测环境尝试使用
  • 性能 运行时会读取一次二进制,在获取结构体字段的方法里面还会有一些可能比较耗时的符号遍历
加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。