Skip to content

Commit

Permalink
Merge pull request #1 from tiancaiamao/master
Browse files Browse the repository at this point in the history
Sync with the master.
  • Loading branch information
NanXiao committed May 9, 2014
2 parents 0a91ae7 + 05b7937 commit e608f98
Show file tree
Hide file tree
Showing 11 changed files with 158 additions and 77 deletions.
20 changes: 1 addition & 19 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,21 +1,3 @@
*~
go/pkg/*
go/bin/*
go/test/bench/shootout/*
go/doc/articles/wiki/get.bin
go/src/cmd/6l/enam.c
go/src/cmd/gc/opnames.h
go/src/pkg/runtime/zasm_darwin_amd64.h
go/src/pkg/runtime/zgoarch_amd64.go
go/src/pkg/runtime/zgoos_darwin.go
go/src/pkg/runtime/zmalloc_darwin_amd64.c
go/src/pkg/runtime/zmprof_darwin_amd64.c
go/src/pkg/runtime/znetpoll_darwin_amd64.c
go/src/pkg/runtime/zruntime1_darwin_amd64.c
go/src/pkg/runtime/zruntime_defs_darwin_amd64.go
go/src/pkg/runtime/zsema_darwin_amd64.c
go/src/pkg/runtime/zsigqueue_darwin_amd64.c
go/src/pkg/runtime/zstring_darwin_amd64.c
go/src/pkg/runtime/ztime_darwin_amd64.c
go/src/pkg/runtime/zversion.go
ebook/_book
*.html
12 changes: 4 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
# 《深入解析Go》
因为自己对Go底层的东西比较感兴趣,所以最近抽空在写一本开源的书籍《深入解析Go》。写这本书不表示我能力很强,而是我愿意分享,和大家一起分享对Go语言的内部实现的一些研究。
因为自己对Go底层的东西比较感兴趣,所以抽空在写一本开源的书籍《深入解析Go》。写这本书不表示我能力很强,而是我愿意分享,和大家一起分享对Go语言的内部实现的一些研究。

我一直认为知识是用来分享的,让更多的人分享自己拥有的一切知识这个才是人生最大的快乐。

这本书目前我放在Github上,我现在基本每天晚上抽空会写一些,时间有限、能力有限,所以希望更多的朋友参与到这个开源项目中来。
这本书目前我放在Github上,时间有限、能力有限,所以希望更多的朋友参与到这个开源项目中来。

# 参与到本项目

你可以fork一份代码,如果对某些章节很有兴趣,可以写作相应章节的内容并pull request给我。

或者你也正在读Go的源代码,可以给代码加入中文注释。
如果对某些章节很有兴趣,可以写作相应章节的内容并pull request给我。如果觉得有哪些相关的内容缺失,欢迎提出。如果发现书中内容有错误或者疏漏,欢迎指正。

不管任何形式的参与都是非常受欢迎的。

# 通过捐款支持谢大的书

本书使用的[astaxie](https://github.com/astaxie/build-web-application-with-golang)的模板。如果你喜欢这本《深入解析Go》的话, 可以给他捐款, 支持他继续为Go语言,为开源事业做出贡献。

捐款地址: [https://me.alipay.com/astaxie](https://me.alipay.com/astaxie)


## 交流
欢迎大家加入QQ群:259316004 《Go Web编程》专用交流群 我在里面潜水

论坛交流:http://bbs.mygolang.com/forum.php 源代码分析版块

## 致谢
先留空

Expand Down
4 changes: 2 additions & 2 deletions ebook/02.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Rect1是一个具有两个Point类型属性的结构体,由在一行的两个P

使用过C的程序员可能对Point属性和`*Point`属性的不同毫不见怪,但用惯Java或Python的程序员们可能就不那么轻松了。Go语言给了程序员基本内存层面的控制,由此提供了诸多能力,如控制给定数据结构集合的总大小、内存分配的次数、内存访问模式以及建立优秀系统的所有要点。

## 字体串
## 字符串

有了前面的准备,我们就可以开始研究更有趣的数据类型了。

Expand All @@ -51,6 +51,6 @@ Rect1是一个具有两个Point类型属性的结构体,由在一行的两个P

## links
* [目录](<preface.md>)
* 上一节: [基本数据结构]
* 上一节: [基本数据结构](<02.0.md>)
* 下一节: [slice](<02.2.md>)

6 changes: 5 additions & 1 deletion ebook/02.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

什么?nil是一种数据结构么?为什么会讲到它,没搞错吧?

没搞错。nil是非常重要的数据类型(虽然叫数据结构并不准确),不仅仅是Go语言中,每门语言中nil都是非常重要的。而且在不同语言中,表示空这个概念都有细微不同。拿scheme为例(一种lisp方言),nil是true的!而在ruby中,什么都是对象,连nil也是一个对象!在C中NULL跟0是等价的。Go跟Java一个抽象层次比C高,但跟Java不同的是它的内置类型并不是等价于一个指针(比如slice就不是一个指针)。
没搞错。nil非常重要(虽然叫数据结构并不准确),不仅仅是Go语言中,每门语言中nil都是非常重要的。在不同语言中,表示空这个概念都有细微不同。比如在scheme语言(一种lisp方言)中,nil是true的!而在ruby语言中,一切都是对象,连nil也是一个对象!在C中NULL跟0是等价的。Go跟Java一个抽象层次比C高,但跟Java不同的是它的内置类型并不是等价于一个指针(比如slice就不是一个指针)。

按照Go语言规范,任何类型在未初始化时都对应一个零值:布尔类型是false,整型是0,字符串是"",而指针,函数,interface,slice,channel和map的零值都是nil。

## 空的interface

指向空interface的指针不是空指针。

## interface值为空

## 空的string
Expand Down
84 changes: 40 additions & 44 deletions ebook/03.5.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,32 @@
# 3.5 分裂栈
# 3.5 拷贝栈

Go语言中使用的是栈不是连续的,原因是需要支持goroutine。假如每个goroutine分配固定栈大小并且不能增长,太小则会导致溢出,太大又会浪费空间,无法开许多的goroutine。分段栈的重要意义就在于,可以初始时只给栈分配很小的空间,然后随着使用过程中的需要自动地增长。这就是为什么Go可以开千千万万个goroutine而不会耗尽内存。
Go语言支持goroutine,每个goroutine需要能够运行,所以它们都有自己的栈。假如每个goroutine分配固定栈大小并且不能增长,太小则会导致溢出,太大又会浪费空间,无法开许多的goroutine。为了解决这个问题,goroutine可以初始时只给栈分配很小的空间,然后随着使用过程中的需要自动地增长。这就是为什么Go可以开千千万万个goroutine而不会耗尽内存。

为了实现栈增长,Go1.3版本以下使用的是分裂栈技术,而1.3版本之后则使用的是拷贝栈。这里将具体分析一下Go语言的拷贝栈技术。

## 基本原理
为了便于理解,可以拿操作系统中的分页和Go语言的分裂栈进行类比。在操作系统中,每个进程可以使用整个地址空间,并不会感觉到它使用的内存是一页一页不连续的。其原理就在于操作系统会处理好底层的细节使得上层看起来像是一个逻辑上连续的内存空间。假如进程访问到一个逻辑地址,该地址还没有分配物理内存,则此时会发生缺页中断。操作系统会把发生中断时的上下文保存下来,然后为进程分配一页大小的物理内存,设置好映射关系,最后恢复中断发生前的上下文。进程继续运行时,并不会感觉到缺页中断的整个过程。

分裂栈也是很类似的。当栈的大小不够用时,会触发“中断”,从当前函数进入到Go的运行时库,Go的运行时库会保存此时的函数上下文环境,然后分配一个新的足够大的栈空间,并做一些设置,使得当函数恢复运行时,它会在新分配的栈中继续执行,仿佛整个过程都没发生过一样,这个函数会觉得自己使用的是一块大小“无限”的连续栈空间。
每次执行函数调用时Go的runtime都会进行检测,当栈的大小不够用时,会触发“中断”,从当前函数进入到Go的运行时库,Go的运行时库会保存此时的函数上下文环境,然后分配一个新
的足够大的栈空间,将旧栈的内容拷贝到新栈中,并做一些设置,使得当函数恢复运行时,函数会在新分配的栈中继续执行,仿佛整个过程都没发生过一样,这个函数会觉得自己使用的
是一块大小“无限”的栈空间。

## 实现过程
在研究Go的实现细节之前让我们先自己思考一下应该如何实现。第一步肯定要有某种机制检测到当前栈大小不够用了,这个应该是把当前的栈寄存器SP跟栈的可用栈空间的边界进行比较。能够检测到栈大小不够用,就相当于捕捉到了“中断”。那么第二步要做的,就应该是进入运行时,保存当前的上下文。别陷入如何保存上下文的细节,先假如我们把函数栈增长时的上下文保存好了,那下一步就是分配新的栈空间了,我们可以将分配空间想象成就是调用一下malloc而已。

接下来怎么办呢?如何让函数在新的栈中继续运行,好像没发生过栈分裂一样?可能很多人会卡在这一步了。我们可以把SP直接设置为新栈空间的顶部,然后用JMP指令而不是CALL指令直接跳到保存PC寄存器的位置。这样确实是让函数在新的栈中继续运行了,但是有个问题,函数返回时会出错。因为我们没有设置好函数返回信息,栈增长后,函数在新栈中执行完却无法回到旧的栈了。

为了解决返回的问题,可以装饰一下新栈的顶部,并引入一个辅助函数,使函数在新栈中返回时是返回到这个辅助函数。这个辅助函数做的事情跟前面的过程正好相反,它会将SP设置回旧栈地址,然后JMP到发生“中断”时的PC。这样就可以恢复到整个旧栈的上下文环境了。

函数调用时的内存布局前面小节已经讲过了,依次是:为返回值保留的空间,传入的参数,保存返回地址,然后就是被调函数的栈的。进入函数后的前几条指令并不像普通C语言那样是push %ebp; mov %esp %ebp这样子,这个细节后面再看。
在研究Go的实现细节之前让我们先自己思考一下应该如何实现。第一步肯定要有某种机制检测到当前栈大小不够用了,这个应该是把当前的栈寄存器SP跟栈的可用栈空间的边界进
行比较。能够检测到栈大小不够用,就相当于捕捉到了“中断”。

可以想象一下切换到新栈时,栈顶空间大致应该是被装饰成这样子的:
捕获完“中断”,第二步要做的,就应该是进入运行时,保存当前的上下文。别陷入如何保存上下文的细节,先假如我们把函数栈增长时的上下文保存好了,那下一步就是分配新的栈空间了,我们可以将分配空间想象成就是调用一下malloc而已

... <-新栈空间顶部
旧栈参数3
旧栈参数2
旧栈参数1
辅助函数地址
... <-SP
接下来怎么办呢?我们要将旧栈中的内容拷贝到新栈中,然后让函数继续在新栈中运行。这里先暂时忽略旧栈内容拷贝到新栈中的一些技术难点,假设在新栈空间中恢复了“中断”时
的上下文,从运行时返回到函数。

到新栈以后,函数继续运行,并不会感觉到发生过从旧栈切换到新栈的过程。当函数返回时,RET指令会使用辅助函数地址覆盖当前PC
函数在新的栈中继续运行了,但是还有个问题:函数如何返回。因为函数返回后栈是要缩小的,否则就会内存浪费空间了,所以还需要在函数返回时处理栈缩小的问题

## 具体细节
Go语言中有个结构体G,用于描述goroutine。这个结构中存了stackbase和stackguard,用于确定这个goroutine使用的栈空间。对于使用分段栈的函数,每次进入函数后,前几条指令就是先比较SP跟g->stackguard,检测是否发生溢出。如果栈寄存器超过了stackguard就需要要扩展栈空间。

所有用于分配栈空间的函数本身不能使用分段栈,引入一个属性来控制函数的生成。当然,还要保证有足够的空间来调用分配函数。编译器在编译阶段加入特殊的标签,链接时对于检测到有这些标签的函数,就会插入特殊指令。扩展完之后,使用新栈之间,需要一些处理。
### 如何捕获函数的栈空间不足?

基于原栈SP的数据都可以直接复制到新栈中来。对于栈中存的是对象的地址,这样做不会造成问题。对于带参函数,因为参数不能直接搬,编译时要特殊处理.函数使用的参数指针不是基于栈帧的.对于在栈中返回对象的函数,对象必须返回到原栈上。

扩展栈时,函数的返回地址会被修改成一个函数,这个函数会释放分配的栈块,将栈指针重新设置成调用者旧栈块的地址,栈指针等,需要在新栈空间中的某处保存着。
Go语言和C不同,不是使用栈指针寄存器和栈基址寄存器确定函数的栈的。在Go的运行时库中,每个goroutine对应一个结构体G。这个结构体中存了stackbase和stackguard,用于确定这个goroutine使用的栈空间信息。对于使用拷贝栈的函数,每个函数调用前几条指令,先比较栈指针寄存器跟g->stackguard,检测是否发生栈溢出。如果栈指针寄存器值超越了stackguard就需要扩展栈空间。

为了加深理解,下面让我们跟踪一下代码,并看看实际生成的汇编吧。首先写一个test.go文件,内容如下:

Expand All @@ -47,37 +39,33 @@ Go语言中有个结构体G,用于描述goroutine。这个结构中存了stack
```

然后生成汇编文件:

```sh
go tool 6g -S test.go | head -8
```

可以看以输出是:

0000 (test.go:3) TEXT main+0(SB),$0-0
0001 (test.go:3) LOCALS ,$0
0002 (test.go:4) CALL ,main+0(SB)
0003 (test.go:5) RET ,
000000 00000 (test.go:3) TEXT "".main+0(SB),$0-0
000000 00000 (test.go:3) MOVQ (TLS),CX
0x0009 00009 (test.go:3) CMPQ SP,(CX)
0x000c 00012 (test.go:3) JHI ,21
0x000e 00014 (test.go:3) CALL ,runtime.morestack00_noctxt(SB)
0x0013 00019 (test.go:3) JMP ,0
0x0015 00021 (test.go:3) NOP ,

让我们用链接器进行反汇编:
让我们好好看一下这些指令。(TLS)取到的是结构体G的第一个域,也就是g->stackguard地址,将它赋值给CX。然后CX地址的值与SP进行比较,如果SP大于g->stackguard了,则会调用runtime.morestack00函数。这几条指令的作用就是检测栈是否溢出。

```sh
go tool 6l -a test.6 | head -10
```
不过并不是所有函数在链接时都会插入这种指令。如果你读源代码,可能会发现"#pragma textflag 7",或者在汇编函数中看到"TEXT reuntime.exit(SB),7,$0",这种函数就是不会检测栈溢出的。这个是编译标记,控制是否生成栈溢出检测指令。

注意观察输出:
##

002000 main.main | (3) TEXT main.main+0(SB),$0
002000 65488b0c25a0080000 | (3) MOVQ 2208(GS),CX
002009 483b21 | (3) CMPQ SP,(CX)
00200c 7705 | (3) JHI ,2013
00200e e84d760100 | (3) CALL ,19660+runtime.morestack00
002013 | (3) NOP ,
002013 | (3) NOP ,
002013 e8e8ffffff | (4) CALL ,2000+main.main
002018 c3 | (5) RET ,

会发现链接器的输出跟编译器的输出是不同的。链接器中插入了一些指令,让我们好好看一下在CALL main.main之前插入的这些指令。(GS)取到的是结构体G的第一个域,也就是g->stackguard,将它赋值给CX。然后与SP进行比较,如果SP大于g->stackguard了,则会调用runtime.morestack00。插入的指令作用就是检测栈是否溢出。编译生成的汇编是没有插入指令的,说明分裂栈是在链接过程中实现的,而不是在编译过程中实现的。编译还是按正常方式编译的,而链接时会在每个函数前面插入检测栈溢出的指令。哇,跟go和defer关键字一样的精妙!不必在编译层做修改,没有打破原有的协议。
基于原栈SP的数据都可以直接复制到新栈中来。对于栈中存的是对象的地址,这样做不会造成问题。对于带参函数,因为参数不能直接搬,编译时要特殊处理.函数使用的参数指针不是基于栈帧的.对于在栈中返回对象的函数,对象必须返回到原栈上。

扩展栈时,函数的返回地址会被修改成一个函数,这个函数会释放分配的栈块,将栈指针重新设置成调用者旧栈块的地址,栈指针等,需要在新栈空间中的某处保存着。



不过并不是所有函数在链接时都会插入这种指令。如果你读源代码,可能会发现"#pragma textflag 7",或者在汇编函数中看到"TEXT reuntime.exit(SB),7,$0",这种函数就是不会检测栈溢出的。这个是编译标记,汇编或者编译器中的标记7让链接器不要插入栈溢出检测的指令。

注意到它调用了runtime.morestack00。在asm_amd64.s中还有runtime.morestack10,runtime.morestack01,runtime.morestack11等好几个类似runtime.morestack00的函数,其实它们都是调用到runtime.morestack的。在stack.h中有说明为什么这么做,对不同大小的函数采用了不同的策略。每个goroutine的g->stackguard设置成指向栈底上面StackGuard的位置。每个函数会通过比较栈指针和g->stackguard检测栈溢出。对于那些只有很小栈帧的函数,为了减小检测的指令数目,可以允许栈越过stack guard下方StackSmall字节以内。使用较大栈的函数则总是会检测栈溢出并调用morestack。

Expand Down Expand Up @@ -229,6 +217,14 @@ runtime.lessstack比较简单,它其实就是切换到m->g0栈之后调用runt

再重复一遍整个过程:

栈帧的全局缓存,用于阻止每个线程栈的不受限增长

newstack 分配一个足够大的栈(大于m->moreframesize),将m->moreframesize字节拷贝到新的栈中,然后伪装runtime.lessstack刚刚掉用过m->morepc



## 小结

1. 使用分段栈的函数头几个指令检测%esp和stackguard,调用于runtime.morestack
2. runtime.more函数的主要功能是保存当前的栈的一些信息,然后转换成调度器的栈了调用runtime.newstack
3. runtime.newstack函数的主要功能是分配空间,装饰此空间,将旧的frame和arg弄到新空间
Expand Down
4 changes: 3 additions & 1 deletion ebook/03.6.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# 3.6 方法调用
# 3.6 闭包的实现



## links
* [目录](<preface.md>)
Expand Down
Loading

0 comments on commit e608f98

Please sign in to comment.