Go 程序启动 で述べたように、現在(実行中としてスケジュールされている)ゴルーチンのデータ構造ポインタである *g
は TLS に配置されています。アセンブリを使用して *g
を取得することができます(もちろん、uintptr であり、型付きポインタではありません)。これにより、実行時のゴルーチンデータにアクセスすることができます。また、g/p/m データには関連があり、他の多くのこともできます。
実際、goid や pid など、同様のリポジトリが多数存在しますが、これらのライブラリには共通の現象があります。リポジトリのコード内で *_go1.13.go, *_go1.14.go, *_go1.15.go などのファイルを見ることができますが、これは各バージョンのランタイムデータ構造が変更される可能性があるためであり、異なるバージョンに対応するために、各バージョンでランタイムと同じ構造体を書き、unsafe.Offsetof を使用してオフセットを取得する必要があります。
リフレクションを使用している際に、Go ELF にこれらのデータ構造のオフセット情報が保存されている場所があるはずだと思いました。Go ELF に関連する情報を調査すると、Go は debuginfo をDWARF形式で保存しており、標準ライブラリには対応するインターフェース debug/dwarf も提供されていますが、いくつかの問題があります。まず、go test を実行すると、デバッグ情報が含まれていないため、dwarf を読み取ることができません。次に、Darwin でコンパイルされた ELF を読み取ると、不正なマジックナンバーとして報告されます。Linux でのみ正常に動作します。
その後は比較的簡単です。os.Args[0]
を使用してバイナリのパスを取得し、dwarf を読み取り、構造体名とフィールド名を使用してシンボル情報を検索することで、実行時に任意のデータ構造の任意のフィールドオフセットを読み取ることができます(ランタイムの g/p/m や他の非公開構造体の非公開フィールドも含まれます)。これで、実行時データを取得する魔法を手に入れました!
具体的な実装は、tools-go リポジトリの rt パッケージを参照してください
使用方法#
以下に、rt パッケージで実装されたいくつかのツールを示します。
任意の構造体の任意のフィールドオフセット rt.Offsetof#
unsafe.Offsetof と似ていますが、ここでは構造体名を正しく指定する必要があります。サードパーティのライブラリの構造体の場合は、シンボルテーブルを使用して検索できます。
// 実行時のgのgoidフィールドのオフセットを取得
offset, _ := rt.Offsetof("runtime.g", "goid")
fmt.Println(offset)
// 152
現在のゴルーチンのスタックサイズ rt.GoStackSize#
g には 2 つのスタック範囲を示すフィールドがありますので、差分を使用してスタックサイズを取得できます。
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 で、その後のスタックサイズが倍増していることがわかります。
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 というグローバル変数も提供しているため、リンク名を使用してすべての p を反復処理することもできます!
もちろん、このメソッドはタイマーの状態の変更や遅延削除などの影響を受けるため、データは完全に正確ではありませんが、相対的な値を推定することができます。負荷テスト時に非常に便利です。
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
}
注意事項 ⚠️⚠️⚠️#
- rt パッケージは現在 Linux でのみ使用可能です
- ビルドが必要です! go run や go test でコンパイルされたバイナリにはデバッグ情報が含まれていないため、dwarf のデコードエラーが発生します。このリポジトリでは、テスト時に dwarf を読み取るために事前にバイナリをビルドしています。
- 危険です! このライブラリでは *g および allp を直接操作しています(ロックもリンクして使用していますが、より危険になる可能性があります)。ランタイムにどのような影響を与えるかはわかりませんので、テスト環境や負荷テスト環境での使用を試してください。
- パフォーマンス 実行時にバイナリを 1 回読み取ります。構造体のフィールドを取得するメソッドでは、シンボルの走査など、いくつかの時間がかかる可能性があります。