在 Go 程序启动 中提到,當前 (被調度為 running) 協程的數據結構指針即 *g
是放在 TLS 上的。可以用一點彙編自行取到 *g
(當然只是一個 uintptr 並非帶類型的指針),以訪問運行時協程數據。且 g/p/m 數據有關聯,還可以做很多其他事情。
事實上現在有很多類似的 repo 比如 goid 和 pid 都用了這個辦法。但類似的庫都有一個現象,我們可以在 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 (雖然也把鎖鏈接過來了用上了
但是感覺更危險了) 不確定會對運行時造成何種影響,可以在測試環境和壓測環境嘗試使用 - 性能 運行時會讀取一次二進制,在獲取結構體字段的方法裡面還會有一些可能比較耗時的符號遍歷