在欢总的 huandu/goroutine 指引下,我们知道了如何获取 goroutine id。简单概述一下目前的方式。首先是 goid 是当前 goroutine 的编号,存在于一个叫 g 的结构体上。这个结构体定义在 runtime 包里,是私有的。目前的版本 g struct 长这个样子
type g struct {
// Stack parameters.
// stack describes the actual stack memory: [stack.lo, stack.hi).
// stackguard0 is the stack pointer compared in the Go stack growth prologue.
// It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
// stackguard1 is the stack pointer compared in the C stack growth prologue.
// It is stack.lo+StackGuard on g0 and gsignal stacks.
// It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).
stack stack // offset known to runtime/cgo
stackguard0 uintptr // offset known to liblink
stackguard1 uintptr // offset known to liblink
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
m *m // current m; offset known to arm liblink
sched gobuf
syscallsp uintptr // if status==Gsyscall, syscallsp = sched.sp to use during gc
syscallpc uintptr // if status==Gsyscall, syscallpc = sched.pc to use during gc
stktopsp uintptr // expected sp at top of stack, to check in traceback
param unsafe.Pointer // passed parameter on wakeup
atomicstatus uint32
stackLock uint32 // sigprof/scang lock; TODO: fold in to atomicstatus
goid int64
waitsince int64 // approx time when the g become blocked
waitreason string // if status==Gwaiting
schedlink guintptr
preempt bool // preemption signal, duplicates stackguard0 = stackpreempt
paniconfault bool // panic (instead of crash) on unexpected fault address
preemptscan bool // preempted g does scan for gc
gcscandone bool // g has scanned stack; protected by _Gscan bit in status
gcscanvalid bool // false at start of gc cycle, true if G has not run since last scan; TODO: remove?
throwsplit bool // must not split stack
raceignore int8 // ignore race detection events
sysblocktraced bool // StartTrace has emitted EvGoInSyscall about this goroutine
sysexitticks int64 // cputicks when syscall has returned (for tracing)
traceseq uint64 // trace event sequencer
tracelastp puintptr // last P emitted an event for this goroutine
lockedm muintptr
sig uint32
writebuf []byte
sigcode0 uintptr
sigcode1 uintptr
sigpc uintptr
gopc uintptr // pc of go statement that created this goroutine
startpc uintptr // pc of goroutine function
racectx uintptr
waiting *sudog // sudog structures this g is waiting on (that have a valid elem ptr); in lock order
cgoCtxt []uintptr // cgo traceback context
labels unsafe.Pointer // profiler labels
timer *timer // cached timer for time.Sleep
selectDone uint32 // are we participating in a select and did someone win the race?
// Per-G GC state
// gcAssistBytes is this G's GC assist credit in terms of
// bytes allocated. If this is positive, then the G has credit
// to allocate gcAssistBytes bytes without assisting. If this
// is negative, then the G must correct this by performing
// scan work. We track this in bytes to make it fast to update
// and check for debt in the malloc hot path. The assist ratio
// determines how this corresponds to scan work debt.
gcAssistBytes int64
}
每个 goroutine 都有这样的一个 g 的结构体的实例,它的指针放在一个叫 TLS 的伪寄存器里面。获取 *g 的汇编代码
MOVQ TLS, CX
MOVQ 0(CX)(TLS*1), AX
MOVQ AX, ret+0(FP)
编写一个 getg 的实现是很容易的,用 Go 的汇编,给不同的几个架构写一个对应的实现就可以了。接下来的问题是如何在 g 的指针上获取其 goid 这个成员变量。问题的难度在于不同的 Go 版本的偏移量是不一样的。
所以 huandu/goroutine 实现的办法就是把各个版本的 g 的结构体定义都拷贝一份出来。然后运行时去判断 Go 的版本号,把指针转成对应版本的结构体的指针。
这里我们提供一种另外的办法
像 Class.forName 一样用名字获取 reflect.Type
在 Java 里,可以很容易地用 class 的名字获得这个 class 的反射对象。但是在 Go 里面并没有提供这样的选项。所以我们要获得 runtime.g 的定义,先要实现一个 Go 版本的 Class.forName
//+build go1.5
package gls
import (
"unsafe"
"reflect"
"runtime"
"strings"
)
// typelinks1 for 1.5 ~ 1.6
//go:linkname typelinks1 reflect.typelinks
func typelinks1() [][]unsafe.Pointer
// typelinks2 for 1.7 ~
//go:linkname typelinks2 reflect.typelinks
func typelinks2() (sections []unsafe.Pointer, offset [][]int32)
var types = map[string]reflect.Type{}
func init() {
ver := runtime.Version()
if ver == "go1.5" || strings.HasPrefix(ver, "go1.5.") {
loadGo15Types()
} else if ver == "go1.6" || strings.HasPrefix(ver, "go1.6.") {
loadGo15Types()
} else {
loadGo17Types()
}
gType := TypeForName("runtime.g")
if gType == nil {
panic("failed to get runtime.g type")
}
goidField, found := gType.FieldByName("goid")
if !found {
panic("failed to get goid from runtime.g type")
}
goidOffset = goidField.Offset
}
func loadGo15Types() {
var obj interface{} = reflect.TypeOf(0)
typePtrss := typelinks1()
for _, typePtrs := range typePtrss {
for _, typePtr := range typePtrs {
(*emptyInterface)(unsafe.Pointer(&obj)).word = typePtr
typ := obj.(reflect.Type)
if typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Struct {
types[typ.Elem().String()] = typ.Elem()
}
if typ.Kind() == reflect.Slice && typ.Elem().Kind() == reflect.Ptr &&
typ.Elem().Elem().Kind() == reflect.Struct {
types[typ.Elem().Elem().String()] = typ.Elem().Elem()
}
}
}
}
func loadGo17Types() {
var obj interface{} = reflect.TypeOf(0)
sections, offset := typelinks2()
for i, offs := range offset {
rodata := sections[i]
for _, off := range offs {
(*emptyInterface)(unsafe.Pointer(&obj)).word = resolveTypeOff(unsafe.Pointer(rodata), off)
typ := obj.(reflect.Type)
if typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Struct {
types[typ.Elem().String()] = typ.Elem()
}
}
}
}
type emptyInterface struct {
typ unsafe.Pointer
word unsafe.Pointer
}
// TypeForName return the type by its name, just like Class.forName in java
func TypeForName(typeName string) reflect.Type {
return types[typeName]
}
其实利用 reflect.typelinks() 这个函数是可以枚举所有的类型的。但是这个函数是 reflect 包私有的。所以我们先要用 go:linkname 把这个私有函数链接进来。
然后我们获得的类型是用 reflect.rtype 的指针来表示的。我们还要把这个具体类型的指针转成 reflect.Type 这样的接口。这里使用的办法是先用 interface{} 包含一个 *rtype。然后把 interface{} 里的指针给换掉。然后这个 interface{} 转成 reflect.Type。
拿到了 runtime.g 的 reflect.Type,获取 goid 的偏移量,直接使用反射的api,去取 field 的 offset 就可以了。
有 goid,我们就可以实现一个类似 tls 的 gls 了: v2pro/plz
参考资料
How to call private functions (bind to hidden symbols) in GoLangAdventures in Go: Accessing Unexported Functionsspance/go-callprivate有疑问加站长微信联系(非本文作者)