diff --git a/SUMMARY.md b/SUMMARY.md index b8ccb5b..00fd8f6 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -29,7 +29,8 @@ * [3.4. 函数](ch3-asm/ch3-04-func.md) * [3.5. 控制流](ch3-asm/ch3-05-control-flow.md) * [3.6. 再论函数](ch3-asm/ch3-06-func-again.md) - * [3.7. 补充说明](ch3-asm/ch3-07-faq.md) + * [3.7. 例子:Goroutine ID](ch3-asm/ch3-07-goroutine-id) + * [3.8. 补充说明](ch3-asm/ch3-08-faq.md) * [第四章 RPC和Protobuf](ch4-rpc/readme.md) * [4.1. RPC入门(TODO)](ch4-rpc/ch4-01-rpc-intro.md) * [4.2. Protobuf简介(TODO)](ch4-rpc/ch4-02-pb-intro.md) diff --git a/ch3-asm/ch3-07-goroutine-id.md b/ch3-asm/ch3-07-goroutine-id.md new file mode 100644 index 0000000..e1d9ce2 --- /dev/null +++ b/ch3-asm/ch3-07-goroutine-id.md @@ -0,0 +1,354 @@ +# 3.7. 例子:Goroutine ID + +在操作系统中,每个进程都会有一个唯一的进程编号,每个线程也有自己唯一的线程编号。同样在Go语言中,每个Goroutine也有自己唯一的Go程编号,这个编号在panic等场景下经常遇到。虽然Goroutine有内在的编号,但是共语言却刻意没有提供获取改编号的接口。本节我们尝试通过Go汇编语言获取Goroutine ID。 + +## 故意设计没有goid + +根据官方的相关资料显示,Go语言刻意没有提供goid的原因是为了避免被滥用。因为大部分用户在轻松拿到goid之后,在之后的编程中会不自觉地编写出强依赖goid的代码。强依赖goid将导致这些代码不好移植,同时也会导致并发模型复杂化。同时,Go语言中可能同时存在海量的Goroutine,但是每个Goroutine合适被销毁并不好实时监控,这也会导致依赖goid的资源无法很好地自动回收(需要手工回收)。如果你是Go汇编语言用户,完全可以忽略这些借口。 + +## 纯Go方式获取goid + +为了便于理解,我们先尝试用纯Go的方式获取goid。使用纯Go的方式获取goid的方式虽然性能较低,但是代码有着很好的移植性,同时也可以用于测试其它方式获取的goid是否正确。 + +每个Go语言用户应该都知道panic函数。panic函数将导致Goroutine异常,如果panic在传递到Goroutine的根函数还没有被recover函数处理,那么将打印相关的信息并退出进程。 + +下面是我们构造的panic例子: + +```go +package main + +func main() { + panic("goid") +} +``` + +运行后将输出以下信息: + +``` +panic: goid + +goroutine 1 [running]: +main.main() + /path/to/main.go:4 +0x40 +``` + +我们可以猜测Panic输出信息`goroutine 1 [running]`中的1就是goid。但是如何才能在程序中获取panic的输出信息呢?其实上述信息只是当前函数调用栈帧的文字化描述,runtime.Stack函数提供了获取该信息的功能。 + +我们基于runtime.Stack函数重新构造一个例子,输出当前栈帧的信息: + +```go +package main + +import "runtime" + +func main() { + var buf = make([]byte, 64) + var stk = buf[:runtime.Stack(buf, false)] + print(string(stk)) +} +``` + +运行后将输出以下信息: + +``` +goroutine 1 [running]: +main.main() + /path/to/main.g +``` + +因此通过runtime.Stack获取的字符串中就可以解析出goid信息: + +```go +func GetGoid() int64 { + var buf [64]byte + n := runtime.Stack(buf[:], false) + idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0] + id, err := strconv.Atoi(idField) + if err != nil { + panic(fmt.Errorf("can not get goroutine id: %v", err)) + } + return int64(id) +} +``` + +GetGoid函数的细节我们不在赘述。需要补充说明的是`runtime.Stack`函数不仅仅可以获取当前Goroutine的栈信息,还可以获取全部Goroutine的栈信息(通过第二个参数控制)。同时在Go语言内部的 [net/http2.curGoroutineID](https://github.com/golang/net/blob/master/http2/gotrack.go) 函数正是采用类似方式获取的goid。 + + +## 从g结构体获取goid + +根据官方的Go汇编语言文档,每个运行的Goroutine结构的g指针保存在当前运行Goroutine的系统线程的局部存储TLS中。可以先获取TLS线程局部存储,然后再从TLS中获取g结构的指针。 + +下面是参考runtime包中定义的get_tls宏获取g指针: + +``` +get_tls(CX) +MOVQ g(CX), AX // Move g into AX. +``` + +其中get_tls是一个宏函数,在 [runtime/go_tls.h](https://github.com/golang/go/blob/master/src/runtime/go_tls.h) 头文件中定义。 + +对于AMD64平台,get_tls宏函数定义如下: + +``` +#ifdef GOARCH_amd64 +#define get_tls(r) MOVQ TLS, r +#define g(r) 0(r)(TLS*1) +#endif +``` + +将get_tls宏函数展开之后,获取g指针的代码如下: + +``` +MOVQ TLS, CX +MOVQ 0(CX)(TLS*1), AX +``` + +其实TLS类似线程局部存储的地址,地址对应的内存里的数据才是g指针。我们可以更直接一点: + +``` +MOVQ (TLS), AX +``` + +基于上述方法可以包装一个getg函数,用于获取g指针: + +``` +// func getg() unsafe.Pointer +TEXT ·getg(SB), NOSPLIT, $0-8 + MOVQ (TLS), AX + MOVQ AX, ret+0(FP) + RET +``` + +然后在Go代码中通过goid成员在g结构体中的偏移量来获取goid的值: + +```go +const g_goid_offset = 152 // Go1.10 + +func GetGroutineId() int64 { + g := getg() + p := (*int64)(unsafe.Pointer(uintptr(g) + g_goid_offset)) + return *p +} +``` + +其中 `g_goid_offset` 是 goid 成员的偏移量,g 结构参考 [runtime/runtime2.go](https://github.com/golang/go/blob/master/src/runtime/runtime2.go)。 + +在Go1.10版本,goid的偏移量是152字节。因此上述代码只能正确运行在goid偏移量也是152字节的Go版本中。 + +根据汤普森大神的神谕,枚举和暴力穷举是解决一切疑难杂症的万金油。我们也可以将goid的偏移保存到表格中,然后根据Go版本号查询goid的偏移量。 + +下面是改进后的代码: + +```go +var offsetDictMap = map[string]int64{ + "go1.10": 152, + "go1.9": 152, + "go1.8": 192, +} + +var g_goid_offset = func() int64 { + goversion := runtime.Version() + for key, off := range offsetDictMap { + if goversion == key || strings.HasPrefix(goversion, key) { + return off + } + } + panic("unsupport go verion:"+goversion) +}() +``` + +现在的goid偏移量已经终于可以自动适配已经发布的Go语言版本。 + + +## 获取g结构体对应的接口对象 + +枚举和暴力穷举虽然够直接,但是对于正在开发中的未发布的Go版本支持并不好,我们无法提前知晓开发中的某个版本的goid成员的偏移量。 + +如果是在runtime包内部,我们可以通过`unsafe.OffsetOf(g.gois)`直接获取成员的偏移量。也可以通过反射获取g结构体的类型,然后通过类型查询某个成员的偏移量。因为g结构体是一个内部类型,Go代码无法从外部包获取g结构体的类型信息。但是再Go汇编语言中,我们是可以看到全部的符号的,因此理论上我们也可以获取g结构体的类型信息。 + +在任意的类型被定义之后,Go语言都会为该类型生成对应的类型信息。比如g结构体会生成一个`type·runtime·g`标识符表示g结构体的值类型信息,同时还有一个`type·*runtime·g`标识符表示指针类型的信息。如果g结构体带有方法,那么同时还会生成`go.itab.runtime.g`和`go.itab.*runtime.g`类型信息,用于表示带方法的类型信息。 + +如果我们能够拿到表示g结构体类型的`type·runtime·g`和g指针,那么就可以构造g对象的接口。下面是改进的getg函数,返回g指针对象的接口: + +``` +// func getg() interface{} +TEXT ·getg(SB), NOSPLIT, $32-16 + // get runtime.g + MOVQ (TLS), AX + // get runtime.g type + MOVQ $type·runtime·g(SB), BX + + // convert (*g) to interface{} + MOVQ AX, 8(SP) + MOVQ BX, 0(SP) + CALL runtime·convT2E(SB) + MOVQ 16(SP), AX + MOVQ 24(SP), BX + + // return interface{} + MOVQ AX, ret+0(FP) + MOVQ BX, ret+8(FP) + RET +``` + +其中AX寄存器对应g指针,BX寄存器对应g结构体的类型。然后通过runtime·convT2E函数将类型转为接口。因为我们使用的不是是g结构体指针类型,因此返回的接口表示的g结构体值类型。理论上我们也可以构造g指针类型的接口,但是因为Go汇编语言的限制,我们无法`type·*runtime·g`标识符。 + +基于g返回的接口,就可以容易获取goid了: + +```go +func GetGoid() int64 { + g := getg() + gid := reflect.ValueOf(g).FieldByName("goid").Int() + return goid +} +``` + +上述代码通过反射直接获取goid,理论上只要反射的接口和goid成员的名字不发生变化,代码都可以正常运行。经过实际测试,以上的代码可以在Go1.8+版本中正确运行。 + +反射虽然具备一定的灵活性,但是反射的性能一直是被大家诟病的地方。一个改进的思路是通过反射获取goid的偏移量,然后通过g指针和偏移量获取goid,这样反射只需要在初始化阶段执行一次。 + +下面是g_goid_offset变量的初始化代码: + +```go +var g_goid_offset uintptr = func() uintptr { + g := GetGroutine() + if f, ok := reflect.TypeOf(g).FieldByName("goid"); ok { + return f.Offset + } + panic("can not find g.goid field") +}() +``` + +有了正确的goid偏移量之后,采用前面讲过的方式获取goid: + + +```go +func GetGroutineId() int64 { + g := getg() + p := (*int64)(unsafe.Pointer(uintptr(g) + g_goid_offset)) + return *p +} +``` + +至此我们获取goid的实现思路已经足够完善了,不过汇编的代码依然有严重的安全隐患。 + +虽然getg函数是用NOSPLIT标志声明的禁止栈分裂的函数类型,但是getg内部又调用了更为复杂的runtime·convT2E函数。runtime·convT2E函数如果遇到栈空间不足,可能触发栈分裂的操作。而栈分裂时,GC将要挪动栈上所有函数的参数和返回值和局部变量中的栈指针。但是我们的getg函数并没有提供局部变量的指针信息。 + +下面是改进后的getg函数的完整实现: + +``` +// func getg() interface{} +TEXT ·getg(SB), NOSPLIT, $32-16 + NO_LOCAL_POINTERS + + MOVQ $0, ret_type+0(FP) + MOVQ $0, ret_data+8(FP) + GO_RESULTS_INITIALIZED + + // get runtime.g + MOVQ (TLS), AX + + // get runtime.g type + MOVQ $type·runtime·g(SB), BX + + // convert (*g) to interface{} + MOVQ AX, 8(SP) + MOVQ BX, 0(SP) + CALL runtime·convT2E(SB) + MOVQ 16(SP), AX + MOVQ 24(SP), BX + + // return interface{} + MOVQ AX, ret_type+0(FP) + MOVQ BX, ret_data+8(FP) + RET +``` + +其中NO_LOCAL_POINTERS表示函数没有局部指针变量。同时对返回的接口进行零值初始化,初始化完成后通过GO_RESULTS_INITIALIZED告知GC。这样可以在保证栈分裂是,GC能够正确处理返回值和局部变量中的指针。 + + +## goid的应用: 局部存储 + +有了goid之后,构造Goroutine局部存储就非常容易了: + +```go +var gls struct { + m map[int64]map[interface{}]interface{} + sync.Mutex +} + +func init() { + gls.m = make(map[int64]map[interface{}]interface{}) +} +``` + +gls包变量简单包装了map,同时通过`sync.Mutex`互斥量支持并发访问。 + +然后定义一个getMap内部函数,用于获取每个Goroutine字节的map: + +```go +func getMap() map[interface{}]interface{} { + gls.Lock() + defer gls.Unlock() + + goid := GetGoid() + if m, _ := gls.m[goid]; m != nil { + return m + } + + m := make(map[interface{}]interface{}) + gls.m[goid] = m + return m +} +``` + +获取到Goroutine私有的map之后,就是正常的增、删、改操作接口了: + +```go +func Get(key interface{}) interface{} { + return getMap()[key] +} +func Put(key interface{}, v interface{}) { + getMap()[key] = v +} +func Delete(key interface{}) { + delete(getMap(), key) +} +``` + +最后我们再提供一个Clean函数,用于是否Goroutine对应的map资源: + +```go +func Clean() { + gls.Lock() + defer gls.Unlock() + + delete(gls.m, GetGoid()) +} +``` + +这样一个极简的Goroutine局部存储gls对象就完成了。 + +下面是使用局部存储简单的例子: + +```go +func main() { + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(1) + go func(idx int) { + defer wg.Done() + defer gls.Clean() + + defer func() { + fmt.Printf("%d: number = %d\n", idx, gls.Get("number")) + }() + gls.Put("number", idx+100) + }(i) + } + wg.Wait() +} +``` + +通过Goroutine局部存储,不同层次函数之间可以共享存储资源。同时未来避免资源泄露,需要再Goroutine的根函数中,通过defer语句调用gls.Clean()函数释放资源。 + diff --git a/ch3-asm/ch3-07-faq.md b/ch3-asm/ch3-08-faq.md similarity index 97% rename from ch3-asm/ch3-07-faq.md rename to ch3-asm/ch3-08-faq.md index c1155da..3036cb2 100644 --- a/ch3-asm/ch3-07-faq.md +++ b/ch3-asm/ch3-08-faq.md @@ -1,3 +1,3 @@ -# 3.9. 补充说明 +# 3.8. 补充说明 得益于Go语言的设计,Go汇编语言的优势也非常明显:跨操作系统、不同CPU之间的用法也非常相似、支持C语言预处理器、支持模块。同时Go汇编语言也存在很多不足:它不是一个独立的语言,底层需要依赖Go语言甚至操作系统;很多高级特性很难通过手工汇编完成。虽然Go语言官方尽量保持Go汇编语言简单,但是汇编语言是一个比较大的话题,大到足以写一本Go汇编语言的教程。本章的目的是让大家对Go汇编语言简单入门,在看到底层汇编代码的时候不会一头雾水,在某些遇到性能或禁制的场合能够通过Go汇编突破限制。这只是一个开始,后续版本会继续完善。 diff --git a/ch3-asm/ch3-09-goroutine-id.md b/ch3-asm/ch3-09-goroutine-id.md deleted file mode 100644 index e863262..0000000 --- a/ch3-asm/ch3-09-goroutine-id.md +++ /dev/null @@ -1,3 +0,0 @@ -# 3.9. 例子:Goroutine ID(TODO) - -TODO diff --git a/ch3-asm/ch3-07-interface.md b/ch3-asm/ch3-xx-interface.md similarity index 100% rename from ch3-asm/ch3-07-interface.md rename to ch3-asm/ch3-xx-interface.md diff --git a/images/ch3-02-x86-64.jpg b/images/ch3-02-x86-64.jpg deleted file mode 100644 index d97f675..0000000 Binary files a/images/ch3-02-x86-64.jpg and /dev/null differ diff --git a/images/ch3-02-x86.jpg b/images/ch3-02-x86.jpg deleted file mode 100644 index f6813b2..0000000 Binary files a/images/ch3-02-x86.jpg and /dev/null differ diff --git a/images/ch3-func-local-var-01.ditaa.png b/images/ch3-func-local-var-01.ditaa.png index efeae60..1f021b8 100644 Binary files a/images/ch3-func-local-var-01.ditaa.png and b/images/ch3-func-local-var-01.ditaa.png differ diff --git a/images/ch3-func-local-var-01.ditaa.txt b/images/ch3-func-local-var-01.ditaa.txt index 59f1890..23cf23a 100644 --- a/images/ch3-func-local-var-01.ditaa.txt +++ b/images/ch3-func-local-var-01.ditaa.txt @@ -19,35 +19,35 @@ | | | | +--------------------+ | | | | | | | | | | | +--=->| var c.Cap int |--==+ - argsize+----------+ | | cGRE | | - ^ | | | +--------------------+ | - | | | | | - | | arguments| | | - | | returns | | | - | | | | local_top-0(SP)---=-+ | - v | cYEL |arg+0(FP) | | | - 0 +----------+ <----------=--- +-------------------------------------+ <-+ | - ^ | cGRE |var-0(SP) | | cGRE | | - | | local | | | c.Cap | | - | | | | | 64bit | | - | | | | +-------------------------------------+ <-=-------------+c.Cap-8(SP) - | | | | | cGRE | | - | | | | | c.Len | | - | | | | | 64bit | | - | | | | +-------------------------------------+ <-=-------------+c.Len-16(SP) - | | | | | cGRE | | - | | | | | c.Data | | - | | | | | 64bit | | - | | | | +----+----+--------+------------------+ <-=-------------+c.Data-24(SP) - | | | : |cBLU|cAAA|cPNK |cAAA | - | | | +----> | a | | b | | - | | | |8bit|8bit| 16bit | 32bit | - | +----------+ <----------=--- +----+----+--------+------------------+ <--=a-32(SP) - | |for callee| var-32(SP) ^ ^ - | | | | | | - | | | | | +-=-b-30(SP) - v |cPNK v | | -framesize+----------+0(SP) +-=-a-32(SP) + ---=- +----------+ | | cGRE | | + ^ | | | +--------------------+ | + : | | | | + | | arguments| | | + argsize| returns | | | + : | | | local_top-0(SP)---=-+ | + v | cYEL |arg+0(FP) | | | + --=-- +----------+ <----------=--- +-------------------------------------+ <-+ | + ^ | cGRE |var-0(SP) | | cGRE | | + : | local | | | c.Cap | | + | | | | | 64bit | | + | | | | +-------------------------------------+ <-=-------------+c.Cap-8(SP) + | | | | | cGRE | | + | | | | | c.Len | | + | | | | | 64bit | | + | | | | +-------------------------------------+ <-=-------------+c.Len-16(SP) + | | | | | cGRE | | +framesize| | | | c.Data | | + | | | | | 64bit | | + | | | | +----+----+--------+------------------+ <-=-------------+c.Data-24(SP) + | | | : |cBLU|cAAA|cPNK |cAAA | + | | | +----> | a | | b | | + | | | |8bit|8bit| 16bit | 32bit | + | +----------+ <----------=--- +----+----+--------+------------------+ <--=a-32(SP) + | |for callee| var-32(SP) ^ ^ + | | | | | | + : | | | | +-=-b-30(SP) + v |cPNK v | | + -=-- +----------+0(SP) +-=-a-32(SP) | unused | | cAAA | +----------+