├── .gitignore ├── LANGS.md ├── README.md ├── TODO ├── all.org ├── book.json ├── graphviz ├── channel.dot ├── funcall.dot └── goroutine_state.dot ├── image ├── gc_bitmap.jpg ├── m_g.jpg ├── mcentral.jpg └── worker.jpg └── zh ├── 01.0.md ├── 01.1.md ├── 01.2.md ├── 01.3.md ├── 02.0.md ├── 02.1.md ├── 02.2.md ├── 02.3.md ├── 02.4.md ├── 03.0.md ├── 03.1.md ├── 03.2.md ├── 03.3.md ├── 03.4.md ├── 03.5.md ├── 03.6.md ├── 04.0.md ├── 04.1.md ├── 04.2.md ├── 05.0.md ├── 05.1.md ├── 05.2.md ├── 05.3.md ├── 05.4.md ├── 05.5.md ├── 06.0.md ├── 06.1.md ├── 06.2.md ├── 06.3.md ├── 07.0.md ├── 07.1.md ├── 07.2.md ├── 07.3.md ├── 08.0.md ├── 08.1.md ├── 08.2.md ├── 09.0.md ├── 09.1.md ├── 09.2.md ├── 09.3.md ├── 09.4.md ├── 10.0.md ├── 10.1.md ├── 10.4.md ├── 10.7.md ├── README.md ├── SUMMARY.md ├── images ├── 1.1.mac.png ├── 2.2.map.png ├── 3.2.funcall.jpg ├── 5.2.goroutine_state.jpg ├── 5.3.m_g.jpg ├── 5.3.m_g_p.jpg ├── 5.3.scheduler.jpg ├── 5.3.steal.jpg ├── 5.3.syscall.jpg ├── 5.3.worker.jpg ├── 6.1.mcentral.jpg ├── 6.1.mheap.jpg ├── 6.2.gc_bitmap.jpg ├── 7.1.channel.png └── epoll.jpg ├── preface.md ├── ref.md ├── ref2.md └── ref3.md /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | _book 3 | *.html 4 | -------------------------------------------------------------------------------- /LANGS.md: -------------------------------------------------------------------------------- 1 | * [中文](zh/) 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 《深入解析Go》 2 | 因为自己对Go底层的东西比较感兴趣,所以抽空在写一本开源的书籍《深入解析Go》。写这本书不表示我能力很强,而是我愿意分享,和大家一起分享对Go语言的内部实现的一些研究。 3 | 4 | 我一直认为知识是用来分享的,让更多的人分享自己拥有的一切知识这个才是人生最大的快乐。 5 | 6 | 这本书目前我放在Github上,时间有限、能力有限,所以希望更多的朋友参与到这个开源项目中来。 7 | 8 | ## 参与到本项目 9 | 10 | 如果对某些章节很有兴趣,可以写作相应章节的内容并pull request给我。如果觉得有哪些相关的内容缺失,欢迎提出。如果发现书中内容有错误或者疏漏,欢迎指正。 11 | 12 | 不管任何形式的参与都是非常受欢迎的。 13 | 14 | ## 致谢 15 | 先留空 16 | 17 | ## 授权许可 18 | 除特别声明外,本书中的内容使用[CC BY-SA 4.0 License](http://creativecommons.org/licenses/by-sa/4.0/)(创作共用 署名-相同方式共享4.0许可协议)授权,代码遵循[BSD 3-Clause License]()(3项条款的BSD许可协议)。 19 | 20 | ## 开始阅读 21 | [开始阅读](http://tiancaiamao.gitbooks.io/go-internals/content/zh/index.html) 22 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | 04.1节 栈那边要写一下初始栈大小 2 | Signal方面的东西 3 | Defer补完 4 | 5 | 几种锁spinning futex mutex cas 6 | 7 | 闭包 8 | 9 | 竞态检测,那边顺便写下Go内存模型和导致的死锁问题 10 | 11 | pprof的实现 12 | 13 | 后续写作思路: 14 | 截止目前,本书已包括了Go的内部实现的很大一部分内容。但是还是前面说过的,时间精力有限。尽管笔者在努力研究,但是Go还是在不断地变化中。 15 | 16 | Go的1.2新版本马上就快出来了。本书原本的写作思路是基于1.1的源代码,给代码加上注释,配合本书的内容通过分析源代码来了解内部实现,更倾向一种源码赏析的风格。但随着写作进行渐渐地发现这样写不太好。 17 | 18 | 如果按照这种思路,就必须定一个版本,那么本书也被限定了,没法给读者提供最新的Go的内部实现方面的信息,内容也比较容易过时。 19 | 20 | 况且了解Go的内部实现最直接的方式应该是去读Go的源代码,写书的作用不应该是让读者迷失在细枝末节中。 21 | 22 | 关于给代码加上注释,发现工作量太大,力不从心。而且Go源代码的英文注释本身还算比较完善,有一定英文基础的读起来还是比较轻松的。 23 | 24 | 25 | 所以后续写作会换一下思路。主要是以下几点: 26 | 1. 不再限定源代码,也不再给1.1的代码加注释。等1.2版本出来后会将代码部分从库中去掉。 27 | 2. 从更高的高度来描述,不再以代码分析为主。只在必要的情况下才使用少量源代码。 28 | 3. 随着Go的更新而更新本书内容。尽管Go的开发一直很活跃,但相信随着核心部分代码慢慢稳定下来,本书能跟得上脚步。 29 | 30 | 只要我还在关注Go这门语言,这本书就会一直维持更新。希望我能做得更好! 31 | 32 | 2.1节翻译过来 33 | -------------------------------------------------------------------------------- /all.org: -------------------------------------------------------------------------------- 1 | 中文的go语言内部细节的资料几乎没有,所以自己研究了一下 2 | 3 | 声明:本文内容主要来自本人对源代码的研究,以及网上找到的一些资料的整理,不保证完全正确性 4 | 5 | * 函数调用协议 6 | plan9编译器是"caller save",调用者保存寄存器。被调者可以使用任何寄存器而不必保存它们。 7 | 8 | go中没有用%ebp,因为是非连续栈,stack\_base和stack_guard来标识一个栈,保存在struct G中。 9 | ** go关键字 10 | 假设调用 go func(1,2,3) ,func函数会在一个新的go线程中运行,显然新的goroutine不能和当前go线程用同一个栈,否则会相互覆盖。 11 | 12 | 所以对go关键字的调用协议与普通函数调用是不同的。不像常规的C语言调用是push参数后直接call func,上面代码汇编之后会是: 13 | 14 | 参数进栈 15 | #+begin_quote 16 | push func\\ 17 | push 12\\ 18 | call runtime.newproc\\ 19 | pop\\ 20 | pop\\ 21 | #+end_quote 22 | 23 | 12是参数占用的大小。在runtime.newproc中,会新建一个栈空间,将栈参数的12个字节拷贝到新栈空间并让栈指针指向参数。 24 | 25 | 这时的线程状态有点像当被调度器剥夺CPU后一样,pc,sp会被存到类型于类似于进程控制块的一个结构体struct G内。func被存放在了struct G的entry域,后面进行调度时调度器会让goroutine从func开始执行。 26 | 27 | 这个仅仅是一个语法糖衣而已,也就是: 28 | #+begin_src C 29 | go f(arg) 30 | #+end_src 31 | 可以看作 32 | #+begin_src C 33 | runtime.newproc(size, f, argp) 34 | #+end_src 35 | 36 | ** defer关键字 37 | defer关键字调用过程类似于go,不同的是call的是runtime.deferproc 38 | 39 | 函数返回时,如果其中包含了defer语句,不是调用add xx SP, return 40 | 41 | 而是call runtime.deferreturn,add 48 sp,return 42 | 43 | ** 多值返回 44 | 表面上看就是一个语法糖衣:\\ 45 | go语言的 46 | #+begin_src C 47 | func f(arg1,arg2 int) (ret1,ret2 int) 48 | #+end_src 49 | 转换为C语言的 50 | #+begin_src C 51 | void f(int arg1, int arg2, int *ret1, int *ret2); 52 | int ret1,ret2; 53 | f(xx,xx, &ret1, &ret2) 54 | #+end_src 55 | 其实简单的这么理解也没什么影响.不过我深究了一下,很类似,但实际上不是这么干的. 56 | 57 | 并不是调用者传递两个返回值的地址,被调者通过修改这两个地址所指向的值达到返回值的目的. 58 | 59 | go的实际做法是在传入的参数之上留了两个空位,被调者直接将返回值放在这两空位,相当于小小的优化了一下. 60 | 61 | 以上面的f为例,go中函数调用前其内存布局是这样的:\\ 62 | #+begin_quote 63 | 留空位供被调函数设置ret1\\ 64 | 留空位供被调函数设置ret2\\ 65 | arg1\\ 66 | arg2\\ 67 | 保存函数返回地址\\ 68 | 被调者的栈...\\ 69 | #+end_quote 70 | 不过要注意的就是参数是按机器字长对齐过的. 71 | 72 | 理论上,如果c的代码中没调用c库函数,那么从go调用c应该是完全不必要cgo的。 73 | 在.go文件中声音,在.c文件中实现.必须包含runtime.h头文件. 74 | go中声明为 75 | func f(arg1,arg2,arg3 int32) (ret1, ret2 int32) 76 | 在c中的实现是: 77 | #+begin_src c 78 | void f(arg1,arg2,arg3, ret1, ret2) { 79 | FLUSH(&ret1) 80 | FLUSH(&ret2) 81 | } 82 | #+end_src 83 | * 分段栈 84 | go语言中使用的是栈不是连续的。原因是需要支持goroutine。分段栈的重要意义就在于,栈空间初始分配很小的大小,然后可以随便需要自动地增长栈空间.这样在多线程环境中就可以开千千万万个线程或协程而不至于耗尽内存. 85 | 86 | http://gcc.gnu.org/wiki/SplitStacks 87 | 88 | ** 基本实现 89 | %gs寄存器存一个tcb结构的地址,go语言中是G这个结构体.这个结构中存了栈基址和stack\_guard 90 | 91 | 对于使用分段栈的函数,每次进入函数后,前几条指令就是先检测%esp跟stack\_guard比较,如果超了就要扩展栈空间 92 | ** 扩展栈 93 | 所有用于分配栈空间的函数本身不能使用分段栈.引入一个属性来控制函数的生成.当然,还要保证有足够的空间来调用分配函数。编译器在编译阶段加入特殊的标识.note.GNU-split-stack,链接时对于检测到有这些标签的函数,就会插入特殊指令。扩展完之后,使用新栈之间,需要一些处理 94 | 95 | 基于原栈%esp的数据都可以直接复制到新栈中来.对于栈中存的是对象的地址,这样做不会造成问题。对于带参函数,因为参数不能直接搬,编译时要特殊处理.函数使用的参数指针不是基于栈帧的.对于在栈中返回对象的函数,对象必须返回到原栈上 96 | 97 | 扩展栈时,函数的返回地址会被修改成一个函数,这个函数会释放分配的栈块,将栈指针重新设置成调用者旧栈块的地址,栈指针等,需要在新栈空间中的某处保存着. 98 | ** 兼容性 99 | GCC中使用split栈的函数,编译split栈的函数,会加入.note.GNU-split-stack信息.链接时如果有这些东西,就会链接上split栈的相关runtime库.在gcc实现的split栈中要hack exit函数以便最后退出时处理这些分裂栈空间. 100 | ** go语言中的具体实现 101 | go语言使用的就是分段栈,这样可以起很多个goroutine. 102 | http://blog.nella.org/?p=849 103 | 104 | 这个上面讲的gcc怎么实现splitstack的,其作者正是gccgo的作者.在go语言的实现中其实思想和方法跟上面都是一致的. 105 | 106 | 进入函数后的前几条指令就是取%gs到%ecb 这时获得了结构体G的地址.这个结构体前两个域就是stackguard和stackbase 107 | 108 | 我还观察到,好象go编译的程序,没有使用%ebp,可能是因为G中已经存的栈基址的缘故.检测stackguard和%esp,如果空间不够了就会调用到runtime.morestack.这是一个汇编函数,在asm_386.s文件中可以找到 109 | 110 | #+begin_quote 111 | TEXT runtime.morestack (SB),7,$0 112 | #+end_quote 113 | 其中这个7就是告诉编译器这个函数不使用分段栈\\ 114 | runtime.morestack会把一些信息存到结构体M\\ 115 | DX中是frame size, AX中是arg size,这些会被保存到M结构体,还有函数返回地址,保存以后这些东西在后面会清空,然后新栈和旧栈信息可以link起来. 116 | 117 | 当morestack函数保存好需要的东西以后,它切换到调度器的栈,然后将控制器交给runtime.newstack 118 | 119 | 注意调用到runtime.newstack的方式是CALL,并且用的是调度器的栈,函数的退出有点特殊. 120 | 121 | 栈空间的分配使用的普通的go runtime的空间分配技术,也就是会垃圾回收.但也有些特殊,也不完全是直接从垃圾回收的池子中来,回来垃圾回收的池子中去. 122 | 123 | runtime.newstack不会返回到调用者morestack.不考虑reflect相关的东西,它做的事情就是分配一块内存,在头部放一个Stktop的结构体,特殊方式退出.\\ 124 | 清除栈时,新栈中保存的这些栈桢的信息会起作用. 125 | 126 | 退出使用的是gogocall,是调度器实现上下文切换上函数.相当于直接jump过去的而不是函数调用协议那样过去的.保存的函数返回地址被设置为一个后处理的函数,这样遇到下一次RET指令时,会jump到more.lessstack函数,这个函数做的正好是跟morestack函数相反的工作.然后就到新栈中去工作了. 127 | 128 | 再重复一遍整个过程: 129 | 1. 使用分段栈的函数头几个指令检测%esp和stackguard,调用于runtime.morestack 130 | 2. runtime.more函数的主要功能是保存当前的栈的一些信息.然后转换成调试器的栈了调用runtime.newstack 131 | 3. runtime.newstack函数的主要功能是分配空间,装饰此空间,将旧的frame和arg弄到新空间 132 | 4. 使用gogocall的方式切换到新分配的栈,gogocall使用的JMP返回到被中断的函数 133 | 5. 继续执行遇到RET指令时会返回到runtime.less,less做的事情跟more相反,它要准备好从newstack到old stack 134 | 整个过程有点像一次中断,中断处理时保存当时的现场,弄个新的栈,中断恢复时恢复到新栈中运行,运行到return时又要从runtime.less走回去 135 | 136 | * 编译过程分析 137 | 138 | $GOROOT/src/cmd/gc目录,这里gc不是垃圾回收的意思,而是go compiler 139 | 140 | 6g/8g的源文件的主函数是在lex.c 141 | 142 | 从这个文件可以看到整个编译的流程。先是利用bison做了词法分析yyparse() 143 | 144 | 后面就是语法分析,注释中有第一步第二步...最后生成目标文件.8或.6,相当于c的.o 145 | 146 | go.y是bison的语法定义文件 147 | 148 | 事实上go在编译阶段也只是将所有的内容按语法分析的结果放入NodeList这个数据结构里,然后export写成一个*.8(比如i386的架构),这个.8的文件大概是这样子的: 149 | 150 | go object linux 386 go1 X:none 151 | exports automatically generated from 152 | hello.go in package "even" 153 | 154 | $$ // exports 155 | package even 156 | import runtime "runtime" 157 | type @"".T struct { @"".id int } 158 | func (@"".this *@"".T "noescape") Id() (? int) { return @"".this.@"".id } 159 | func @"".Even(@"".i int) (? bool) { return @"".i % 2 == 0 } 160 | func @"".odd(@"".i int) (? bool) { return @"".i % 2 == 1 } 161 | 162 | $$ // local types 163 | 164 | $$ 165 | 166 | .... 167 | 168 | 可以自己做实验写个hello.go,运行go tool 8g hello.go 169 | 170 | 具体的文件格式,可以参考src/cmd/gc/obj.c里的dumpobj函数的实现 171 | 172 | 而如果我们在源文件里写一个import时,它实际上会将这个obj文件导入到当前的词法分析过程中来,比如 173 | 174 | import xxx 175 | 176 | 它就是会把pkg/amd64-linux/xxx.a加载进来,接着解析这个obj文件 177 | 178 | 如果我们看go.y的语法分析定义,就会看到许多hidden和there命名的定义,比如import_there, hidden_import等等,这些其实就是从obj文件来的定义。 179 | 180 | 又比如我们可能会看到一些根本就不存在于源代码中的语法定义,但是它确实编译过了,这是因为在编译过程中源文件被根据需要插入一些其他的碎片进来,比如builtin的一些库或者自定义的一些lib库。 181 | 182 | 理解了这些,基本上就对go的编译过程有了一个了解,事实上go的编译过程做的事情也就是把它变成obj完事,至少我们目前没有看到更多的工作。接下来想要更深入的理解,就要再看xl的实现了,这部分是将obj变成可执行代码的过程,应该会比较有趣了。 183 | 184 | --------------------------------------------------------------------------------------------- 185 | 186 | * 系统的初始化 187 | 188 | proc.c中有一段注释 189 | 190 | // The bootstrap sequence is: 191 | // 192 | // call osinit 193 | // call schedinit 194 | // make & queue new G 195 | // call runtime·mstart 196 | // 197 | // The new G calls runtime·main. 198 | 199 | 这个可以在$GOROOT/src/pkg/runtime/asm_386.S中看到。go编译生成的程序应该是从这个文件开始执行的。 200 | 201 | // saved argc, argv 202 | ... 203 | CALL runtime·args(SB) 204 | CALL runtime·osinit(SB) //这个设置cpu核心数量 205 | CALL runtime·schedinit(SB) 206 | 207 | // create a new goroutine to start program 208 | PUSHL $runtime·main(SB) // entry 209 | PUSHL $0 // arg size 210 | CALL runtime·newproc(SB) 211 | POPL AX 212 | POPL AX 213 | 214 | // start this M 215 | CALL runtime·mstart(SB) 216 | 217 | 还记得前面讲的go线程的调用协议么?先push参数,再push被调函数和参数字节数,接着调用runtime.newproc 218 | 219 | 所以这里其实就是新开个线程执行runtime.main 220 | 221 | runtime.newproc会把runtime.main放到就绪线程队列里面。 222 | 223 | 本线程继续执行runtime.mstart,m意思是machine。runtime.mstart会调用到schedule 224 | 225 | schedule函数绝不返回,它会根据当前线程队列中线程状态挑选一个来运行。 226 | 227 | 然后就调度到了runtime.main函数中来,runtime.main会调用用户的main函数,即main.main从此进入用户代码 228 | 229 | 总结一下函数调用流程就是 230 | 231 | runtime.osinit --> runtime.schedinit --> runtime.newproc --> runtime.mstart --> schedule --> 232 | 233 | runtime.main --> main.main 234 | 235 | 这个可以写个helloworld了用gdb调试,一步一步的跟 236 | 237 | ----------------------------------------------------------------------------------------------- 238 | 239 | * 调度器 240 | ** 总体介绍 241 | $GOROOT/src/pkg/runtime目录很重要,值得好好研究,源代码可以从runtime.h开始读起。 242 | 243 | goroutine实现的是自己的一套线程系统,语言级的支持,与pthread或系统级的线程无关。 244 | 245 | 一些重要的结构体定义在runtime.h中。两个重要的结构体是G和M 246 | 247 | 结构体G名字应该是goroutine的缩写,相当于操作系统中的进程控制块,在这里就是线程的控制结构,是对线程的抽象。 248 | 249 | 其中包括 250 | #+begin_quote 251 | goid //线程ID\\ 252 | status//线程状态,如Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead等\\ 253 | #+end_quote 254 | 255 | 有个常驻的寄存器extern register G* g被使用,这个是当前线程的线程控制块指针。amd64中这个寄存器是使用R15,在x86中使用0(GS) 分段寄存器 256 | 257 | 结构体M名字应该是machine的缩写。是对机器的抽象,m其实是对应到操作系统线程。 258 | 259 | proc.c中是实现的线程调度相关。 260 | 261 | 调度器调度的时机是某线程进入系统调用,或申请内存,或由于等待管道而堵塞等 262 | ------------------------------------------------------------------------------------------ 263 | ** goroutine的生老病死 264 | 前面函数调用协议里面有说过go关键字最终被弄成了runtime.newproc.就以这个为出发点看整个调度器吧.runtime目录下的proc.c文件.这里有一份我加了注释的文件,放在https://github.com/tiancaiamao/go-internals 265 | 266 | runtime.newproc功能是创建一个新的g.这个函数不能用分段栈,真正的工作是调用newproc1完成的.newproc1的动作包括: 267 | 268 | #+begin_quote 269 | 分配一个g的结构体\ 270 | 初始化这个结构体的一些域\ 271 | 将g挂在就绪队列\ 272 | 引发一次调度matchmg 273 | #+end_quote 274 | 初始化newg的域时,会将调用参数保存到g的栈,将sp,pc等上下文环境保存在g的sched域,这样当这个g被分配了一个m时就可以运行了. 275 | 276 | 接下来看matchmg函数.这个函数就是做个匹配,只要m没有突破上限GOMAXPROCS,就拿一个m绑定一个g.如果m的waiting队列中有就从队列中拿,否则就要新建一个m,调用runtime.newm 277 | 278 | runtime.newm功能跟newproc相似,前者分配一个goroutine,而后者是分配一个machine.调用的runtime.newosproc函数.其实一个machine就是一个操作系统线程的抽象,可以看到它会调用runtime.newosproc.这个新线程会以mstart作为入口地址.当m和g绑定后,mstart会恢复g的sched域中保存的上下文环境,然后继续运行. 279 | 280 | 随便扫一下runtime.newosproc还是蛮有意思的,代码在thread_linux.c文件中(平台相关的),它调用了runtime.clone(平台相关). runtime.clone是用汇编实现的,代码在sys_linux_386.s.可以看到上面有\\ 281 | INT $0x80\\ 282 | 看到这个就放心了,只要有一点汇编基础知道,你懂的.可以看出,go的runtime果然跟c的runtime半毛钱关系都没有啊 283 | 284 | 回到runtime.newm函数继续看,它调用runtime.newosproc建立了新的线程,线程是以runtime.mstart为入口的,那么接下来看mstart函数. 285 | 286 | mstart是runtime.newosproc新建的线程的入口地址,新线程执行时会从这里开始运行.新线程的执行和goroutine的执行是两个概念,由于有m这一层对机器的抽象,是m在执行g而不是线程在执行g.所以线程的入口是mstart,g的执行要到schedule才算入口.函数mstart的最后调用了schedule. 287 | 288 | 终于到了schedule了! 289 | 290 | 如果从mstart进入到schedule的,那么schedule中逻辑非常简单,前面省了一大段代码.大概就这几步: 291 | 292 | #+begin_quote 293 | 找到一个等待运行的g\\ 294 | 将它搬到m->curg,设置好状态为Grunning\\ 295 | 直接切换到g的上下文环境,恢复g的执行 296 | #+end_quote 297 | 298 | 从newproc一直出生一直到运行的过程分析,到此结束! 299 | 300 | 虽然按这样a调用b,b调用c,c调用d,d调用e的方式去分析源代码谁看都会晕掉,但我还是想重复一遍这里的读代码过程后再往下写些有意思的,希望真正感兴趣的读者可以拿着注释过的源码按顺序走一遍: 301 | 302 | newproc -> newproc1 -> newprocreadylocked -> matchmg -> (可能引发)newm -> newosproc -> (线程入口)mstart -> schedule -> gogo跳到goroutine运行 303 | 304 | 以上状态变化经历了Gwaiting->Grunnable->Grunning,经历了创建,到挂在就绪队列,到从就绪队列拿出并运行.下面将从其它几种状态变化继续看调度器,从runtime.entersyscall开始. 305 | 306 | runtime.entersyscall做的事情大致是设置g的状态为Gsyscall,减少mcpu.如果mcpu减少之后小于mcpumax了并且有处于就绪态的g,则matchmg 307 | 308 | runtime.exitsyscall函数中,如果退出系统调用后mcpu小于mcpumax,直接设置g的状态Grunning.表示让它继续运行.否则如果mcpu达到上限了,则设置readyonstop,表示下一次schedule中将它改成Grunnable了放到就绪队列中 309 | 310 | 现在Gwaiting,Grunnable,Grunning,Gwaiting都出现过的,接下来看最后两种状态Gmoribund和Gdead.看runtime.goexit函数.这个函数直接把g的状态设置成Gmoribund,然后调用gosched,进入到schedule中.在schedule中如果遇到状态为Gmoribund的g,直接设置g的状态为Gdead,将g与m分离,把g放回到free队列. 311 | ** 简单理解 312 | 接下来看一些有意思点的吧,先不读代码了.一个常规的 线程池+任务队列 的模型如图所示: 313 | [[file:image/worker.jpg]] 314 | 把每个工作线程叫worker的话,每条线程运行一个worker,每个worker做的事情就是不停地从队列中取出任务并执行: 315 | #+begin_src c 316 | while(!empty(queue)) { 317 | q = get(queue); //从任务队列中取一个(涉及加锁等) 318 | q->callback(); //执行该任务 319 | } 320 | #+end_src 321 | 这当然是最简单的情形,但是一个很明显的问题就是一个进入到callback之后,就失去了控制权.因为没有一个调度器层的东西,一个任务可以执行很长很长时间一直占用的worker线程,或者阻塞于io之类的. 322 | 323 | 这时协程一类的东西就会提供类似yield的函数.callback函数中运行到一定时候就主动调用yield放弃自己的执行,把自己再次放回到任务队列中等待下一次调用时机等等. 324 | 325 | 将一个正在执行的任务yield出去,再在某个时刻再弄回来继续运行,这就涉及到一个问题,即执行线程的上下文环境.其实go语言中的goroutine就是这里任务的抽象.每个struct G中都会有一个sched域就是用于保存自己上下文的.这样这种"任务"就可以被换出去,再换进来.go语言另一个重要东西就是分段栈,栈初始大小很小(4k),可以自动增长,这样就可以开千千万万的goroutine了. 326 | 327 | 现在我们的任务变成了这个样子的: 328 | #+begin_src c 329 | struct G { 330 | Gobuf sched; 331 | byte *stack; 332 | } 333 | #+end_src 334 | 335 | 一个线程是一个worker,假如运行到阻塞了呢?那干事的家伙岂不就少了,解耦还是不够.所以不是一个worker对应一条线程的,go语言中又引入了struct M这层抽象.m就是这里的worker,但不是线程.处理系统调用中的m不会占用线程,只有干事的m才会对应线程. 336 | 337 | 于是就变成了这样子: 338 | [[file:image/m_g.jpg]] 339 | 然后就变成了线程的入口是mstart,而goroutine的入口是在schedule中m和g都满足之后切换上下文进入的. 340 | 只是由于要优化,所以会搞的更复杂一些.比如要重用内存空间所以会有gfree和mhead之类的东西. 341 | ** 还有几个没讲清楚的地方 342 | 一个没讲清楚的地方就是m->g0这是个什么东西 343 | 还有一点疑问就是:一个m对应一个系统线程,当g进入到syscall时会和m一起绑定.如果g不停地进入syscall并且暂时不返回,岂不是会开很多的系统级线程?? 344 | m寄存器的切换 345 | 346 | * 内存管理 347 | go的内存分配器是基于tcmalloc的.为每个系统线程M分配一个本地的MCache,少量的地址分配就直接从Cache中分配,并且定期做垃圾回收,将线程本地Cache中的空闲内存返回给全局控制堆. 348 | 349 | 小于32K为小对象,大对象直接从全局控制堆上以页(4k)为单位进行分配,也就是说大对象总是以页对齐的. 350 | 351 | 一个页可以存入一些相同大小的小对象,小对象从本地内存链表中分配,大对象从中心内存堆中分配 352 | 353 | 大约有100种内存块类别,每一类别都有自己对象的free list.小于32kB的内存分配被向上取整到对应的尺寸类别,从相应的free list中分配.一页内存可以被分裂成一种尺寸类别的对象,然后由free list分配器管理. 354 | 355 | 分配器的数据结构包括: 356 | + FixAlloc: 固定大小(128kB)的对象的空闲链分配器,被分配器用于管理存储 357 | + MHeap: 分配堆,按页的粒度进行管理(4kB) 358 | + MSpan: 一些由MHeap管理的页 359 | + MCentral: 对于给定尺寸类别的共享的free list 360 | + MCache: 用于小对象的每M一个的cache 361 | + MStats: 关于分配的统计信息 362 | 363 | 分配一个小对象(<32kB)进行的缓存层次结构: 364 | 1. 将小对象大小向上取整到一个对应的尺寸类别,查找相应的MCache的空闲链表,如果链表不空,直接从上面分配一个对象.这个过程可以不必加锁. 365 | 2. 如果MCache自由链是空的,通过从MCentral自由链拿一些对象进行补充.拿"一些"分摊了MCentral锁的开销 366 | 3. 如果MCentral自由链是空的,则通过从MHeap中拿一些页进行补充,然后将这些内存截断成规定的大小.分配一些的对象分摊了对堆加锁的开销 367 | 4. 如果MHeap是空的,或者没有足够大小的页了,从操作系统分配一组新的页(至少1MB).分配一大批的页分摊了从操作系统分配的开销. 368 | 369 | 释放一个小对象进行类似的层次: 370 | 1. 查找对象所属的尺寸类别,将它添加到MCache的自由链 371 | 2. 如果MCache自由链太长或者MCache内存大多了,则返还一些到MCentral自由链 372 | 3. 如果在某个范围的所有的对象都归还到MCentral链了,则将它们归还到页堆. 373 | 4. 如果堆的内存太多,则归还一些到操作系统(TODO:这步还没有实现) 374 | 375 | 分配和释放大的对象则直接使用页堆,跳过MCache和MCentral自由链 376 | 377 | MCache和MCentral中自由链的小对象可能是也可能不是清0了的.当且仅当该对象的第2个字节是清0时,它是清0了的.页堆中的总是清零的.当一定范围的对象归还到页堆时,需要先清零. 378 | 379 | 写到这里突然看到一篇文章,我觉得他写得比我好,所以我就不继续写下去了: 380 | http://shiningray.cn/tcmalloc-thread-caching-malloc.html 381 | 382 | ---------------- 383 | 涉及的文件包括: 384 | malloc.h 头文件 385 | malloc.goc 最外层的包装 386 | msize.c 将各种大小向上取整到相应的尺寸类别 387 | mheap.c 对应MHeap中相关实现,还有MSpan 388 | mcache.c 对应MCache中相关实现 389 | mcentral.c 对应MCentral中相关实现 390 | mem_linux.c SysAlloc等sys相关的实现 391 | 392 | ** MHeap层次 393 | MHeap层次用于直接分配较大(>32kB)的内存空间,以及给MCentral和MCache等下层提供空间。它管理的基本单位是MSpan。MSpan是一个表示若干连续内存页的数据结构,简化后如下: 394 | #+begin_src C 395 | struct MSpan 396 | { 397 | PageID start; // starting page number 398 | uintptr npages; // number of pages in span 399 | }; 400 | #+end_src 401 | 通过一个基地址+(页号*页大小),就可以定位到实际的地址空间了。 402 | 403 | MHeap负责将MSpan组织和管理起来,MHeap数据结构中的重要部分如图所示。 404 | [[../image/mheap.jpg]] 405 | free是一个分配池,从free[i]出去的MSpan每个大小都i页的,总共256个槽位。再大了之后,大小就不固定了,由large链起来。 406 | 分配过程: 407 | 如果能从free[]的分配池中分配,则从其中分配。如果发生切割则将剩余部分放回free[]中。比如要分配2页大小的空间,从图上2号槽位开始寻找,直到4号槽位有可用的MSpan,则拿一个出来,切出两页,剩余的部分再放回2号槽位中。 408 | 否则从large链表中去分配,按BestFit算法去找一块空间 409 | 410 | 化整为零简单,化零为整麻烦。回收的时候如果相邻的块是未使用的,要进行合并,否则一直划分下去就会产生很多碎片,找不到一个足够大小的连续空间。因为涉及到合并,回收会比分配复杂一些,所有就有什么伙伴算法,边界标识算法,位示图之类的。 411 | go在这里使用的大概类似于位示图。可以看到MHeap中有一个 412 | #+begin_src c 413 | MSpan *map[1<nonempty->freelist分配。如果发现freelist空了,则说明这一块MSpan满了,将它移到MCentral->empty。 432 | 前面我说过,回收比分配复杂,因为涉及到合并。这里用引用计数弄的。MSpan中每划出一个对象,则引用计数加一,每回收一个对象,则引用计数减一。如果减之后引用计数为零了,则说明这整块的MSpan已经没被使用了,可以将它归还给MHeap。 433 | 434 | 忘记说了,前面MHeap结构体中也有用于管理MCentral的相关域。每种尺寸类别都会有一个central的,所以是NumSizeClasses的数组。MCentral中再通过MSpan划分成小对象的,就是从MSpan->freelist链起来。 435 | #+begin_src c 436 | union { 437 | MCentral; 438 | byte pad[CacheLineSize]; 439 | } central[NumSizeClasses]; 440 | #+end_src 441 | * 垃圾回收 442 | 这里假设读者对mark-sweep的垃圾回收算法有基本的了解,否则没办法读懂这部分的代码。 443 | ** 位图标记和内存布局 444 | 目前go中的垃圾回收用的是标记清扫法.保守的垃圾回收,进行回收时会stoptheworld. 445 | 446 | 每个机器字节(32位或64位)会对应4位的标记位.因此相当于64位系统中每个标记位图的字节对应16个堆字节. 447 | 448 | 字节中的位先根据类型,再根据堆中的分配位置进行打包,因此每个64位的标记位图从上到下依次包括:\\ 449 | #+begin_quote 450 | 16位特殊位,对应堆字节\\ 451 | 16位垃圾回收的标记位\\ 452 | 16字节的 无指针/块边界 的标记位 453 | 16位的 已分配 标记位\\ 454 | #+end_quote 455 | 这样设计使得对一个类型的相应的位进行遍历很容易. 456 | 457 | 地址与它们的标记位图是分开存储和.以mheap.arena_start地址为边界,向上是实际使用的地址空间,向下是标记位图.比如在64位系统中,计算某个地址的标记位的公式如下: 458 | #+begin_quote 459 | 偏移 = 地址 - mheap.arena_start\\ 460 | 标记位地址 = mheap.arena_start - 偏移/16 - 1 (32位中是偏移/8,就是每标记字节对应多少机器字节)\\ 461 | 移位 = 偏移 % 16 462 | 标记位 = *标记位地址 >> 移位 463 | #+end_quote 464 | 然后就可以通过 (标记位 & 垃圾回收标记位),(标记位 & 分配位),等来测试相应的位. 465 | 其中已分配的标记为1<<0,无指针/块边界是1<<16,垃圾回收的标记位为1<<32,特殊位1<<48 466 | 467 | 内存布局如下图所示: 468 | ../image/gc_bitmap.jpg 469 | 470 | ** 基本的mark过程 471 | go的垃圾回收还不是很完善.相应的代码在mgc0.c,可以看到这部分的代码质量相对其它部分是明显做得比较糙的.比如反复出现的模块都没写个函数: 472 | #+begin_src c 473 | off = (uintptr*)obj - (uintptr*)runtime·mheap->arena_start; 474 | bitp = (uintptr*)runtime·mheap->arena_start - off/wordsPerBitmapWord - 1; 475 | shift = off % wordsPerBitmapWord; 476 | xbits = *bitp; 477 | bits = xbits >> shift; 478 | #+end_src 479 | 再比如说markallocated和markspan,markfreed做的事情都差不多一样的,却写了三个函数. 480 | 由于代码写得不行,所以读得出吃力一些.先抛开这些不谈,还是从最简单的开始看,mark过程,从debug_scanblock开始读,这个跟普通的标记-清扫的垃圾回收算法结构是一样的. 481 | 482 | debug_scanblock函数是递归实现的,单线程的,更简单更慢的scanblock版本.该函数接收的参数分别是一个指针表示要扫描的地址,以及字节数. 483 | 484 | 首先要将传入的地址,按机器字节大小对应.\\ 485 | 然后对待扫描区域的每个地址:\\ 486 | 找到它所在的MSpan,再找到该地址在MSpan中所处的对象地址(内存管理中分析过,go中的内存池中的小对象).\\ 487 | 既然有了对象的地址,则根据它找到对应位图里的标记位.前一小节已经写了从地址到标记位图的转换过程.\\ 488 | 判断标记位,如果是未分配则跳过.否则打上特殊位标记(debug_scanblock中用特殊位代码的mark位)完成标记.\\ 489 | 还要判断标记位中是否含有无指针的标记位,如果没有,则还要递归地调用debug_scanblock. 490 | 491 | 如果对mark-sweep算法有点基础,读debug_scanblock应该不难理解。 492 | ** 并行的垃圾回收操作 493 | 整个的gc是以runtime.gc函数为入口的,它实际调用的是gc.进入gc后会先stoptheworld.接着添加标记的root. 494 | 然后会设置markroot和sweepspan的并行任务。 495 | 运行mark的任务,扫描块,运行sweep的任务,最后starttheworld并切换出去。 496 | 497 | 总体来讲现在版本的go中的垃圾回收是设计成多线程合作完成的,有个parfor.c文件中有相应代码。以前版本是单线程做的。在gc函数中调用了 498 | #+begin_src c 499 | runtime·parforsetup(work.markfor, work.nproc, work.nroot, nil, false, markroot); 500 | runtime·parforsetup(work.sweepfor, work.nproc, runtime·mheap->nspan, nil, true, sweepspan); 501 | #+end_src 502 | 是设置好回调让线程去执行markroot和sweepspan函数。 503 | 504 | 实现方式就是设置一个工作缓存,原来debug_scanblock中是遇到一个新的指针就递归地调用处理,而现在是遇到一个新的指针就进队列加到工作缓存中。 505 | 功能上差不多,一个是非递归一个是递归。scanblock从工作区开始扫描,扫描到的加个mark标记,如果遇到可能的指针,不是递归处理而是加到工作队列中。这样可以多个线程同时进行。 506 | 并行设计中,有设置工作区的概念,多个worker同时去工作缓存中取数据出来处理,如果自己的任务做完了,就会从其它的任务中“偷”一些过来执行。 507 | 508 | ** 精确的垃圾回收以及虚拟机 509 | scanblock函数非常难读,我觉得应该好好重构一下。上面有两个大的循环,第一个作用是对整个扫描块区域,将类型信息提取出来。另一个大循环是实现一个虚拟机操作码的解析执行。 510 | 511 | 为什么会弄个虚拟机呢?目前我也不明白为啥这么搞。反正垃圾回收的操作都被弄成了操作码,用虚拟机去解释执行的。不同类型的对象,由于垃圾回收的方式不一样,把各种类型的回收操作独立出来做成操作码,可能是灵活度更大吧。 512 | 513 | go是这样弄的啊: 514 | 从一个地址可以找到相应的标记位图。\\ 515 | 过程是通过地址到MSpan,然后MSpan->type.compression得到一个type的描述\\ 516 | 再由type描述得到类型信息\\ 517 | 类型信息是一个Type结构体(在type.h头文件中定义),其中有个void *gc域\\ 518 | gc其实就是代码段了。通过虚拟机解释其中的操作码完成各种类型的对象的垃圾回收操作。 519 | 520 | 回收ptr,slice,string...不同类型都会对应到不同的操作码。其中也有一些小技巧的东西比如type描述符。它是一个uintptr,由于内存分配是机器字节对齐的,所以地址就只用到了高位。type描述符中高位存放的是Type结构体的指针,低位可以用来存放类型。通过 521 | #+begin_src c 522 | t = (Type*)(type & ~(uintptr)(PtrSize-1)); 523 | #+end_src 524 | 就可以从type的描述符得到Type结构体,而通过 525 | #+begin_src c 526 | type & (PtrSize-1) 527 | #+end_src 528 | 就可以得到类型。 529 | 530 | gc的触发是由一个gcpercent的变量控制的,当新分配的内存占已在使用中的内存的比例超过gcprecent时就会触发.比如说gcpercent=100,当前使用了4M,当内存分配到达8M时就会再次gc. 531 | * 番外篇 532 | 读代码始终有些东西还是很难看懂,如果自己写代码,就能真正理解了,所以这一节就尝试自己写写代码。要写的是一个收集当前内存状态的函数,自己写以便理解前面内存管理的垃圾回收的一些东西。 533 | 534 | 在go中调用c代码,如果没有调用c的库函数,是可以不必要用到cgo的。c函数中可以使用go的运行时代码,比如runtime·printf。 535 | 建个test文件夹,写个test.go文件,上面进行声明: 536 | #+begin_src c 537 | package c 538 | func MemInfo() 539 | #+end_src 540 | 然后是test文件夹下,建个test.c实现MemInfo函数。函数名是void ·MemInfo(),而不是void MemInfo(),注意函数中的那个·符号。 541 | 542 | 在这个c文件中是可以访问go的runtime的全局对象的,所以runtime·mheap就是堆了。这是一个MHeap结构体,通过上面的allspans域就可以访问到所有的MSpan。根据MSpan结构中有状态信息,可以跳过不关心的MSpan。 543 | #+begin_src c 544 | h = runtime·mheap; 545 | for(i=0; i < h->nspan; i++) { 546 | s = h->allspans[i]; 547 | if(s == nil || s->state != MSpanInUse) 548 | continue; 549 | } 550 | #+end_src 551 | 到这里时会遇到一个问题,MSpanInUse未定义,没关系,把go源代码中的malloc.h拷到test文件夹就行了。后面还会用到type.h,也先拷过来。只有runtime.h是go编译c代码时会默认使用,其它的runtime中的头文件,想用的话拷过来就好了。不过想任意调用runtime中的函数还是不行的,只有runtime.h中声明的runtime·xxx是可以调用的,像malloc.h中声明的函数都调用不了。 552 | 553 | 接下来就是对每块MSpan进行分析了。对照malloc.h文件中MSpan的结构体定义,可以打印出这个结构体的一些信息: 554 | #+begin_src c 555 | MSpan *s; 556 | runtime·printf("页号:%D,页数:%D,大小类:%d,元素大小:%D\n", s->start, s->npages, s->sizeclass, s->elemsize); 557 | #+end_src 558 | 559 | MSpan的types是一个MTypes结构,继续打印出类型信息。根据MTypes中的compression的不同,data对应的是不同的东西。 560 | #+begin_src c 561 | switch(s->types.compression) { 562 | case MTypes_Empty: 563 | break; 564 | case MTypes_Single: 565 | runtime·printf("MTypes_Single\n"); 566 | break; 567 | case MTypes_Words: 568 | runtime·printf("MTypes_Words\n"); 569 | break; 570 | case MTypes_Bytes: 571 | runtime·printf("MTypes_Bytes\n"); 572 | } 573 | #+end_src 574 | 575 | 这里的类型信息是关于整块MSpan的。MTypes_Empty表明这一块的类型信息不可用。MTypes_Single表示这整个MSpan存的都是一个对象。MTypes_Bytes是这个MSpan中存放的不同对象类型在7种以内。而MTypes_Words表明这块MSpan存放了超过8种以上的不同类型的对象。 576 | 577 | MTypes_XXX是关于整块MSpan在存放的对象类型的信息。比如挑其中MTypes_Bytes的MSpan为例,可以继续再看具体对象的类型信息。data[i]是一个uintptr值,值的高位是指向Type结构体的指针,低位是类型信息。 578 | #+begin_src c 579 | ptr = data[i] 580 | Type *t = (Type*)(ptr & ~(uintptr)(PtrSize-1)); 581 | ptr & (PtrSize-1) 582 | #+end_src 583 | MTypes_Bytes中共有最多7种类型信息,可能data\[1\]到data\[7\]得到对应的Type结构体指针。然后可以继续打印Type结构体内的一些信息出来。 584 | 585 | 最后代码放在[[http://github.com/tiancaiamao/go-internals/test/][这里]] 了,想跑的可以拿去玩一玩。 586 | #+begin_src c 587 | package main 588 | 589 | import ( 590 | "github.com/tiancaiamao/go-internals/test" 591 | "github.com/syndtr/goleveldb/leveldb" 592 | "github.com/syndtr/goleveldb/leveldb/storage" 593 | "github.com/syndtr/goleveldb/leveldb/opt" 594 | ) 595 | 596 | type S struct { 597 | aa uint32 598 | bb []byte 599 | cc string 600 | } 601 | 602 | func main() { 603 | for i:=0; i <100000; i++ { 604 | workthegc() 605 | } 606 | for i:=0; i<200; i++ { 607 | func() []S { 608 | return make([]S,300) 609 | }() 610 | } 611 | 612 | stor, _ := storage.OpenFile("test.db") 613 | defer stor.Close() 614 | db, _ := leveldb.Open(stor, &opt.Options{Flag: opt.OFCreateIfMissing}) 615 | defer db.Close() 616 | 617 | ro := &opt.ReadOptions{} 618 | wo := &opt.WriteOptions{} 619 | db.Get([]byte("key"), ro) 620 | db.Put([]byte("key"), []byte("value"), wo) 621 | db.Delete([]byte("key"), wo) 622 | 623 | test.MemInfo() 624 | } 625 | 626 | func workthegc() []byte { 627 | return make([]byte, 1029) 628 | } 629 | #+end_src 630 | 额...我啥都不会,就是精通"hello world",哈哈~ 631 | * 类型系统 632 | ** chan类型的实现 633 | chan类型的实现在文件chan.c中.channel其实就是一个数据结构而已,如下图所示: 634 | ../image/chan.jpg 635 | 其实它本身是一个循环队列,qcount记录了队列总数据个数,dataqsiz记录循环队列的大小,elemalg是元素操作的一个Alg结构体,记录下元素的操作.如copy函数,equal函数,hash函数等. 636 | recvq和sendq是两个链表,分别记录下因读chan阻塞和因写chan而阻塞的goroutine.它是一个SudoG结构,该结构中主要的就是一个g和一个elem.如果g阻塞于chan了,那么它就被挂在recvq或sendq中,对应的数据是放在elem中的. 637 | 638 | 如果是带缓冲区的chan,则缓冲区数据实际上是接着Hchan结构体中分配的.会分配 639 | #+begin_src c 640 | c = (Hchan*)runtime路mal(n + hint*elem->size); 641 | #+end_src 642 | 643 | 以runtime.chansend为例来看向chan中写数据的过程. 644 | 645 | 先要区分是同步还是异步.同步是指chan是不带缓冲区的,因此可能写阻塞.而异步是指chan带缓冲区,只有缓冲区满才阻塞. 646 | 同步的情况,首先会看recvq中有没有因读该管道而阻塞的goroutine,如果有,则把数据拷贝到它的elem中,将它置为ready,然后函数返回. 647 | 否则要将当前goroutine和数据作为SudoG结构体,挂到通道的阻塞队列中. 648 | 649 | 异步的情况,如果缓冲区满了,也是要将当前goroutine和数据一起作为SudoG结构体挂在sendq队列中的. 650 | 否则也是先看有没有recvq,有就唤醒.没有就将数据放到通过的缓冲区中. 651 | 652 | runtime.chanrecv跟chansend一个样子的.只不过一个是收一个是发. 653 | 654 | select-case被中的chan编译成了if-else.比如 655 | #+begin_src c 656 | select { 657 | case v = <-c: 658 | ...foo 659 | default: 660 | ...bar 661 | } 662 | #+end_src 663 | 会被编译为: 664 | #+begin_src c 665 | if selectnbrecv(&v, c) { 666 | ...foo 667 | } else { 668 | ...bar 669 | } 670 | #+end_src 671 | 672 | 类似地 673 | #+begin_src c 674 | select { 675 | case v, ok = <-c: 676 | ... foo 677 | default: 678 | ... bar 679 | } 680 | #+end_src 681 | 会被编译为: 682 | #+begin_src c 683 | if c != nil && selectnbrecv2(&v, &ok, c) { 684 | ... foo 685 | } else { 686 | ... bar 687 | } 688 | #+end_src 689 | select-case中的case是随机的.而不像switch-case那样一条一条的顺序.如何实现随机的呢? 690 | 其实上面用到了Scase和Select数据结构.在Select数据结构中有个Scase数组,记录下了每一个Scase. 691 | 然后将数组元素随机排列,这样就可以将Scase乱序了. 692 | 693 | ** interface的实现 694 | 695 | 假设我们把类型分为具体类型和接口类型。 696 | 697 | 具体类型例如type myint int32 或type mytype struct {...} 698 | 699 | 接口类型是例如type I interface {} 700 | 701 | 接口类型的值,在内存中的存放形式是两个域,一个指向真实数据(具体类型的数据)的指针,一个itab指针。 702 | 703 | 具体见$GOROOT/src/pkg/reflect/value.go 的type nonEmptyInterface struct {...} 定义 704 | 705 | itab中包含了数据(具体类型的)的类型描述符信息和一个方法表 706 | 707 | 方法表就类似于C++中的对象的虚函数表,上面存的全是函数指针。 708 | 709 | 方法表是在接口值在初始化的时候动态生成的。具体的说: 710 | 711 | 对每个具体类型,都会生成一个类型描述结构,这个类型描述结构包含了这个类型的方法列表 712 | 713 | 对接口类型,同样也生成一个类型描述结构,这个类型描述结构包含了接口的方法列表 714 | 715 | 接口值被初始化的时候,利用具体类型的方法表来动态生成接口值的方法表。 716 | 717 | 比如说var i I = mytype的过程就是: 718 | 719 | 构造一个接口类型I的值,值的第一个域是一个指针,指向mytype数据的一个副本。注意是副本而不是mytype数据本身,因为如果不这样的话改变了mytype的值,i的值也被改变。 720 | 721 | 值的第二个域是指向一个动态构造出来的itab,itab的类型描述符域是存mytype的类型描述符,itab的方法表域是将mytype的类型描述符的方法表的对应函数指针拷贝过来。构造itab的代码在$ROOT/src/pkg/runtime/iface.c中的函数 722 | 723 | static Itab* itab(InterfaceType *inter, Type *type, int32 canfail) 724 | 725 | 这里还有个小细节是类型描述符的方法表是按方法名排序过的,这样itab的动态构建过程更快一些,复杂度就是O(接口类型方法表长度+具体类型方法表长度) 726 | 727 | 可能有人有过疑问:编译器怎么知道某个类型是否实现了某个接口呢?这里正好解决了这个疑问: 728 | 729 | 在var i I = mytype 的过程中,如果发现mytype的类型描述符中的方法表跟接口I的类型描述符中的方法表对不上,这个初始化过程就会出错,提示说mytype没有实现接口中的某某方法。 730 | 731 | 再暴一个细节,所有的方法,在编译过程中都被转换成了函数 732 | 733 | 比如说 func (s *mytype) Get()会被变成func Get(s *mytype)。 734 | 735 | 接口值进行方法调用的时候,会找到itab中的方法表的某个函数指针,其第一个参数传的正是这个接口值的第一个域,即指向具体类型数据的指针。 736 | 737 | 在具体实现上面还有一些优化过程,比如接口值的真实数据指针那个域,如果真实数据大小是32位,就不用存指针了,直接存数据本身。再有就是对类接口类型interface{},其itab中是不需要方法表的,所以这里不是itab而直接是一个指向真实数据的类型描述结构的指针。 738 | 739 | ------------------------------------------------------------------------------------------------- 740 | ** 关于反射 741 | * 收集的一些关于go internals的链接: 742 | 743 | http://code.google.com/p/try-catch-finally/wiki/GoInternals 744 | 745 | http://research.swtch.com/gopackage 746 | 747 | http://research.swtch.com/interfaces 748 | 749 | http://research.swtch.com/goabstract 750 | 751 | http://blog.csdn.net/hopingwhite/article/details/5782888 752 | 753 | http://www.douban.com/note/251142022/ 调度器 754 | 755 | http://shiningray.cn/tcmalloc-thread-caching-malloc.html tcmalloc内存管理 756 | 757 | 方法原码分析 758 | 1独立的函数 759 | 2对象怎样调用方法 760 | 3组合对象(或接口)后怎样调用方法 761 | 4接口怎样调用方法 762 | 763 | 764 | 765 | 传值和传引用 766 | 767 | func httpGet(url string, wg sync.WaitGroup) { 768 | defer wg.Done() 769 | http.Get(url) 770 | } 771 | var wg sync.WaitGroup 772 | var urls = []string{ 773 | "http://www.golang.org/", 774 | "http://www.google.com/", 775 | "http://www.somestupidname.com/", 776 | } 777 | for _, url := range urls { 778 | wg.Add(1) 779 | go httpGet(url, wg) 780 | } 781 | wg.Wait() 782 | 783 | 运行,会发生程序死锁。为什么呢?因为Go语言是传值约定 784 | 785 | go httpGet(url, wg) 786 | 787 | httpGet中的wg实际上是复制的一份参数,对它的修改不会影响到原来的wg,也就是wg.Done()不会减少原来的wg的计数,于是发生了死锁。 788 | 789 | 790 | 791 | map中使用[]操作符获得的对象是不能直接修改状态 792 | 793 | 1.直接对map对象使用[]操作符获得的对象不能直接修改状态 794 | package main 795 | func main() { 796 | type person struct {age int} 797 | m := map[string]person{"steve":{10}} 798 | m["steve"].age = 100 // 编译错误:cannot assign to m["steve"].age 799 | } 800 | 801 | 2.通过查询map获得的对象是个拷贝,对此对象的修改不能影响原有对象的状态 802 | package main 803 | func main() { 804 | type person struct {age int} 805 | m := map[string]person {"steve":{10}} 806 | p := m["steve"] 807 | p.age = 100 // 没有改变map中对象的状态! 808 | println(p.age) 809 | println(m["steve"].age) 810 | } 811 | 输出: 812 | 100 813 | 10 814 | 解决方法: 815 | 1)map中存储指针而不是结构体 816 | package main 817 | func main() { 818 | type person struct {age int} 819 | m := map[string]*person{"steve":{10}} 820 | p := m["steve"] 821 | p.age = 100 822 | println(p.age) 823 | println(m["steve"].age) 824 | } 825 | 输出: 826 | 100 827 | 100 828 | 2)修改了对象状态以后重新加到map里 829 | 830 | 831 | import "fmt" 832 | type ErrLogin int 833 | func (n ErrLogin) Error() string { 834 | return fmt.Sprintf("failed to login(%x)", n) 835 | } 836 | func (n ErrLogin) Value() int { 837 | return int(n) 838 | } 839 | 然后就死循环了。。。 840 | 死循环的原因是,Sprintf是会通过接口查询知道n是一个接口类型,所以就会调用n的Error函数,但这个fmt.Sprintf本身就是在Error函数里调用的,所以就构成循环调用了。j 841 | 842 | 843 | 844 | 以上代码有没有什么问题?如果看出问题,OK,以下的不用再看了;没看出问题的请继续。 845 | 846 | 我们把以下代码用data race detector试一下,看看有什么结果: 847 | steve@stevepc:~/play$ go build -race race.go 848 | steve@stevepc:~/play$ ./race 849 | ================== 850 | WARNING: DATA RACE 851 | Write by goroutine 5: 852 | main.func·001() 853 | /home/steve/play/race.go:16 +0x76 854 | gosched0() 855 | /usr/local/go/src/pkg/runtime/proc.c:1218 +0x9f 856 | 857 | Previous write by goroutine 4: 858 | main.func·001() 859 | /home/steve/play/race.go:18 +0x9f 860 | gosched0() 861 | /usr/local/go/src/pkg/runtime/proc.c:1218 +0x9f 862 | 863 | Goroutine 5 (running) created at: 864 | main.main() 865 | /home/steve/play/race.go:21 +0x1aa 866 | runtime.main() 867 | /usr/local/go/src/pkg/runtime/proc.c:182 +0x91 868 | 869 | Goroutine 4 (finished) created at: 870 | main.main() 871 | /home/steve/play/race.go:21 +0x1aa 872 | runtime.main() 873 | /usr/local/go/src/pkg/runtime/proc.c:182 +0x91 874 | 875 | ================== 876 | Found 1 data race(s) 877 | 878 | 我们看到data race detector有报告data race,也就是多个goroutine同时访问同一个数据。根据上面的报告,data race发生在16和18行,对应的代码是"count++"和"count--"。即同时有一个goroutine在执行"count++"而另一个goroutine在执行"count--"。 879 | 这怎么可能呢?ch的长度明明是1,怎么可能两个goroutine同时去访问count呢? 880 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /graphviz/channel.dot: -------------------------------------------------------------------------------- 1 | digraph { 2 | rankdir = LR; 3 | node[shape=record] 4 | Hchan[label="struct\ Hchan|qcount|dataqsize|elemsize|alg|sendx|recvx|recvq|sendq|LOCK|..."] 5 | WaitQ[label="struct\ WaitQ|{first|last}"] 6 | SudoG1[label="struct\ SudoG|{G*\ g|byte*\ elem|next}"] 7 | SudoG2[label="struct\ SudoG|{G*\ g|byte*\ elem}"] 8 | dots[shape="plaintext",label="..."] 9 | Hchan:recvq->WaitQ 10 | WaitQ:first:s->SudoG1 11 | WaitQ:last:se->SudoG2 12 | SudoG1:link->dots->SudoG2 13 | {rank=same;WaitQ, SudoG1} 14 | } -------------------------------------------------------------------------------- /graphviz/funcall.dot: -------------------------------------------------------------------------------- 1 | digraph { 2 | rankdir = LR 3 | node[shape=record] 4 | Memory[label="...|为ret2保留空位|为ret1保留空位|参数3|参数2|参数1|保存PC|f的栈|..."] 5 | FP[label="FP"] 6 | SP[label="SP"] 7 | 8 | FP->Memory:fp 9 | SP->Memory:sp 10 | } -------------------------------------------------------------------------------- /graphviz/goroutine_state.dot: -------------------------------------------------------------------------------- 1 | digraph G { 2 | Grunnable->Grunning; 3 | Grunning->Gwaiting; 4 | Grunning->Gdead; 5 | Grunning->Gsyscall; 6 | Gsyscall->Grunning; 7 | Gsyscall->Grunnable; 8 | Gwaiting->Grunnable; 9 | } -------------------------------------------------------------------------------- /image/gc_bitmap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/image/gc_bitmap.jpg -------------------------------------------------------------------------------- /image/m_g.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/image/m_g.jpg -------------------------------------------------------------------------------- /image/mcentral.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/image/mcentral.jpg -------------------------------------------------------------------------------- /image/worker.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/image/worker.jpg -------------------------------------------------------------------------------- /zh/01.0.md: -------------------------------------------------------------------------------- 1 | # 1 如何阅读 2 | 3 | 欢迎来到Go的世界,让我们开始探索吧! 4 | 5 | Go是一种新的语言,一种并发的、带垃圾回收的、快速编译的语言。它具有以下特点: 6 | 7 | - 它可以在一台计算机上用几秒钟的时间编译一个大型的Go程序。 8 | - Go为软件构造提供了一种模型,它使依赖分析更加容易,且避免了大部分C风格include文件与库的开头。 9 | - Go是静态类型的语言,它的类型系统没有层级。因此用户不需要在定义类型之间的关系上花费时间,这样感觉起来比典型的面向对象语言更轻量级。 10 | - Go完全是垃圾回收型的语言,并为并发执行与通信提供了基本的支持。 11 | - 按照其设计,Go打算为多核机器上系统软件的构造提供一种方法。 12 | 13 | Go是一种编译型语言,它结合了解释型语言的游刃有余,动态类型语言的开发效率,以及静态类型的安全性。它也打算成为现代的,支持网络与多核计算的语言。要满足这些目标,需要解决一些语言上的问题:一个富有表达能力但轻量级的类型系统,并发与垃圾回收机制,严格的依赖规范等等。这些无法通过库或工具解决好,因此Go也就应运而生了。 14 | 15 | 在本章中,我们将讲述Go的安装方法,以及如何阅读本书。 16 | 17 | ## links 18 | * [目录]() 19 | * 下一节: [从源码安装Go](<01.1.md>) 20 | -------------------------------------------------------------------------------- /zh/01.1.md: -------------------------------------------------------------------------------- 1 | # 1.1 从源代码安装Go 2 | 3 | 本书面向的是已经对Go语言有一定的经验,希望能了解它的底层机制的用户。因此,只推荐从源代码安装Go。 4 | 5 | ## Go源码安装 6 | 在Go的源代码中,有些部分是用Plan 9 C和AT&T汇编写的,因此假如你要想从源码安装,就必须安装C的编译工具。 7 | 8 | 在Mac系统中,只要你安装了Xcode,就已经包含了相应的编译工具。 9 | 10 | 在类Unix系统中,需要安装gcc等工具。例如Ubuntu系统可通过在终端中执行`sudo apt-get install gcc libc6-dev`来安装编译工具。 11 | 12 | 在Windows系统中,你需要安装MinGW,然后通过MinGW安装gcc,并设置相应的环境变量。 13 | 14 | Go使用[Mercurial][hg]进行版本管理,首先你必须安装了Mercurial,然后才能下载。假设你已经安装好Mercurial,执行如下代码: 15 | 16 | 假设已经位于Go的安装目录 `$GO_INSTALL_DIR`下 17 | 18 | hg clone -u release https://code.google.com/p/go 19 | cd go/src 20 | ./all.bash 21 | 22 | 运行all.bash后出现"ALL TESTS PASSED"字样时才算安装成功。 23 | 24 | 上面是Unix风格的命令,Windows下的安装方式类似,只不过是运行all.bat,调用的编译器是MinGW的gcc。 25 | 26 | 然后设置几个环境变量, 27 | 28 | export GOROOT=$HOME/go 29 | export GOBIN=$GOROOT/bin 30 | export PATH=$PATH:$GOBIN 31 | 32 | 看到如下图片即说明你已经安装成功 33 | 34 | ![](images/1.1.mac.png?raw=true) 35 | 36 | 图1.1 源码安装之后执行Go命令的图 37 | 38 | 如果出现Go的Usage信息,那么说明Go已经安装成功了;如果出现该命令不存在,那么可以检查一下自己的PATH环境变中是否包含了Go的安装目录。 39 | 40 | ## links 41 | * [目录]() 42 | * 上一节: [如何阅读](<01.0.md>) 43 | * 下一节: [本书的组织结构](<01.2.md>) 44 | 45 | [downlink]: http://code.google.com/p/go/downloads/list "Go安装包下载" 46 | [hg]: http://mercurial.selenic.com/downloads/ "Mercurial下载" 47 | -------------------------------------------------------------------------------- /zh/01.2.md: -------------------------------------------------------------------------------- 1 | # 1.2 本书的组织结构 2 | 3 | ## 本书结构 4 | 5 | 第二章首先会介绍一些Go的基本数据结构的实现,如slice和map。 6 | 7 | 第三章会介绍Go语言中的函数调用协议。 8 | 9 | 第四章分析runtime初始化过程。 10 | 11 | 第五章是goroutine的调度。 12 | 13 | 第六章分析Go语言中的内存管理。 14 | 15 | 第七章分析Go语言中一些高级数据结构的实现。 16 | 17 | 第八章是网络封装的实现 18 | 19 | 第九章讲cgo使用的一些技术 20 | 21 | 第十章是其它一些杂项 22 | 23 | ## 推荐的阅读方式 24 | 25 | 本书的写作基本上是按一个循序浅近的过程。大多数章节可以独立阅读,如内存管理,goroutine调度等。而某些知识则需要前面章节的一些基础知识,比如cgo必须了解前面函数调用协议方面的一些知识,第七章高级数据结构最好对前面内存管理和goroutine调度有一定的了解。 26 | 27 | 推荐的阅读方式还是按本文章节顺序,如果读者已经有一定基础,也可以只挑自己感兴趣的章节阅读。 28 | 29 | 如果想更深入的了解Go语言的内部实现,希望读者能拿着Go的源代码亲自分析。通过自己学习研究得到的东西才是理解最深的。 30 | 31 | ## links 32 | * [目录]() 33 | * 上一节: [从源码安装Go](<01.1.md>) 34 | * 下一节: [基本技巧](<01.3.md>) 35 | -------------------------------------------------------------------------------- /zh/01.3.md: -------------------------------------------------------------------------------- 1 | # 1.3 基本技巧 2 | 研究Go的内部实现,这里介绍一些基本的技巧。 3 | 4 | ## 阅读源代码 5 | 6 | Go语言的源代码布局是有一些规律的。假定读者在$GOROOT下: 7 | 8 | - ./misc 一些工具 9 | - ./src 源代码 10 | - ./src/cmd 命令工具,包括6c, 6l, 6g等等。最后打包成go命令。 11 | - ./src/pkg 各个package的源代码 12 | - ./src/pkg/runtime Go的runtime包,本书分析的最主要的部分 13 | - AUTHORS — 文件,官方 Go语言作者列表 14 | |– CONTRIBUTORS — 文件,第三方贡献者列表 15 | |– LICENSE — 文件,Go语言发布授权协议 16 | |– PATENTS — 文件,专利 17 | |– README — 文件,README文件,大家懂的。提一下,经常有人说:Go官网打不开啊,怎么办?其实,在README中说到了这个。该文件还提到,如果通过二进制安装,需要设置GOROOT环境变量;如果你将Go放在了/usr/local/go中,则可以不设置该环境变量(Windows下是C:\go)。当然,建议不管什么时候都设置GOROOT。另外,确保$GOROOT/bin在PATH目录中。 18 | |– VERSION — 文件,当前Go版本 19 | |– api — 目录,包含所有API列表,方便IDE使用 20 | |– doc — 目录,Go语言的各种文档,官网上有的,这里基本会有,这也就是为什么说可以本地搭建”官网”。这里面有不少其他资源,比如gopher图标之类的。 21 | |– favicon.ico — 文件,官网logo 22 | |– include — 目录,Go 基本工具依赖的库的头文件 23 | |– lib — 目录,文档模板 24 | |– misc — 目录,其他的一些工具,相当于大杂烩,大部分是各种编辑器的Go语言支持,还有cgo的例子等 25 | |– robots.txt — 文件,搜索引擎robots文件 26 | |– src — 目录,Go语言源码:基本工具(编译器等)、标准库 27 | `– test — 目录,包含很多测试程序(并非_test.go方式的单元测试,而是包含main包的测试),包括一些fixbug测试。可以通过这个学到一些特性的使用。 28 | 29 | 学习Go语言的内部实现,主要依靠对源代码的分析,所以阅读源代码是很好的方式。linus谈到如何学习Linux内核时也说过"Read the F**ing Source code"。 30 | 31 | ## 使用调试器 32 | 33 | 通过gdb下断点,跟踪程序的行为。调试跟代码的方式是源代码阅读的一种辅助手段。 34 | 35 | 用户代码入口是在main.main,runtime库中的函数可以通过runtime.XXX断点捕获。比如写一个test.go: 36 | 37 | package main 38 | 39 | import ( 40 | "fmt" 41 | ) 42 | 43 | func main() { 44 | fmt.Println("hello world!") 45 | } 46 | 47 | 编译,调试 48 | 49 | go build test.go 50 | gdb test 51 | 52 | 可以在main.main处下断点,单步执行,你会发现进入了一个runtime·convT2E的函数。这个就是由于fmt.Println接受的是一个interface,而传入的是一个string,这里会做一个转换。以这个为一个突破点去跟代码,就可以研究Go语言中具体类型如何转为interface抽象类型。 53 | 54 | ## 分析生成的汇编代码 55 | 56 | 有时候分析会需要研究生成的汇编代码,这里介绍生成汇编代码的方法。 57 | 58 | 59 | go tool 6g -S hello.go 60 | 61 | -S参数表示打印出汇编代码,更多参数可以通过-h参数查看。 62 | 63 | go tool 6g -h 64 | 65 | 或者可以反汇编生成的可执行文件: 66 | 67 | go build test.go 68 | go tool 6l -a test | less 69 | 70 | 本机是amd64的机器,如果是i386的机器,则命令是8g 71 | 72 | 需要注意的是用6g的-S生成的汇编代码和6l -a生成的反汇编代码是不太一样的。前者是直接对源代码进行汇编,后者是对可执行文件进行反汇编。在6l进行链接过程中,可能会在原汇编文件基础上插入新的指令。所以6l反汇编出来的是最接近真实代码的。 73 | 74 | 不过Go的汇编语法跟常用的有点不太一致,可能读起来会不太习惯。还有另一种方式,就是在用gdb调试的过程中查看汇编。 75 | 76 | gdb test 77 | b main.main 78 | disas 79 | 80 | ## links 81 | * [目录]() 82 | * 上一节: [本书的组织结构](<01.2.md>) 83 | * 下一节: [基本数据结构](<02.0.md>) 84 | -------------------------------------------------------------------------------- /zh/02.0.md: -------------------------------------------------------------------------------- 1 | # 2 基本数据结构 2 | 3 | 这一章中我们将看一下基本的数据结构,都是Go语言内置的类型。这些知识很基础,但是理解它们非常重要。 4 | 5 | 我们将从最基本的类型开始,Go语言的基本类型部分跟C语言很类似,熟习C语言的朋友们应该不会陌生。我们也将对slice和map的实现一窥究竟。看完这章,你会知道slice不是一个指针,它在栈中是占三个机器字节的。 6 | 7 | 好吧,让我们开始吧! 8 | 9 | ## links 10 | * [目录]() 11 | * 上一节: [基本技巧](<01.3.md>) 12 | * 下一节: [基本类型](<02.1.md>) -------------------------------------------------------------------------------- /zh/02.1.md: -------------------------------------------------------------------------------- 1 | # 2.1 基本类型 2 | 3 | 向新手介绍Go语言时,解释一下Go中各种类型变量在内存中的布局通常有利于帮助他们加深理解。 4 | 5 | 先看一些基础的例子: 6 | 7 | ![](http://research.swtch.com/godata1.png) 8 | 9 | 变量i属于类型int,在内存中用一个32位字长(word)表示。(32位内存布局方式) 10 | 11 | 变量j由于做了精确的转换,属于int32类型。尽管i和j有着相同的内存布局,但是它们属于不同的类型:赋值操作 `i = j` 是一种类型错误,必须写成更精确的转换方式:`i = int(j)`。 12 | 13 | 变量f属于float类型,Go语言当前使用32位浮点型值表示(float32)。它与int32很像,但是内部实现不同。 14 | 15 | 接下来,变量bytes的类型是\[5\]byte,一个由5个字节组成的数组。它的内存表示就是连起来的5个字节,就像C的数组。类似地,变量primes是4个int的数组。 16 | 17 | ## 结构体和指针 18 | 19 | 与C相同而与Java不同的是,Go语言让程序员决定何时使用指针。举例来说,这种类型定义: 20 | 21 | type Point struct { X, Y int } 22 | 23 | 先来定义一个简单的struct类型,名为Point,表示内存中两个相邻的整数。 24 | 25 | ![](http://research.swtch.com/godata1a.png) 26 | 27 | ` Point{10,20}`表示一个已初始化的Point类型。对它进行取地址表示一个指向刚刚分配和初始化的Point类型的指针。前者在内存中是两个词,而后者是一个指向两个词的指针。 28 | 29 | 结构体的域在内存中是紧挨着排列的。 30 | 31 | type Rect1 struct { Min, Max Point } 32 | type Rect2 struct { Min, Max *Point } 33 | 34 | ![](http://research.swtch.com/godata1b.png) 35 | 36 | Rect1是一个具有两个Point类型属性的结构体,由在一行的两个Point--四个int代表。Rect2是一个具有两个`*Point`类型属性的结构体,由两个*Point表示。 37 | 38 | 使用过C的程序员可能对`Point`和`*Point`的不同毫不见怪,但用惯Java或Python的程序员们可能就不那么轻松了。Go语言给了程序员基本内存层面的控制,由此提供了诸多能力,如控制给定数据结构集合的总大小、内存分配的次数、内存访问模式以及建立优秀系统的所有要点。 39 | 40 | ## 字符串 41 | 42 | 有了前面的准备,我们就可以开始研究更有趣的数据类型了。 43 | 44 | ![](http://research.swtch.com/godata2.png) 45 | 46 | (灰色的箭头表示已经实现的但不能直接可见的指针) 47 | 48 | 字符串在Go语言内存模型中用一个2字长的数据结构表示。它包含一个指向字符串存储数据的指针和一个长度数据。因为string类型是不可变的,对于多字符串共享同一个存储数据是安全的。切分操作`str[i:j]`会得到一个新的2字长结构,一个可能不同的但仍指向同一个字节序列(即上文说的存储数据)的指针和长度数据。这意味着字符串切分可以在不涉及内存分配或复制操作。这使得字符串切分的效率等同于传递下标。 49 | 50 | (说句题外话,在Java和其他语言里有一个有名的“疑难杂症”:在你分割字符串并保存时,对于源字符串的引用在内存中仍然保存着完整的原始字符串--即使只有一小部分仍被需要,Go也有这个“毛病”。另一方面,我们努力但又失败了的是,让字符串分割操作变得昂贵--包含一次分配和一次复制。在大多数程序中都避免了这么做。) 51 | 52 | ## links 53 | * [目录]() 54 | * 上一节: [基本数据结构](<02.0.md>) 55 | * 下一节: [slice](<02.2.md>) -------------------------------------------------------------------------------- /zh/02.2.md: -------------------------------------------------------------------------------- 1 | # 2.2 slice 2 | 3 | 一个slice是一个数组某个部分的引用。在内存中,它是一个包含3个域的结构体:指向slice中第一个元素的指针,slice的长度,以及slice的容量。长度是下标操作的上界,如x[i]中i必须小于长度。容量是分割操作的上界,如x[i:j]中j不能大于容量。 4 | 5 | ![](http://research.swtch.com/godata3.png) 6 | 7 | 数组的slice并不会实际复制一份数据,它只是创建一个新的数据结构,包含了另外的一个指针,一个长度和一个容量数据。 8 | 如同分割一个字符串,分割数组也不涉及复制操作:它只是新建了一个结构来放置一个不同的指针,长度和容量。在例子中,对`[]int{2,3,5,7,11}`求值操作会创建一个包含五个值的数组,并设置x的属性来描述这个数组。分割表达式`x[1:3]`并不分配更多的数据:它只是写了一个新的slice结构的属性来引用相同的存储数据。在例子中,长度为2--只有y[0]和y[1]是有效的索引,但是容量为4--y[0:4]是一个有效的分割表达式。 9 | 10 | 由于slice是不同于指针的多字长结构,分割操作并不需要分配内存,甚至没有通常被保存在堆中的slice头部。这种表示方法使slice操作和在C中传递指针、长度对一样廉价。Go语言最初使用一个指向以上结构的指针来表示slice,但是这样做意味着每个slice操作都会分配一块新的内存对象。即使使用了快速的分配器,还是给垃圾收集器制造了很多没有必要的工作。移除间接引用及分配操作可以让slice足够廉价,以避免传递显式索引。 11 | 12 | ## slice的扩容 13 | 14 | 其实slice在Go的运行时库中就是一个C语言动态数组的实现,在$GOROOT/src/pkg/runtime/runtime.h中可以看到它的定义: 15 | 16 | struct Slice 17 | { // must not move anything 18 | byte* array; // actual data 19 | uintgo len; // number of elements 20 | uintgo cap; // allocated number of elements 21 | }; 22 | 23 | 在对slice进行append等操作时,可能会造成slice的自动扩容。其扩容时的大小增长规则是: 24 | 25 | - 如果新的大小是当前大小2倍以上,则大小增长为新大小 26 | - 否则循环以下操作:如果当前大小小于1024,按每次2倍增长,否则每次按当前大小1/4增长。直到增长的大小超过或等于新大小。 27 | 28 | ## make和new 29 | Go有两个数据结构创建函数:new和make。两者的区别在学习Go语言的初期是一个常见的混淆点。基本的区别是`new(T)`返回一个`*T`,返回的这个指针可以被隐式地消除引用(图中的黑色箭头)。而`make(T, args)`返回一个普通的T。通常情况下,T内部有一些隐式的指针(图中的灰色箭头)。一句话,new返回一个指向已清零内存的指针,而make返回一个复杂的结构。 30 | 31 | ![](http://research.swtch.com/godata4.png) 32 | 33 | 有一种方法可以统一这两种创建方式,但是可能会与C/C++的传统有显著不同:定义`make(*T)`来返回一个指向新分配的T的指针,这样一来,new(Point)得写成make(*Point)。但这样做实在是和人们期望的分配函数太不一样了,所以Go没有采用这种设计。 34 | 35 | ## slice与unsafe.Pointer相互转换 36 | 37 | 有时候可能需要使用一些比较tricky的技巧,比如利用make弄一块内存自己管理,或者用cgo之类的方式得到的内存,转换为Go类型使用。 38 | 39 | 从slice中得到一块内存地址是很容易的: 40 | 41 | s := make([]byte, 200) 42 | ptr := unsafe.Pointer(&s[0]) 43 | 44 | 从一个内存指针构造出Go语言的slice结构相对麻烦一些,比如其中一种方式: 45 | 46 | var ptr unsafe.Pointer 47 | s := ((*[1<<10]byte)(ptr))[:200] 48 | 49 | 先将`ptr`强制类型转换为另一种指针,一个指向`[1<<10]byte`数组的指针,这里数组大小其实是假的。然后用slice操作取出这个数组的前200个,于是`s`就是一个200个元素的slice。 50 | 51 | 或者这种方式: 52 | 53 | var ptr unsafe.Pointer 54 | var s1 = struct { 55 | addr uintptr 56 | len int 57 | cap int 58 | }{ptr, length, length} 59 | s := *(*[]byte)(unsafe.Pointer(&s1)) 60 | 61 | 把slice的底层结构写出来,将addr,len,cap等字段写进去,将这个结构体赋给s。相比上一种写法,这种更好的地方在于cap更加自然,虽然上面写法中实际上1<<10就是cap。 62 | 63 | 或者使用reflect.SliceHeader的方式来构造slice,比较推荐这种做法: 64 | 65 | var o []byte 66 | sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&o))) 67 | sliceHeader.Cap = length 68 | sliceHeader.Len = length 69 | sliceHeader.Data = uintptr(ptr) 70 | 71 | ## links 72 | * [目录]() 73 | * 上一节: [基本类型](<02.1.md>) 74 | * 下一节: [map的实现](<02.3.md>) 75 | -------------------------------------------------------------------------------- /zh/02.3.md: -------------------------------------------------------------------------------- 1 | # 2.3 map的实现 2 | 3 | Go中的map在底层是用哈希表实现的,你可以在 $GOROOT/src/pkg/runtime/hashmap.goc 找到它的实现。 4 | 5 | ## 数据结构 6 | 哈希表的数据结构中一些关键的域如下所示: 7 | 8 | struct Hmap 9 | { 10 | uint8 B; // 可以容纳2^B个项 11 | uint16 bucketsize; // 每个桶的大小 12 | 13 | byte *buckets; // 2^B个Buckets的数组 14 | byte *oldbuckets; // 前一个buckets,只有当正在扩容时才不为空 15 | }; 16 | 17 | 上面给出的结构体只是Hmap的部分的域。需要注意到的是,这里直接使用的是Bucket的数组,而不是Bucket*指针的数组。这意味着,第一个Bucket和后面溢出链的Bucket分配有些不同。第一个Bucket是用的一段连续的内存空间,而后面溢出链的Bucket的空间是使用mallocgc分配的。 18 | 19 | 这个hash结构使用的是一个可扩展哈希的算法,由hash值mod当前hash表大小决定某一个值属于哪个桶,而hash表大小是2的指数,即上面结构体中的2^B。每次扩容,会增大到上次大小的两倍。结构体中有一个buckets和一个oldbuckets是用来实现增量扩容的。正常情况下直接使用buckets,而oldbuckets为空。如果当前哈希表正在扩容中,则oldbuckets不为空,并且buckets大小是oldbuckets大小的两倍。 20 | 21 | 具体的Bucket结构如下所示: 22 | 23 | struct Bucket 24 | { 25 | uint8 tophash[BUCKETSIZE]; // hash值的高8位....低位从bucket的array定位到bucket 26 | Bucket *overflow; // 溢出桶链表,如果有 27 | byte data[1]; // BUCKETSIZE keys followed by BUCKETSIZE values 28 | }; 29 | 30 | 其中BUCKETSIZE是用宏定义的8,每个bucket中存放最多8个key/value对, 如果多于8个,那么会申请一个新的bucket,并将它与之前的bucket链起来。 31 | 32 | 按key的类型采用相应的hash算法得到key的hash值。将hash值的低位当作Hmap结构体中buckets数组的index,找到key所在的bucket。将hash的高8位存储在了bucket的tophash中。**注意,这里高8位不是用来当作key/value在bucket内部的offset的,而是作为一个主键,在查找时对tophash数组的每一项进行顺序匹配的**。先比较hash值高位与bucket的tophash[i]是否相等,如果相等则再比较bucket的第i个的key与所给的key是否相等。如果相等,则返回其对应的value,反之,在overflow buckets中按照上述方法继续寻找。 33 | 34 | 整个hash的存储如下图所示(临时先采用了XX同学画的图,这个图有点问题): 35 | 36 | ![](images/2.2.map.png?raw=true) 37 | 38 | 图2.2 HMap的存储结构 39 | 40 | 注意一个细节是Bucket中key/value的放置顺序,是将keys放在一起,values放在一起,为什么不将key和对应的value放在一起呢?如果那么做,存储结构将变成key1/value1/key2/value2… 设想如果是这样的一个map[int64]int8,考虑到字节对齐,会浪费很多存储空间。不得不说通过上述的一个小细节,可以看出Go在设计上的深思熟虑。 41 | 42 | ## 增量扩容 43 | 44 | 大家都知道哈希表表就是以空间换时间,访问速度是直接跟填充因子相关的,所以当哈希表太满之后就需要进行扩容。 45 | 46 | 如果扩容前的哈希表大小为2^B,扩容之后的大小为2^(B+1),每次扩容都变为原来大小的两倍,哈希表大小始终为2的指数倍,则有(hash mod 2^B)等价于(hash & (2^B-1))。这样可以简化运算,避免了取余操作。 47 | 48 | 假设扩容之前容量为X,扩容之后容量为Y,对于某个哈希值hash,一般情况下(hash mod X)不等于(hash mod Y),所以扩容之后要重新计算每一项在哈希表中的新位置。当hash表扩容之后,需要将那些旧的pair重新哈希到新的table上(源代码中称之为evacuate), 这个工作并没有在扩容之后一次性完成,而是逐步的完成(在insert和remove时每次搬移1-2个pair),Go语言使用的是增量扩容。 49 | 50 | 为什么会增量扩容呢?主要是缩短map容器的响应时间。假如我们直接将map用作某个响应实时性要求非常高的web应用存储,如果不采用增量扩容,当map里面存储的元素很多之后,扩容时系统就会卡往,导致较长一段时间内无法响应请求。不过增量扩容本质上还是将总的扩容时间分摊到了每一次哈希操作上面。 51 | 52 | 扩容会建立一个大小是原来2倍的新的表,将旧的bucket搬到新的表中之后,并不会将旧的bucket从oldbucket中删除,而是加上一个已删除的标记。 53 | 54 | 正是由于这个工作是逐渐完成的,这样就会导致一部分数据在old table中,一部分在new table中, 所以对于hash table的insert, remove, lookup操作的处理逻辑产生影响。只有当所有的bucket都从旧表移到新表之后,才会将oldbucket释放掉。 55 | 56 | 扩容的填充因子是多少呢?如果grow的太频繁,会造成空间的利用率很低, 如果很久才grow,会形成很多的overflow buckets,查找的效率也会下降。 这个平衡点如何选取呢(在go中,这个平衡点是有一个宏控制的(#define LOAD 6.5), 它的意思是这样的,如果table中元素的个数大于table中能容纳的元素的个数, 那么就触发一次grow动作。那么这个6.5是怎么得到的呢?原来这个值来源于作者的一个测试程序,遗憾的是没能找到相关的源码,不过作者给出了测试的结果: 57 | 58 | LOAD %overflow bytes/entry hitprobe missprobe 59 | 4.00 2.13 20.77 3.00 4.00 60 | 4.50 4.05 17.30 3.25 4.50 61 | 5.00 6.85 14.77 3.50 5.00 62 | 5.50 10.55 12.94 3.75 5.50 63 | 6.00 15.27 11.67 4.00 6.00 64 | 6.50 20.90 10.79 4.25 6.50 65 | 7.00 27.14 10.15 4.50 7.00 66 | 7.50 34.03 9.73 4.75 7.50 67 | 8.00 41.10 9.40 5.00 8.00 68 | 69 | %overflow = percentage of buckets which have an overflow bucket 70 | bytes/entry = overhead bytes used per key/value pair 71 | hitprobe = # of entries to check when looking up a present key 72 | missprobe = # of entries to check when looking up an absent key 73 | 74 | 可以看出作者取了一个相对适中的值。 75 | 76 | ## 查找过程 77 | 1. 根据key计算出hash值。 78 | 2. 如果存在old table, 首先在old table中查找,如果找到的bucket已经evacuated,转到步骤3。 反之,返回其对应的value。 79 | 3. 在new table中查找对应的value。 80 | 81 | 这里一个细节需要注意一下。不认真看可能会以为低位用于定位bucket在数组的index,那么高位就是用于key/valule在bucket内部的offset。事实上高8位不是用作offset的,而是用于加快key的比较的。 82 | 83 | do { //对每个桶b 84 | //依次比较桶内的每一项存放的tophash与所求的hash值高位是否相等 85 | for(i = 0, k = b->data, v = k + h->keysize * BUCKETSIZE; i < BUCKETSIZE; i++, k += h->keysize, v += h->valuesize) { 86 | if(b->tophash[i] == top) { 87 | k2 = IK(h, k); 88 | t->key->alg->equal(&eq, t->key->size, key, k2); 89 | if(eq) { //相等的情况下再去做key比较... 90 | *keyp = k2; 91 | return IV(h, v); 92 | } 93 | } 94 | } 95 | b = b->overflow; //b设置为它的下一下溢出链 96 | } while(b != nil); 97 | 98 | ## 插入过程分析 99 | 1. 根据key算出hash值,进而得出对应的bucket。 100 | 2. 如果bucket在old table中,将其重新散列到new table中。 101 | 3. 在bucket中,查找空闲的位置,如果已经存在需要插入的key,更新其对应的value。 102 | 4. 根据table中元素的个数,判断是否grow table。 103 | 5. 如果对应的bucket已经full,重新申请新的bucket作为overbucket。 104 | 6. 将key/value pair插入到bucket中。 105 | 106 | 这里也有几个细节需要注意一下。 107 | 108 | 在扩容过程中,oldbucket是被冻结的,查找时会在oldbucket中查找,但不会在oldbucket中插入数据。如果在oldbucket是找到了相应的key,做法是将它迁移到新bucket后加入evalucated标记。并且还会额外的迁移另一个pair。 109 | 110 | 然后就是只要在某个bucket中找到第一个空位,就会将key/value插入到这个位置。也就是位置位于bucket前面的会覆盖后面的(类似于存储系统设计中做删除时的常用的技巧之一,直接用新数据追加方式写,新版本数据覆盖老版本数据)。找到了相同的key或者找到第一个空位就可以结束遍历了。不过这也意味着做删除时必须完全的遍历bucket所有溢出链,将所有的相同key数据都删除。所以目前map的设计是为插入而优化的,删除效率会比插入低一些。 111 | 112 | ## map设计中的性能优化 113 | 读完map源代码发现作者还是做了很多设计上的选择的。本人水平有限,谈不上优劣的点评,这里只是拿出来与读者分享。 114 | 115 | HMap中是Bucket的数组,而不是Bucket指针的数组。好的方面是可以一次分配较大内存,减少了分配次数,避免多次调用mallocgc。但相应的缺点,其一是可扩展哈希的算法并没有发生作用,扩容时会造成对整个数组的值拷贝(如果实现上用Bucket指针的数组就是指针拷贝了,代价小很多)。其二是首个bucket与后面产生了不一致性。这个会使删除逻辑变得复杂一点。比如删除后面的溢出链可以直接删除,而对于首个bucket,要等到evalucated完毕后,整个oldbucket删除时进行。 116 | 117 | 没有重用设freelist重用删除的结点。作者把这个加了一个TODO的注释,不过想了一下觉得这个做的意义不大。因为一方面,bucket大小并不一致,重用比较麻烦。另一方面,下层存储已经做过内存池的实现了,所以这里不做重用也会在内存分配那一层被重用的, 118 | 119 | bucket直接key/value和间接key/value优化。这个优化做得蛮好的。注意看代码会发现,如果key或value小于128字节,则它们的值是直接使用的bucket作为存储的。否则bucket中存储的是指向实际key/value数据的指针, 120 | 121 | bucket存8个key/value对。查找时进行顺序比较。第一次发现高位居然不是用作offset,而是用于加快比较的。定位到bucket之后,居然是一个顺序比较的查找过程。后面仔细想了想,觉得还行。由于bucket只有8个,顺序比较下来也不算过分。仍然是O(1)只不过前面系数大一点点罢了。相当于hash到一个小范围之后,在这个小范围内顺序查找。 122 | 123 | 插入删除的优化。前面已经提过了,插入只要找到相同的key或者第一个空位,bucket中如果存在一个以上的相同key,前面覆盖后面的(只是如果,实际上不会发生)。而删除就需要遍历完所有bucket溢出链了。这样map的设计就是为插入优化的。考虑到一般的应用场景,这个应该算是很合理的。 124 | 125 | 作者还列了另个2个TODO:将多个几乎要empty的bucket合并;如果table中元素很少,考虑shrink table。(毕竟现在的实现只是单纯的grow)。 126 | 127 | ## links 128 | * [目录]() 129 | * 上一节: [slice](<02.2.md>) 130 | * 下一节: [nil的语义](<02.4.md>) 131 | -------------------------------------------------------------------------------- /zh/02.4.md: -------------------------------------------------------------------------------- 1 | # 2.4 nil的语义 2 | 3 | 什么?nil是一种数据结构么?为什么会讲到它,没搞错吧?没搞错。不仅仅是Go语言中,每门语言中nil都是非常重要的,它代表的是空值的语义。 4 | 5 | 在不同语言中,表示空这个概念都有细微不同。比如在scheme语言(一种lisp方言)中,nil是true的!而在ruby语言中,一切都是对象,连nil也是一个对象!在C中NULL跟0是等价的。 6 | 7 | 按照Go语言规范,任何类型在未初始化时都对应一个零值:布尔类型是false,整型是0,字符串是"",而指针,函数,interface,slice,channel和map的零值都是nil。 8 | 9 | ## interface 10 | 11 | 一个interface在没有进行初始化时,对应的值是nil。也就是说`var v interface{}`, 12 | 13 | 此时v就是一个nil。在底层存储上,它是一个空指针。与之不同的情况是,interface值为空。比如: 14 | 15 | var v *T 16 | var i interface{} 17 | i = v 18 | 19 | 此时i是一个interface,它的值是nil,但它自身不为nil。 20 | 21 | Go中的error其实就是一个实现了Error方法的接口: 22 | 23 | type error interface { 24 | Error() string 25 | } 26 | 27 | 因此,我们可以自定义一个error: 28 | 29 | type Error struct { 30 | errCode uint8 31 | } 32 | func (e *Error) Error() string { 33 | switch e.errCode { 34 | case 1: 35 | return "file not found" 36 | case 2: 37 | return "time out" 38 | case 3: 39 | return "permission denied" 40 | default: 41 | return "unknown error" 42 | } 43 | } 44 | 45 | 如果我们这样使用它: 46 | 47 | func checkError(err error) { 48 | if err != nil { 49 | panic(err) 50 | } 51 | } 52 | var e *Error 53 | checkError(e) 54 | 55 | e是nil的,但是当我们checkError时就会panic。请读者思考一下为什么? 56 | 57 | 总之,interface跟C语言的指针一样非常灵活,关于空的语义,也跟空指针一样容易困扰新手的,需要注意。 58 | 59 | ## string和slice 60 | 61 | string的空值是"",它是不能跟nil比较的。即使是空的string,它的大小也是两个机器字长的。slice也类似,它的空值并不是一个空指针,而是结构体中的指针域为空,空的slice的大小也是三个机器字长的。 62 | 63 | ## channel和map 64 | 65 | channel跟string或slice有些不同,它在栈上只是一个指针,实际的数据都是由指针所指向的堆上面。 66 | 67 | 跟channel相关的操作有:初始化/读/写/关闭。channel未初始化值就是nil,未初始化的channel是不能使用的。下面是一些操作规则: 68 | 69 | * 读或者写一个nil的channel的操作会永远阻塞。 70 | * 读一个关闭的channel会立刻返回一个channel元素类型的零值。 71 | * 写一个关闭的channel会导致panic。 72 | 73 | map也是指针,实际数据在堆中,未初始化的值是nil。 74 | 75 | ## links 76 | * [目录]() 77 | * 上一节: [map的实现](<02.3.md>) 78 | * 下一节: [函数调用协议](<03.0.md>) 79 | -------------------------------------------------------------------------------- /zh/03.0.md: -------------------------------------------------------------------------------- 1 | # 3 函数调用协议 2 | 3 | 理解Go的函数调用协议对于研究其内部实现非常重要。这里将会介绍Go进行函数调用时的内存布局,参数传递和返回值的约定。正如C和汇编都是同一套约定所以能相互调用一样,Go和C以及汇编也是要满足某些约定才能够相互调用。 4 | 5 | 本章先从Go调用C和汇编的例子开始(非cgo方式),通过分析其实现学习Go的函数调用协议。然后将会研究go和defer关键字等神奇的魔法。接着会研究连续栈的实现,最后看一下闭包。 6 | 7 | 这一章的内容将是后面研究cgo,goroutine实现的基础。连续栈技术是Go能够开千千万万条“线程”而不耗尽内存的基本保证,也为cgo带来了很大的限制,这些将会在后面章节中再讨论。 8 | 9 | 好,让我们进入正题吧! 10 | 11 | ## links 12 | * [目录]() 13 | * 上一节: [nil的语义](<02.4.md>) 14 | * 下一节: [Go调用汇编和C](<03.1.md>) -------------------------------------------------------------------------------- /zh/03.1.md: -------------------------------------------------------------------------------- 1 | # 3.1 Go调用汇编和C 2 | 只要不使用C的标准库函数,Go中是可以直接调用C和汇编语言的。其实道理很简单,Go的运行时库就是用C和汇编实现的,Go必须是能够调用到它们的。当然,会有一些额外的约束,这就是函数调用协议。 3 | 4 | ## Go中调用汇编 5 | 假设我们做一个汇编版本的加法函数。首先GOPATH的src下新建一个add目录,然后在该目录加入add.go的文件,内容如下: 6 | 7 | 8 | package add 9 | 10 | func Add(a, b uint64) uint64 { 11 | return a+b 12 | } 13 | 14 | 这个函数将两个uint64的数字相加,并返回结果。我们写一个简单的函数调用它,内容如下: 15 | 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "add" 22 | ) 23 | 24 | func main() { 25 | fmt.Println(add.Add(2, 15)) 26 | } 27 | 28 | 可以看到输出了结果为17。好的,接下来让我们删除Add函数的实现,只留下定义部分: 29 | 30 | package add 31 | 32 | func Add(a, b uint64) uint64 33 | 34 | 35 | 然后在add.go同一目录中建立一个add_amd64.s的文件(假设你使用的是64位系统),内容如下: 36 | 37 | TEXT ·Add+0(SB),$0-24 38 | MOVQ a+0(FP),BX 39 | MOVQ b+8(FP),BP 40 | ADDQ BP,BX 41 | MOVQ BX,res+16(FP) 42 | RET , 43 | 44 | 虽然汇编是相当难理解的,但我相信读懂上面这段不会有困难。前两条MOVQ指令分别将第一个参数放到寄存器BX,第二个参数放到寄存器BP,然后ADDQ指令将两者相加后,最后的MOVQ和RET指令返回结果。 45 | 46 | 现在,再次运行前面的main函数,它将使用自定义的汇编版本函数,可以看到成功的输出了结果17。从这个例子中可以看出Go是可以直接调用汇编实现的函数的。大多时候不必要你去写汇编,即使是研究Go的内部实现,能读懂汇编已经很足够了。 47 | 48 | 也许你真的觉得在Go中写汇编很酷,但是不要忽视了这些忠告: 49 | 50 | - 汇编很难编写,特别是很难写好。通常编译器会比你写出更快的代码。 51 | - 汇编仅能运行在一个平台上。在这个例子中,代码仅能运行在 amd64 上。这个问题有一个解决方案是给 Go 对于 x86 和 不同版本的代码分别写一套代码,文件名相应的以_386.s和_arm.s结尾。 52 | - 汇编让你和底层绑定在一起,而标准的 Go 不会。例如,slice 的长度当前是 32 位整数。但是也不是不可能为长整型。当发生这些变化时,这些代码就被破坏了。 53 | 54 | 当前Go编译器不能将汇编编译为函数的内联,但是对于小的Go函数是可以的。因此使用汇编可能意味着让你的程序更慢。 55 | 56 | 有时需要汇编给你带来一些力量(不论是性能方面的原因,还是一些相当特殊的关于CPU的操作)。对于什么时候应该使用它,Go源码包括了若干相当好的例子(可以看看 crypto 和 math)。由于它非常容易实践,所以这绝对是个学习汇编的好途径。 57 | 58 | ## Go中调用C 59 | 60 | 接下来,我们继续尝试在Go中调用C,跟调用汇编的过程很类似。首先删掉前面的add_amd64.s文件,并确保add.go文件中只是给出了Add函数的声明部分: 61 | 62 | 63 | package add 64 | 65 | func Add(a, b uint64) uint64 66 | 67 | 68 | 然后在add.go同目录中,新建一个add.c文件,内容如下: 69 | 70 | #include "runtime.h" 71 | 72 | void ·Add(uint64 a, uint64 b, uint64 ret) { 73 | ret = a + b; 74 | FLUSH(&ret); 75 | } 76 | 77 | 编译该包,运行前面的测试函数: 78 | 79 | go install add 80 | 81 | 会发现输出结果为17,说明Go中成功地调用到了C写的函数。 82 | 83 | 要注意的是不管是C或是汇编实现的函数,其函数名都是以·开头的。还有,C文件中需要包含runtime.h头文件。这个原因在该文件中有说明: 84 | Go用了特殊寄存器来存放像全局的struct G和struct M。包含这个头文件可以让所有链接到Go的C文件都知道这一点,这样编译器可以避免使用这些特定的寄存器作其它用途。 85 | 86 | 让我们仔细看一下这个C实现的函数。可以看到函数的返回值为空,而参数多了一个,第三个参数实际上被作为了返回值使用。其中FLUSH是在pkg/runtime/runtime.h中定义为USED(x),这个定义是Go的C编译器自带的primitive,作用是抑制编译器优化掉对*x的赋值的。如果你很好奇USED是怎样定义的,可以去$GOROOT/include/libc.h文件里去找找。 87 | 88 | 被调函数中对参数ret的修改居然返回到了调用函数,这个看起来似乎不可理解,不过早期的C编译器确实是可以这么做的。 89 | 90 | ## 函数调用时的内存布局 91 | Go中使用的C编译器其实是plan9的C编译器,和我们平时理解的gcc等会有一些区别。我们将上面的add.c汇编一下: 92 | 93 | go tool 6c -I $GOROOT/src/pkg/runtime -S add.c 94 | 95 | 生成的汇编代码大概是这个样子的: 96 | 97 | "".Add t=1 size=16 value=0 args=0x18 locals=0 98 | 000000 00000 (add.c:3) TEXT "".Add+0(SB),4,$0-24 99 | 000000 00000 (add.c:3) NOP , 100 | 000000 00000 (add.c:3) NOP , 101 | 000000 00000 (add.c:3) FUNCDATA $2,gcargs.0<>+0(SB) 102 | 000000 00000 (add.c:3) FUNCDATA $3,gclocals.1<>+0(SB) 103 | 000000 00000 (add.c:4) MOVQ a+8(FP),AX 104 | 0x0005 00005 (add.c:4) ADDQ b+16(FP),AX 105 | 0x000a 00010 (add.c:4) MOVQ AX,c+24(FP) 106 | 0x000f 00015 (add.c:5) RET , 107 | 000000 48 8b 44 24 08 48 03 44 24 10 48 89 44 24 18 c3 H.D$.H.D$.H.D$.. 108 | 109 | 110 | 这是Go使用的汇编代码,是一种类似plan9的汇编代码。类似a+8(FP)这种表示的含义是“变量名+偏移(寄存器)”。其中FP是帧寄存器,它是一个伪寄存器,实际上是内存位置的一个引用,其实就是BP(栈基址寄存器)上移一个机器字长位置的内存地址。 111 | 112 | 函数调用之前,a+8(FP),b+16(FP)分别表示参数a和b,而参数3的位置被空着,在被调函数中,这个位置将用于存放返回值。此时的其内存布局如下所示: 113 | 114 | 参数3 115 | 参数2 116 | 参数1 <-SP 117 | 118 | 进入被调函数之后,内存布局如下所示: 119 | 120 | 参数3 121 | 参数2 122 | 参数1 <-FP 123 | 保存PC <-SP 124 | ... 125 | ... 126 | 127 | CALL指令会使得SP下移,SP位置的内存用于保存返回地址。帧寄存器FP此时位置在SP上面。在plan9汇编中,进入函数之后的前几条指令并没有出现`push ebp; mov esp ebp`这种模式。plan9函数调用协议中采用的是caller-save的模式,也就是由调用者负责保存寄存器。注意这和传统的C是不同的。传统C中是callee-save的模式,被调函数要负责保存它想使用的寄存器,在函数退出时恢复这些寄存器。 128 | 129 | 需要注意的是参数和返回值都是有对齐的。这里是按Structrnd对齐的,Structrnd在源代码中义为sizeof(uintptr)。 130 | 131 | ## links 132 | * [目录]() 133 | * 上一节: [函数调用协议](<03.0.md>) 134 | * 下一节: [多值返回](<03.2.md>) -------------------------------------------------------------------------------- /zh/03.2.md: -------------------------------------------------------------------------------- 1 | # 3.2 多值返回 2 | 3 | Go语言是支持多值返回的。怎么实现的呢?让我们先看一看C语言是如果返回多个值的。在C中如果想返回多个值,通常会在调用函数中分配返回值的空间,并将返回值的指针传给被调函数。 4 | 5 | ```c 6 | int ret1, ret2; 7 | f(a, b, &ret1, &ret2) 8 | ``` 9 | 10 | 被调函数被定义为下面形式,在函数中会修改ret1和ret2。对指针参数所指向的内容的修改会被返回到调用函数,用这种方式实现多值返回。 11 | 12 | ```c 13 | void f(int arg1, int arg2, int *ret1, int *ret2); 14 | ``` 15 | 16 | 所以,从表面上看Go的多值返回只不过像是这种实现方式的一个语法糖衣。其实简单的这么理解也没什么影响,但实际上Go不是这么干的,Go和我们常用的C编译器的函数调用协议是不同的。 17 | 18 | 假设我们定义一个Go函数如下: 19 | 20 | ```go 21 | func f(arg1, arg2 int) (ret1, ret2 int) 22 | ``` 23 | 24 | Go的做法是在传入的参数之上留了两个空位,被调者直接将返回值放在这两空位,函数f调用前其内存布局是这样的: 25 | 26 | 为ret2保留空位 27 | 为ret1保留空位 28 | 参数3 29 | 参数2 30 | 参数1 <-SP 31 | 32 | 调用之后变为: 33 | 34 | 为ret2保留空位 35 | 为ret1保留空位 36 | 参数2 37 | 参数1 <-FP 38 | 保存PC <-SP 39 | f的栈 40 | ... 41 | 42 | ![](images/3.2.funcall.jpg?raw=true) 43 | 44 | Go的C编译器按是plan9的C编译器实现的,在被调函数中对参数值的修改是会返回到调用函数中的。在函数体中设置ret1和ret2的值,实际上会被编译成这样: 45 | 46 | MOVQ BX,ret1+16(FP) 47 | ... 48 | MOVQ BX,ret2+24(FP) 49 | 50 | 51 | 对ret1+16(FP)的赋值其实是修改的调用函数的栈中的内容,这样就会将结果返回给调用函数了。这就是Go和C函数调用协议中很重要的一个区别:为了实现多值返回,Go是使用栈空间来返回值的。而常见的C语言是通过寄存器来返回值的。 52 | 53 | ## links 54 | * [目录]() 55 | * 上一节: [Go调用汇编和C](<03.1.md>) 56 | * 下一节: [go关键字](<03.3.md>) 57 | -------------------------------------------------------------------------------- /zh/03.3.md: -------------------------------------------------------------------------------- 1 | # 3.2 go关键字 2 | 3 | 在Go语言中,表达式go f(x, y, z)会启动一个新的goroutine运行函数f(x, y, z)。函数f,变量x、y、z的值是在原goroutine计算的,只有函数f的执行是在新的goroutine中的。显然,新的goroutine不能和当前go线程用同一个栈,否则会相互覆盖。所以对go关键字的调用协议与普通函数调用是不同的。 4 | 5 | 首先,让我们看一下如果是C代码新建一条线程的实现会是什么样子的。大概会先建一个结构体,结构体里存f、x、y和z的值。然后写一个help函数,将这个结构体指针作为输入,函数体内调用f(x, y, z)。接下来,先填充结构体,然后调用newThread(help, structptr)。其中help是刚刚那个函数,它会调用f(x, y, z)。help函数将作为所有新建线程的入口函数。 6 | 7 | 这样做有什么问题么?没什么问题...只是这样实现代价有点高,每次调用都会花上不少的指令。其实Go语言中对go关键字的实现会更加hack一些,避免了这么做。 8 | 9 | 先看看正常的函数调用,下面是调用f(1, 2, 3)时的汇编代码: 10 | 11 | ```asm 12 | MOVL $1, 0(SP) 13 | MOVL $2, 4(SP) 14 | MOVL $3, 8(SP) 15 | CALL f(SB) 16 | ``` 17 | 18 | 首先将参数1、2、3进栈,然后调用函数f。 19 | 20 | 下面是go f(1, 2, 3)生成的代码: 21 | 22 | ```asm 23 | MOVL $1, 0(SP) 24 | MOVL $2, 4(SP) 25 | MOVL $3, 8(SP) 26 | PUSHQ $f(SB) 27 | PUSHQ $12 28 | CALL runtime.newproc(SB) 29 | POPQ AX 30 | POPQ AX 31 | ``` 32 | 33 | 对比一个会发现,前面部分跟普通函数调用是一样的,将参数存储在正常的位置,并没有新建一个辅助的结构体。接下来的两条指令有些不同,将f和12作为参数进栈而不直接调用f,然后调用函数`runtime.newproc`。 34 | 35 | 12是参数占用的大小。`runtime.newproc`函数接受的参数分别是:参数大小,新的goroutine是要运行的函数,函数的n个参数。 36 | 37 | 在`runtime.newproc`中,会新建一个栈空间,将栈参数的12个字节拷贝到新栈空间中并让栈指针指向参数。这时的线程状态有点像当被调度器剥夺CPU后一样,寄存器PC、SP会被保存到类似于进程控制块的一个结构体struct G内。f被存放在了struct G的entry域,后面进行调度器恢复goroutine的运行,新线程将从f开始执行。 38 | 39 | 和前面说的如果用C实现的差别就在于,没有使用辅助的结构体,而`runtime.newproc`实际上就是help函数。在函数协议上,go表达式调用就比普通的函数调用多四条指令而已,并且在实际上并没有为go关键字设计一套特殊的东西。不得不说这个做法真的非常精妙! 40 | 41 | 总结一个,go关键字的实现仅仅是一个语法糖衣而已,也就是: 42 | ```go 43 | go f(args) 44 | ``` 45 | 可以看作 46 | 47 | ```c 48 | runtime.newproc(size, f, args) 49 | ``` 50 | 51 | ## links 52 | * [目录]() 53 | * 上一节: [多值返回](<03.2.md>) 54 | * 下一节: [defer关键字](<03.4.md>) 55 | -------------------------------------------------------------------------------- /zh/03.4.md: -------------------------------------------------------------------------------- 1 | # 3.4 defer关键字 2 | 3 | defer和go一样都是Go语言提供的关键字。defer用于资源的释放,会在函数返回之前进行调用。一般采用如下模式: 4 | 5 | f,err := os.Open(filename) 6 | if err != nil { 7 | panic(err) 8 | } 9 | defer f.Close() 10 | 11 | 如果有多个defer表达式,调用顺序类似于栈,越后面的defer表达式越先被调用。 12 | 13 | 不过如果对defer的了解不够深入,使用起来可能会踩到一些坑,尤其是跟带命名的返回参数一起使用时。在讲解defer的实现之前先看一看使用defer容易遇到的问题。 14 | 15 | ## defer使用时的坑 16 | 先来看看几个例子。例1: 17 | 18 | 19 | func f() (result int) { 20 | defer func() { 21 | result++ 22 | }() 23 | return 0 24 | } 25 | 26 | 例2: 27 | 28 | func f() (r int) { 29 | t := 5 30 | defer func() { 31 | t = t + 5 32 | }() 33 | return t 34 | } 35 | 36 | 例3: 37 | 38 | func f() (r int) { 39 | defer func(r int) { 40 | r = r + 5 41 | }(r) 42 | return 1 43 | } 44 | 45 | 请读者先不要运行代码,在心里跑一遍结果,然后去验证。 46 | 47 | 例1的正确答案不是0,例2的正确答案不是10,如果例3的正确答案不是6...... 48 | 49 | defer是在return之前执行的。这个在 [官方文档](http://golang.org/ref/spec#defer_statements)中是明确说明了的。要使用defer时不踩坑,最重要的一点就是要明白,**return xxx这一条语句并不是一条原子指令!** 50 | 51 | 函数返回的过程是这样的:先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中。 52 | 53 | defer表达式可能会在设置函数返回值之后,在返回到调用函数之前,修改返回值,使最终的函数返回值与你想象的不一致。 54 | 55 | 其实使用defer时,用一个简单的转换规则改写一下,就不会迷糊了。改写规则是将return语句拆成两句写,return xxx会被改写成: 56 | 57 | 返回值 = xxx 58 | 调用defer函数 59 | 空的return 60 | 61 | 先看例1,它可以改写成这样: 62 | 63 | func f() (result int) { 64 | result = 0 //return语句不是一条原子调用,return xxx其实是赋值+ret指令 65 | func() { //defer被插入到return之前执行,也就是赋返回值和ret指令之间 66 | result++ 67 | }() 68 | return 69 | } 70 | 71 | 所以这个返回值是1。 72 | 73 | 再看例2,它可以改写成这样: 74 | 75 | func f() (r int) { 76 | t := 5 77 | r = t //赋值指令 78 | func() { //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过 79 | t = t + 5 80 | } 81 | return //空的return指令 82 | } 83 | 84 | 所以这个的结果是5。 85 | 86 | 最后看例3,它改写后变成: 87 | 88 | func f() (r int) { 89 | r = 1 //给返回值赋值 90 | func(r int) { //这里改的r是传值传进去的r,不会改变要返回的那个r值 91 | r = r + 5 92 | }(r) 93 | return //空的return 94 | } 95 | 96 | 所以这个例子的结果是1。 97 | 98 | defer确实是在return之前调用的。但表现形式上却可能不像。本质原因是return xxx语句并不是一条原子指令,defer被插入到了赋值 与 ret之间,因此可能有机会改变最终的返回值。 99 | 100 | ## defer的实现 101 | 102 | defer关键字的实现跟go关键字很类似,不同的是它调用的是runtime.deferproc而不是runtime.newproc。 103 | 104 | 在defer出现的地方,插入了指令call runtime.deferproc,然后在函数返回之前的地方,插入指令call runtime.deferreturn。 105 | 106 | 普通的函数返回时,汇编代码类似: 107 | 108 | add xx SP 109 | return 110 | 111 | 如果其中包含了defer语句,则汇编代码是: 112 | 113 | call runtime.deferreturn, 114 | add xx SP 115 | return 116 | 117 | goroutine的控制结构中,有一张表记录defer,调用runtime.deferproc时会将需要defer的表达式记录在表中,而在调用runtime.deferreturn的时候,则会依次从defer表中出栈并执行。 118 | 119 | ## links 120 | * [目录]() 121 | * 上一节: [go关键字](<03.3.md>) 122 | * 下一节: [连续栈](<03.5.md>) 123 | -------------------------------------------------------------------------------- /zh/03.5.md: -------------------------------------------------------------------------------- 1 | # 3.5 连续栈 2 | 3 | Go语言支持goroutine,每个goroutine需要能够运行,所以它们都有自己的栈。假如每个goroutine分配固定栈大小并且不能增长,太小则会导致溢出,太大又会浪费空间,无法存在许多的goroutine。 4 | 5 | 为了解决这个问题,goroutine可以初始时只给栈分配很小的空间,然后随着使用过程中的需要自动地增长。这就是为什么Go可以开千千万万个goroutine而不会耗尽内存。 6 | 7 | Go1.3版本之后则使用的是continuous stack,下面将具体分析一下这种技术。 8 | 9 | ## 基本原理 10 | 11 | 每次执行函数调用时Go的runtime都会进行检测,若当前栈的大小不够用,则会触发“中断”,从当前函数进入到Go的运行时库,Go的运行时库会保存此时的函数上下文环境,然后分配一个新的足够大的栈空间,将旧栈的内容拷贝到新栈中,并做一些设置,使得当函数恢复运行时,函数会在新分配的栈中继续执行,仿佛整个过程都没发生过一样,这个函数会觉得自己使用的是一块大小“无限”的栈空间。 12 | 13 | ## 实现过程 14 | 15 | 在研究Go的实现细节之前让我们先自己思考一下应该如何实现。第一步肯定要有某种机制检测到当前栈大小不够用了,这个应该是把当前的栈寄存器SP跟栈的可用栈空间的边界进行比较。能够检测到栈大小不够用,就相当于捕捉到了“中断”。 16 | 17 | 捕获完“中断”,第二步要做的,就应该是进入运行时,保存当前goroutine的上下文。别陷入如何保存上下文的细节,先假如我们把函数栈增长时的上下文保存好了,那下一步就是分配新的栈空间了,我们可以将分配空间想象成就是调用一下malloc而已。 18 | 19 | 接下来怎么办呢?我们要将旧栈中的内容拷贝到新栈中,然后让函数继续在新栈中运行。这里先暂时忽略旧栈内容拷贝到新栈中的一些技术难点,假设在新栈空间中恢复了“中断”时的上下文,从运行时返回到函数。 20 | 21 | 函数在新的栈中继续运行了,但是还有个问题:函数如何返回。因为函数返回后栈是要缩小的,否则就会内存浪费空间了,所以还需要在函数返回时处理栈缩小的问题。 22 | 23 | ## 具体细节 24 | 25 | ### 如何捕获到函数的栈空间不足 26 | 27 | Go语言和C不同,不是使用栈指针寄存器和栈基址寄存器确定函数的栈的。在Go的运行时库中,每个goroutine对应一个结构体G,大致相当于进程控制块的概念。这个结构体中存了stackbase和stackguard,用于确定这个goroutine使用的栈空间信息。每个Go函数调用的前几条指令,先比较栈指针寄存器跟g->stackguard,检测是否发生栈溢出。如果栈指针寄存器值超越了stackguard就需要扩展栈空间。 28 | 29 | 为了加深理解,下面让我们跟踪一下代码,并看看实际生成的汇编吧。首先写一个test.go文件,内容如下: 30 | 31 | package main 32 | func main() { 33 | main() 34 | } 35 | 36 | 然后生成汇编文件: 37 | 38 | go tool 6g -S test.go | head -8 39 | 40 | 可以看以输出是: 41 | 42 | 000000 00000 (test.go:3) TEXT "".main+0(SB),$0-0 43 | 000000 00000 (test.go:3) MOVQ (TLS),CX 44 | 0x0009 00009 (test.go:3) CMPQ SP,(CX) 45 | 0x000c 00012 (test.go:3) JHI ,21 46 | 0x000e 00014 (test.go:3) CALL ,runtime.morestack00_noctxt(SB) 47 | 0x0013 00019 (test.go:3) JMP ,0 48 | 0x0015 00021 (test.go:3) NOP , 49 | 50 | 让我们好好看一下这些指令。(TLS)取到的是结构体G的第一个域,也就是g->stackguard地址,将它赋值给CX。然后CX地址的值与SP进行比较,如果SP大于g->stackguard了,则会调用runtime.morestack函数。这几条指令的作用就是检测栈是否溢出。 51 | 52 | 不过并不是所有函数在链接时都会插入这种指令。如果你读源代码,可能会发现`#pragma textflag 7`,或者在汇编函数中看到`TEXT reuntime.exit(SB),7,$0`,这种函数就是不会检测栈溢出的。这个是编译标记,控制是否生成栈溢出检测指令。 53 | 54 | runtime.morestack是用汇编实现的,做的事情大致是将一些信息存在M结构体中,这些信息包括当前栈桢,参数,当前函数调用,函数返回地址(两个返回地址,一个是runtime.morestack的函数地址,一个是f的返回地址)。通过这些信息可以把新栈和旧栈链起来。 55 | 56 | 57 | void runtime.morestack() { 58 | if(g == g0) { 59 | panic(); 60 | } else { 61 | m->morebuf.gobuf_pc = getCallerCallerPC(); 62 | void *SP = getCallerSP(); 63 | m->morebuf.gobuf_sp = SP; 64 | m->moreargp = SP; 65 | m->morebuf.gobuf_g = g; 66 | m->morepc = getCallerPC(); 67 | 68 | void *g0 = m->g0; 69 | g = g0; 70 | setSP(g0->g_sched.gobuf_sp); 71 | runtime.newstack(); 72 | } 73 | } 74 | 75 | 需要注意的就是newstack是切换到m->g0的栈中去调用的。m->g0是调度器栈,go的运行时库的调度器使用的都是m->g0。 76 | 77 | ### 旧栈数据复制到新栈 78 | 79 | runtime.morestack会调用于runtime.newstack,newstack做的事情很好理解:分配一个足够大的新的空间,将旧的栈中的数据复制到新的栈中,进行适当的修饰,伪装成调用过runtime.lessstack的样子(这样当函数返回时就会调用runtime.lessstack再次进入runtime中做一些栈收缩的处理)。 80 | 81 | 这里有一个技术难点:旧栈数据复制到新栈的过程,要考虑指针失效问题。 82 | 83 | 比如有某个指针,引用了旧栈中的地址,如果仅仅是将旧栈内容搬到新栈中,那么该指针就失效了,因为旧栈已被释放,应该修改这个指针让它指向新栈的对应地址。考虑如下代码: 84 | 85 | func f1() { 86 | var a A 87 | f(&a) 88 | } 89 | func f2(a *A) { 90 | // modify a 91 | } 92 | 93 | 如果在f2中发生了栈增长,此时分配更大的空间作为新栈,并将旧栈内容拷贝到新栈中,仅仅这样是不够的,因为f2中的a还是指向旧栈中的f1的,所以必须调整。 94 | 95 | Go实现了精确的垃圾回收,运行时知道每一块内存对应的对象的类型信息。在复制之后,会进行指针的调整。具体做法是,对当前栈帧之前的每一个栈帧,对其中的每一个指针,检测指针指向的地址,如果指向地址是落在旧栈范围内的,则将它加上一个偏移使它指向新栈的相应地址。这个偏移值等于新栈基地址减旧栈基地址。 96 | 97 | runtime.lessstack比较简单,它其实就是切换到m->g0栈之后调用runtime.oldstack函数。这时之前保存的那个Stktop结构体是时候发挥作用了,从上面可以找到旧栈空间的SP和PC等信息,通过runtime.gogo跳转过去,整个过程就完成了。 98 | 99 | gp = m->curg; //当前g 100 | top = (Stktop*)gp->stackbase; //取得Stktop结构体 101 | label = top->gobuf; //从结构体中取出Gobuf 102 | runtime·gogo(&label, cret); //通过Gobuf恢复上下文 103 | 104 | 105 | ## 小结 106 | 107 | 1. 使用分段栈的函数头几个指令检测SP和stackguard,调用runtime.morestack 108 | 2. runtime.morestack函数的主要功能是保存当前的栈的一些信息,然后转换成调度器的栈调用runtime.newstack 109 | 3. runtime.newstack函数的主要功能是分配空间,装饰此空间,将旧的frame和arg弄到新空间 110 | 4. 使用gogocall的方式切换到新分配的栈,gogocall使用的JMP返回到被中断的函数 111 | 5. 继续执行遇到RET指令时会返回到runtime.lessstack,lessstack做的事情跟morestack相反,它要准备好从new stack到old stack 112 | 113 | 整个过程有点像一次中断,中断处理时保存当时的现场,弄个新的栈,中断恢复时恢复到新栈中运行。栈的收缩是垃圾回收的过程中实现的.当检测到栈只使用了不到1/4时,栈缩小为原来的1/2. 114 | 115 | ## links 116 | * [目录]() 117 | * 上一节: [defer关键字](<03.4.md>) 118 | * 下一节: [闭包的实现](<03.6.md>) -------------------------------------------------------------------------------- /zh/03.6.md: -------------------------------------------------------------------------------- 1 | # 3.6 闭包的实现 2 | 3 | 闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。 4 | 5 | ## Go中的闭包 6 | 7 | 闭包是函数式语言中的概念,没有研究过函数式语言的用户可能很难理解闭包的强大,相关的概念超出了本书的范围。Go语言是支持闭包的,这里只是简单地讲一下在Go语言中闭包是如何实现的。 8 | 9 | func f(i int) func() int { 10 | return func() int { 11 | i++ 12 | return i 13 | } 14 | } 15 | 16 | 函数f返回了一个函数,返回的这个函数,返回的这个函数就是一个闭包。这个函数中本身是没有定义变量i的,而是引用了它所在的环境(函数f)中的变量i。 17 | 18 | c1 := f(0) 19 | c2 := f(0) 20 | c1() // reference to i, i = 0, return 1 21 | c2() // reference to another i, i = 0, return 1 22 | 23 | c1跟c2引用的是不同的环境,在调用i++时修改的不是同一个i,因此两次的输出都是1。函数f每进入一次,就形成了一个新的环境,对应的闭包中,函数都是同一个函数,环境却是引用不同的环境。 24 | 25 | 变量i是函数f中的局部变量,假设这个变量是在函数f的栈中分配的,是不可以的。因为函数f返回以后,对应的栈就失效了,f返回的那个函数中变量i就引用一个失效的位置了。所以闭包的环境中引用的变量不能够在栈上分配。 26 | 27 | ## escape analyze 28 | 29 | 在继续研究闭包的实现之前,先看一看Go的一个语言特性: 30 | 31 | func f() *Cursor { 32 | var c Cursor 33 | c.X = 500 34 | noinline() 35 | return &c 36 | } 37 | 38 | Cursor是一个结构体,这种写法在C语言中是不允许的,因为变量c是在栈上分配的,当函数f返回后c的空间就失效了。但是,在Go语言规范中有说明,这种写法在Go语言中合法的。语言会自动地识别出这种情况并在堆上分配c的内存,而不是函数f的栈上。 39 | 40 | 为了验证这一点,可以观察函数f生成的汇编代码: 41 | 42 | MOVQ $type."".Cursor+0(SB),(SP) // 取变量c的类型,也就是Cursor 43 | PCDATA $0,$16 44 | PCDATA $1,$0 45 | CALL ,runtime.new(SB) // 调用new函数,相当于new(Cursor) 46 | PCDATA $0,$-1 47 | MOVQ 8(SP),AX // 取c.X的地址放到AX寄存器 48 | MOVQ $500,(AX) // 将AX存放的内存地址的值赋为500 49 | MOVQ AX,"".~r0+24(FP) 50 | ADDQ $16,SP 51 | 52 | 识别出变量需要在堆上分配,是由编译器的一种叫escape analyze的技术实现的。如果输入命令: 53 | 54 | go build --gcflags=-m main.go 55 | 56 | 可以看到输出: 57 | 58 | ./main.go:20: moved to heap: c 59 | ./main.go:23: &c escapes to heap 60 | 61 | 表示c逃逸了,被移到堆中。escape analyze可以分析出变量的作用范围,这是对垃圾回收很重要的一项技术。 62 | 63 | ## 闭包结构体 64 | 65 | 回到闭包的实现来,前面说过,闭包是函数和它所引用的环境。那么是不是可以表示为一个结构体呢: 66 | 67 | type Closure struct { 68 | F func()() 69 | i *int 70 | } 71 | 72 | 事实上,Go在底层确实就是这样表示一个闭包的。让我们看一下汇编代码: 73 | 74 | func f(i int) func() int { 75 | return func() int { 76 | i++ 77 | return i 78 | } 79 | } 80 | 81 | 82 | MOVQ $type.int+0(SB),(SP) 83 | PCDATA $0,$16 84 | PCDATA $1,$0 85 | CALL ,runtime.new(SB) // 是不是很熟悉,这一段就是i = new(int) 86 | ... 87 | MOVQ $type.struct { F uintptr; A0 *int }+0(SB),(SP) // 这个结构体就是闭包的类型 88 | ... 89 | CALL ,runtime.new(SB) // 接下来相当于 new(Closure) 90 | PCDATA $0,$-1 91 | MOVQ 8(SP),AX 92 | NOP , 93 | MOVQ $"".func·001+0(SB),BP 94 | MOVQ BP,(AX) // 函数地址赋值给Closure的F部分 95 | NOP , 96 | MOVQ "".&i+16(SP),BP // 将堆中new的变量i的地址赋值给Closure的值部分 97 | MOVQ BP,8(AX) 98 | MOVQ AX,"".~r1+40(FP) 99 | ADDQ $24,SP 100 | RET , 101 | 102 | 其中func·001是另一个函数的函数地址,也就是f返回的那个函数。 103 | 104 | ## 小结 105 | 106 | 1. Go语言支持闭包 107 | 2. Go语言能通过escape analyze识别出变量的作用域,自动将变量在堆上分配。将闭包环境变量在堆上分配是Go实现闭包的基础。 108 | 3. 返回闭包时并不是单纯返回一个函数,而是返回了一个结构体,记录下函数返回地址和引用的环境中的变量地址。 109 | 110 | ## links 111 | * [目录]() 112 | * 上一节: [连续栈](<03.5.md>) 113 | * 下一节: [Go语言程序初始化过程](<04.0.md>) 114 | -------------------------------------------------------------------------------- /zh/04.0.md: -------------------------------------------------------------------------------- 1 | # 4 Go语言程序初始化过程 2 | 3 | 作为下一章goroutine调度的一个前序,本章先讲一些基础内容,看一看Go语言编写的程序的初始化过程。其实初始化过程中会做很多很多的事情,这里忽略大部分细节,只看一下脉络。从程序入口开始分析也是学习源代码的一个好方式。 4 | 5 | 首先,写一个hello world文件,内容如下: 6 | ```go 7 | package main 8 | import "fmt" 9 | func main() { 10 | fmt.Println("hello world!") 11 | } 12 | ``` 13 | 14 | 编译,使用gdb调试。给下列函数下断点: 15 | 16 | _rt0_amd64_darwin 17 | main 18 | _rt0_amd64 19 | runtime.check 20 | runtime.args 21 | runtime.osinit 22 | runtime.hashinit 23 | runtime.schedinit 24 | runtime.newproc 25 | runtime.mstart 26 | main.main 27 | runtime.exit 28 | 29 | 你可能需要根据自己的系统将_rt0_amd64_darwin改成_rt0_amd64_linux或者别的。在gdb中先点r,回车,然后点c,回车,接着一路回车。 30 | 31 | 别着急,只是让你有一个直观的感受一下Go程序从系统初始化直到退出必经的流程。下面让我们正式开始吧! 32 | 33 | ## links 34 | * [目录]() 35 | * 上一节: [闭包的实现](<03.6.md>) 36 | * 下一节: [系统初始化](<04.1.md>) 37 | -------------------------------------------------------------------------------- /zh/04.1.md: -------------------------------------------------------------------------------- 1 | # 4.1 系统初始化 2 | 3 | 整个程序启动是从_rt0_amd64_darwin开始的,然后JMP到main,接着到_rt0_amd64。前面只有一点点汇编代码,做的事情就是通过参数argc和argv等,确定栈的位置,得到寄存器。下面将从_rt0_amd64开始分析。 4 | 5 | 这里首先会设置好m->g0的栈,将当前的SP设置为stackbase,将SP往下大约64K的地方设置为stackguard。然后会获取处理器信息,放在全局变量runtime·cpuid_ecx和runtime·cpuid_edx中。接着,设置本地线程存储。本地线程存储是依赖于平台实现的,比如说这台机器上是调用操作系统函数thread_fast_set_cthread_self。设置本地线程存储之后还会立即测试一下,写入一个值再读出来看是否正常。 6 | 7 | ## 本地线程存储 8 | 这里解释一下本地线程存储。比如说每个goroutine都有自己的控制信息,这些信息是存放在一个结构体G中。假设我们有一个全局变量g是结构体G的指针,我们希望只有唯一的全局变量g,而不是g0,g1,g2...但是我们又希望不同goroutine去访问这个全局变量g得到的并不是同一个东西,它们得到的是相对自己线程的结构体G,这种情况下就需要本地线程存储。g确实是一个全局变量,却在不同线程有多份不同的副本。每个goroutine去访问g时,都是对应到自己线程的这一份副本。 9 | 10 | 设置好本地线程存储之后,就可以为每个goroutine和machine设置寄存器了。这样设置好了之后,每次调用get_tls(r),就会将当前的goroutine的g的地址放到寄存器r中。你可以在源代码中看到一些类似这样的汇编: 11 | 12 | get_tls(CX) 13 | MOVQ g(CX), AX //get_tls(CX)之后,g(CX)得到的就是当前的goroutine的g 14 | 15 | 不同的goroutine调用`get_tls`,得到的g是本地的结构体G的,结构体中记录goroutine的相关信息。 16 | 17 | ## 初始化顺序 18 | 接下来的事情就非常直白,可以直接上代码: 19 | 20 | ```asm 21 | CLD // convention is D is always left cleared 22 | CALL runtime·check(SB) //检测像int8,int16,float等是否是预期的大小,检测cas操作是否正常 23 | MOVL 16(SP), AX // copy argc 24 | MOVL AX, 0(SP) 25 | MOVQ 24(SP), AX // copy argv 26 | MOVQ AX, 8(SP) 27 | CALL runtime·args(SB) //将argc,argv设置到static全局变量中了 28 | CALL runtime·osinit(SB) //osinit做的事情就是设置runtime.ncpu,不同平台实现方式不一样 29 | CALL runtime·hashinit(SB) //使用读/dev/urandom的方式从内核获得随机数种子 30 | CALL runtime·schedinit(SB) //内存管理初始化,根据GOMAXPROCS设置使用的procs等等 31 | ``` 32 | 33 | proc.c中有一段注释,也说明了bootstrap的顺序: 34 | 35 | // The bootstrap sequence is: 36 | // 37 | // call osinit 38 | // call schedinit 39 | // make & queue new G 40 | // call runtime·mstart 41 | // 42 | // The new G calls runtime·main. 43 | 44 | 先调用osinit,再调用schedinit,创建就绪队列并新建一个G,接着就是mstart。这几个函数都不太复杂。 45 | 46 | ## 调度器初始化 47 | 让我们看一下runtime.schedinit函数。该函数其实是包装了一下其它模块的初始化函数。有调用mallocinit,mcommoninit分别对内存管理模块初始化,对当前的结构体M初始化。 48 | 49 | 接着调用runtime.goargs和runtime.goenvs,将程序的main函数参数argc和argv等复制到了os.Args中。 50 | 51 | 也是在这个函数中,根据环境变量GOMAXPROCS决定可用物理线程数目的: 52 | 53 | ```c 54 | procs = 1; 55 | p = runtime·getenv("GOMAXPROCS"); 56 | if(p != nil && (n = runtime·atoi(p)) > 0) { 57 | if(n > MaxGomaxprocs) 58 | n = MaxGomaxprocs; 59 | procs = n; 60 | } 61 | ``` 62 | 63 | 回到前面的汇编代码继续看: 64 | 65 | ```asm 66 | // 新建一个G,当它运行时会调用main.main 67 | PUSHQ $runtime·main·f(SB) // entry 68 | PUSHQ $0 // arg size 69 | CALL runtime·newproc(SB) 70 | POPQ AX 71 | POPQ AX 72 | 73 | // start this M 74 | CALL runtime·mstart(SB) 75 | ``` 76 | 77 | 还记得前面章节讲的go关键字的调用协议么?先将参数进栈,再被调函数指针和参数字节数进栈,接着调用runtime.newproc函数。所以这里其实就是新开个goroutine执行runtime.main。 78 | 79 | runtime.newproc会把runtime.main放到就绪线程队列里面。本线程继续执行runtime.mstart,m意思是machine。runtime.mstart会调用到调度函数schedule 80 | 81 | schedule函数绝不返回,它会根据当前线程队列中线程状态挑选一个来运行。由于当前只有这一个goroutine,它会被调度,然后就到了runtime.main函数中来,runtime.main会调用用户的main函数,即main.main从此进入用户代码。前面已经写过helloworld了,用gdb调试,一步一步的跟踪观察这个过程。 82 | 83 | ## links 84 | * [目录]() 85 | * 上一节: [Go语言程序初始化过程](<04.0.md>) 86 | * 下一节: [main.main之前的准备](<04.2.md>) 87 | -------------------------------------------------------------------------------- /zh/04.2.md: -------------------------------------------------------------------------------- 1 | # 4.2 main.main之前的准备 2 | 3 | main.main就是用户的main函数。这里是指Go的runtime在进入用户main函数之前做的一些事情。 4 | 5 | 前面已经介绍了从Go程序执行后的第一条指令,到启动runtime.main的主要流程,比如其中要设置好本地线程存储,设置好main函数参数,根据环境变量GOMAXPROCS设置好使用的procs,初始化调度器和内存管理等等。 6 | 7 | 接下来将是从runtime.main到main.main之间的一些过程。注意,main.main是在runtime.main函数里面调用的。不过在调用main.main之前,还有一些工作要做。 8 | 9 | ## sysmon 10 | 11 | 在main.main执行之前,Go语言的runtime库会初始化一些后台任务,其中一个任务就是sysmon。 12 | 13 | newm(sysmon, nil); 14 | 15 | newm新建一个结构体M,第一个参数是这个结构体M的入口函数,也就说会在一个新的物理线程中运行sysmon函数。由此可见sysmon是一个地位非常高的后台任务,整个函数体一个死循环的形式,目前主要处理两个事件:对于网络的epoll以及抢占式调度的检测。大致过程如下: 16 | 17 | ```C 18 | for(;;) { 19 | runtime.usleep(delay); 20 | if(lastpoll != 0 && lastpoll + 10*1000*1000 > now) { 21 | runtime.netpoll(); 22 | } 23 | retake(now); // 根据每个P的状态和运行时间决定是否要进行抢占 24 | } 25 | ``` 26 | 27 | sysmon会根据系统当前的繁忙程度睡一小段时间,然后每隔10ms至少进行一次epoll并唤醒相应的goroutine。同时,它还会检测是否有P长时间处于Psyscall状态或Prunning状态,并进行抢占式调度。 28 | 29 | ## scavenger 30 | scavenger是另一个后台任务,但是它的创建跟sysmon有点区别: 31 | 32 | runtime·newproc(&scavenger, nil, 0, 0, runtime·main); 33 | 34 | newproc创建一个goroutine,第一个参数是goroutine运行的函数。scavenger的地位是没有sysmon那么高的——sysmon是由物理线程运行的,而scavenger只是由goroutine运行的。接下来的章节会说明goroutine与物理线程的区别。 35 | 36 | 那么,scavenger执行什么工作?它又为什么不像sysmon那样呢?其实scavenger执行的是runtime·MHeap_Scavenger函数。它将一些不再使用的内存归还给操作系统。Go是一门`垃圾回收`的语言,垃圾回收会在系统运行过程中被触发,内存会被归还到Go的内存管理系统中,Go的内存管理是基于内存池进行重用的,而这个函数会真正地将内存归还给操作系统。 37 | 38 | scavenger显然没有sysmon要求那么高,所以它仅仅是一个普通的goroutine而不是一个线程。 39 | 40 | main.main在这些后台任务运行起来之后执行,不过在它执行之前,还有最后一个:main.init,每个包的init函数会在包使用之前先执行。 41 | 42 | ## links 43 | * [目录]() 44 | * 上一节: [系统初始化](<04.1.md>) 45 | * 下一节: [goroutine调度](<05.0.md>) 46 | -------------------------------------------------------------------------------- /zh/05.0.md: -------------------------------------------------------------------------------- 1 | # 5. goroutine调度 2 | 3 | ## links 4 | * [目录]() 5 | * 上一节: [main.main之前的准备](<04.2.md>) 6 | * 下一节: [调度器相关数据结构](<05.1.md>) 7 | -------------------------------------------------------------------------------- /zh/05.1.md: -------------------------------------------------------------------------------- 1 | # 5.1 调度器相关数据结构 2 | Go的调度的实现,涉及到几个重要的数据结构。运行时库用这几个数据结构来实现goroutine的调度,管理goroutine和物理线程的运行。这些数据结构分别是结构体G,结构体M,结构体P,以及Sched结构体。前三个的定义在文件runtime/runtime.h中,而Sched的定义在runtime/proc.c中。Go语言的调度相关实现也是在文件proc.c中。 3 | 4 | ## 结构体G 5 | G是goroutine的缩写,相当于操作系统中的进程控制块,在这里就是goroutine的控制结构,是对goroutine的抽象。其中包括goid是这个goroutine的ID,status是这个goroutine的状态,如Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead等。 6 | 7 | struct G 8 | { 9 | uintptr stackguard; // 分段栈的可用空间下界 10 | uintptr stackbase; // 分段栈的栈基址 11 | Gobuf sched; //进程切换时,利用sched域来保存上下文 12 | uintptr stack0; 13 | FuncVal* fnstart; // goroutine运行的函数 14 | void* param; // 用于传递参数,睡眠时其它goroutine设置param,唤醒时此goroutine可以获取 15 | int16 status; // 状态Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead 16 | int64 goid; // goroutine的id号 17 | G* schedlink; 18 | M* m; // for debuggers, but offset not hard-coded 19 | M* lockedm; // G被锁定只能在这个m上运行 20 | uintptr gopc; // 创建这个goroutine的go表达式的pc 21 | ... 22 | }; 23 | 24 | 25 | 结构体G中的部分域如上所示。可以看到,其中包含了栈信息stackbase和stackguard,有运行的函数信息fnstart。这些就足够成为一个可执行的单元了,只要得到CPU就可以运行。 26 | 27 | goroutine切换时,上下文信息保存在结构体的sched域中。goroutine是轻量级的`线程`或者称为`协程`,切换时并不必陷入到操作系统内核中,所以保存过程很轻量。看一下结构体G中的Gobuf,其实只保存了当前栈指针,程序计数器,以及goroutine自身。 28 | 29 | struct Gobuf 30 | { 31 | // The offsets of these fields are known to (hard-coded in) libmach. 32 | uintptr sp; 33 | byte* pc; 34 | G* g; 35 | ... 36 | }; 37 | 38 | 记录g是为了恢复当前goroutine的结构体G指针,运行时库中使用了一个常驻的寄存器`extern register G* g`,这个是当前goroutine的结构体G的指针。这样做是为了快速地访问goroutine中的信息,比如,Go的栈的实现并没有使用%ebp寄存器,不过这可以通过g->stackbase快速得到。"extern register"是由6c,8c等实现的一个特殊的存储。在ARM上它是实际的寄存器;其它平台是由段寄存器进行索引的线程本地存储的一个槽位。在linux系统中,对g和m使用的分别是0(GS)和4(GS)。需要注意的是,链接器还会根据特定操作系统改变编译器的输出,例如,6l/linux下会将0(GS)重写为-16(FS)。每个链接到Go程序的C文件都必须包含runtime.h头文件,这样C编译器知道避免使用专用的寄存器。 39 | 40 | ## 结构体M 41 | 42 | M是machine的缩写,是对机器的抽象,每个m都是对应到一条操作系统的物理线程。M必须关联了P才可以执行Go代码,但是当它处理阻塞或者系统调用中时,可以不需要关联P。 43 | 44 | struct M 45 | { 46 | G* g0; // 带有调度栈的goroutine 47 | G* gsignal; // signal-handling G 处理信号的goroutine 48 | void (*mstartfn)(void); 49 | G* curg; // M中当前运行的goroutine 50 | P* p; // 关联P以执行Go代码 (如果没有执行Go代码则P为nil) 51 | P* nextp; 52 | int32 id; 53 | int32 mallocing; //状态 54 | int32 throwing; 55 | int32 gcing; 56 | int32 locks; 57 | int32 helpgc; //不为0表示此m在做帮忙gc。helpgc等于n只是一个编号 58 | bool blockingsyscall; 59 | bool spinning; 60 | Note park; 61 | M* alllink; // 这个域用于链接allm 62 | M* schedlink; 63 | MCache *mcache; 64 | G* lockedg; 65 | M* nextwaitm; // next M waiting for lock 66 | GCStats gcstats; 67 | ... 68 | }; 69 | 70 | 这里也是截取结构体M中的部分域。和G类似,M中也有alllink域将所有的M放在allm链表中。lockedg是某些情况下,G锁定在这个M中运行而不会切换到其它M中去。M中还有一个MCache,是当前M的内存的缓存。M也和G一样有一个常驻寄存器变量,代表当前的M。同时存在多个M,表示同时存在多个物理线程。 71 | 72 | 结构体M中有两个G是需要关注一下的,一个是curg,代表结构体M当前绑定的结构体G。另一个是g0,是带有调度栈的goroutine,这是一个比较特殊的goroutine。普通的goroutine的栈是在堆上分配的可增长的栈,而g0的栈是M对应的线程的栈。所有调度相关的代码,会先切换到该goroutine的栈中再执行。 73 | 74 | ## 结构体P 75 | Go1.1中新加入的一个数据结构,它是Processor的缩写。结构体P的加入是为了提高Go程序的并发度,实现更好的调度。M代表OS线程。P代表Go代码执行时需要的资源。当M执行Go代码时,它需要关联一个P,当M为idle或者在系统调用中时,它也需要P。有刚好GOMAXPROCS个P。所有的P被组织为一个数组,在P上实现了工作流窃取的调度器。 76 | 77 | 78 | struct P 79 | { 80 | Lock; 81 | uint32 status; // Pidle或Prunning等 82 | P* link; 83 | uint32 schedtick; // 每次调度时将它加一 84 | M* m; // 链接到它关联的M (nil if idle) 85 | MCache* mcache; 86 | 87 | G* runq[256]; 88 | int32 runqhead; 89 | int32 runqtail; 90 | 91 | // Available G's (status == Gdead) 92 | G* gfree; 93 | int32 gfreecnt; 94 | byte pad[64]; 95 | }; 96 | 97 | 结构体P中也有相应的状态: 98 | 99 | Pidle, 100 | Prunning, 101 | Psyscall, 102 | Pgcstop, 103 | Pdead, 104 | 105 | 注意,跟G不同的是,P不存在`waiting`状态。MCache被移到了P中,但是在结构体M中也还保留着。在P中有一个Grunnable的goroutine队列,这是一个P的局部队列。当P执行Go代码时,它会优先从自己的这个局部队列中取,这时可以不用加锁,提高了并发度。如果发现这个队列空了,则去其它P的队列中拿一半过来,这样实现工作流窃取的调度。这种情况下是需要给调用器加锁的。 106 | 107 | ## Sched 108 | Sched是调度实现中使用的数据结构,该结构体的定义在文件proc.c中。 109 | 110 | struct Sched { 111 | Lock; 112 | 113 | uint64 goidgen; 114 | 115 | M* midle; // idle m's waiting for work 116 | int32 nmidle; // number of idle m's waiting for work 117 | int32 nmidlelocked; // number of locked m's waiting for work 118 | int3 mcount; // number of m's that have been created 119 | int32 maxmcount; // maximum number of m's allowed (or die) 120 | 121 | P* pidle; // idle P's 122 | uint32 npidle; //idle P的数量 123 | uint32 nmspinning; 124 | 125 | // Global runnable queue. 126 | G* runqhead; 127 | G* runqtail; 128 | int32 runqsize; 129 | 130 | // Global cache of dead G's. 131 | Lock gflock; 132 | G* gfree; 133 | 134 | int32 stopwait; 135 | Note stopnote; 136 | uint32 sysmonwait; 137 | Note sysmonnote; 138 | uint64 lastpoll; 139 | 140 | int32 profilehz; // cpu profiling rate 141 | } 142 | 143 | 大多数需要的信息都已放在了结构体M、G和P中,Sched结构体只是一个壳。可以看到,其中有M的idle队列,P的idle队列,以及一个全局的就绪的G队列。Sched结构体中的Lock是非常必须的,如果M或P等做一些非局部的操作,它们一般需要先锁住调度器。 144 | 145 | ## links 146 | * [目录]() 147 | * 上一节: [goroutine调度](<05.0.md>) 148 | * 下一节: [goroutine的生老病死](<05.2.md>) 149 | -------------------------------------------------------------------------------- /zh/05.2.md: -------------------------------------------------------------------------------- 1 | # 5.2 goroutine的生老病死 2 | 3 | 本小节将通过goroutine的创建,消亡,阻塞和恢复等过程,来观察Go语言的调度策略,这里就称之为生老病死吧。整个Go语言的调度系统是比较复杂的,为了避免结构体M和结构体P引入的其它干扰,这里主要将注意力集中到结构体G中,以goroutine为主线。 4 | 5 | ## goroutine的创建 6 | 前面讲函数调用协议时说过go关键字最终被弄成了runtime.newproc。这就是一个goroutine的出生,所有新的goroutine都是通过这个函数创建的。 7 | 8 | runtime.newproc(size, f, args)功能就是创建一个新的g,这个函数不能用分段栈,因为它假设参数的放置顺序是紧接着函数f的(见前面函数调用协议一章,有关go关键字调用时的内存布局)。分段栈会破坏这个布局,所以在代码中加入了标记#pragma textflag 7表示不使用分段栈。它会调用函数newproc1,在newproc1中可以使用分段栈。真正的工作是调用newproc1完成的。newproc1进行下面这些动作。 9 | 10 | 首先,它会检查当前结构体M中的P中,是否有可用的结构体G。如果有,则直接从中取一个,否则,需要分配一个新的结构体G。如果分配了新的G,需要将它挂到runtime的相关队列中。 11 | 12 | 获取了结构体G之后,将调用参数保存到g的栈,将sp,pc等上下文环境保存在g的sched域,这样整个goroutine就准备好了,整个状态和一个运行中的goroutine被中断时一样,只要等分配到CPU,它就可以继续运行。 13 | 14 | newg->sched.sp = (uintptr)sp; 15 | newg->sched.pc = (byte*)runtime·goexit; 16 | newg->sched.g = newg; 17 | runtime·gostartcallfn(&newg->sched, fn); 18 | newg->gopc = (uintptr)callerpc; 19 | newg->status = Grunnable; 20 | newg->goid = runtime·xadd64(&runtime·sched.goidgen, 1); 21 | 22 | 23 | 然后将这个“准备好”的结构体G挂到当前M的P的队列中。这里会给予新的goroutine一次运行的机会,即:如果当前的P的数目没有到上限,也没有正在自旋抢CPU的M,则调用wakep将P立即投入运行。 24 | 25 | wakep函数唤醒P时,调度器会试着寻找一个可用的M来绑定P,必要的时候会新建M。让我们看看新建M的函数newm: 26 | 27 | // 新建一个m,它将以调用fn开始,或者是从调度器开始 28 | static void 29 | newm(void(*fn)(void), P *p) 30 | { 31 | M *mp; 32 | mp = runtime·allocm(p); 33 | mp->nextp = p; 34 | mp->mstartfn = fn; 35 | runtime·newosproc(mp, (byte*)mp->g0->stackbase); 36 | } 37 | 38 | runtime.newm功能跟newproc相似,前者分配一个goroutine,而后者分配一个M。其实一个M就是一个操作系统线程的抽象,可以看到它会调用runtime.newosproc。 39 | 40 | 总算看到了从Go的运行时库到操作系统的接口,runtime.newosproc(平台相关的)会调用系统的runtime.clone(平台相关的)来新建一个线程,新的线程将以runtime.mstart为入口函数。runtime.newosproc是个很有意思的函数,还有一些信号处理方面的细节,但是对鉴于我们是专注于调度方面,就不对它进行更细致的分析了,感兴趣的读者可以自行去runtime/os\_linux.c看看源代码。runtime.clone是用汇编实现的,代码在sys\_linux_amd64.s。 41 | 42 | 既然线程是以runtime.mstart为入口的,那么接下来看mstart函数。 43 | 44 | mstart是runtime.newosproc新建的系统线程的入口地址,新线程执行时会从这里开始运行。新线程的执行和goroutine的执行是两个概念,由于有m这一层对机器的抽象,是m在执行g而不是线程在执行g。所以线程的入口是mstart,g的执行要到schedule才算入口。函数mstart最后调用了schedule。 45 | 46 | 终于到了schedule了! 47 | 48 | 如果是从mstart进入到schedule的,那么schedule中逻辑非常简单,大概就这几步: 49 | 50 | 找到一个等待运行的g 51 | 如果g是锁定到某个M的,则让那个M运行 52 | 否则,调用execute函数让g在当前的M中运行 53 | 54 | execute会恢复newproc1中设置的上下文,这样就跳转到新的goroutine去执行了。从newproc出生一直到运行的过程分析,到此结束! 55 | 56 | 虽然按这样a调用b,b调用c,c调用d,d调用e的方式去分析源代码谁看都会晕掉,但还是要重复一遍这里的读代码过程,希望感兴趣的读者可以拿着注释过的源码按顺序走一遍: 57 | 58 | newproc -> newproc1 -> (如果P数目没到上限)wakep -> startm -> (可能引发)newm -> newosproc -> (线程入口)mstart -> schedule -> execute -> goroutine运行 59 | 60 | ## 进出系统调用 61 | 62 | 假设goroutine"生病"了,它要进入系统调用了,暂时无法继续执行。进入系统调用时,如果系统调用是阻塞的,goroutine会被剥夺CPU,将状态设置成Gsyscall后放到就绪队列。Go的syscall库中提供了对系统调用的封装,它会在真正执行系统调用之前先调用函数.entersyscall,并在系统调用函数返回后调用.exitsyscall函数。这两个函数就是通知Go的运行时库这个goroutine进入了系统调用或者完成了系统调用,调度器会做相应的调度。 63 | 64 | 比如syscall包中的Open函数,它会调用Syscall(SYS_OPEN, uintptr(unsafe.Pointer(\_p0)), uintptr(mode), uintptr(perm))实现。这个函数是用汇编写的,在syscall/asm\_linux\_amd64.s中可以看到它的定义: 65 | 66 | TEXT ·Syscall(SB),7,$0 67 | CALL runtime·entersyscall(SB) 68 | MOVQ 16(SP), DI 69 | MOVQ 24(SP), SI 70 | MOVQ 32(SP), DX 71 | MOVQ $0, R10 72 | MOVQ $0, R8 73 | MOVQ $0, R9 74 | MOVQ 8(SP), AX // syscall entry 75 | SYSCALL 76 | CMPQ AX, $0xfffffffffffff001 77 | JLS ok 78 | MOVQ $-1, 40(SP) // r1 79 | MOVQ $0, 48(SP) // r2 80 | NEGQ AX 81 | MOVQ AX, 56(SP) // errno 82 | CALL runtime·exitsyscall(SB) 83 | RET 84 | ok: 85 | MOVQ AX, 40(SP) // r1 86 | MOVQ DX, 48(SP) // r2 87 | MOVQ $0, 56(SP) // errno 88 | CALL runtime·exitsyscall(SB) 89 | RET 90 | 91 | 可以看到它进系统调用和出系统调用时分别调用了runtime.entersyscall和runtime.exitsyscall函数。那么,这两个函数做什么特殊的处理呢? 92 | 93 | 首先,将函数的调用者的SP,PC等保存到结构体G的sched域中。同时,也保存到g->gcsp和g->gcpc等,这个是跟垃圾回收相关的。 94 | 95 | 然后检查结构体Sched中的sysmonwait域,如果不为0,则将它置为0,并调用runtime·notewakeup(&runtime·sched.sysmonnote)。做这这一步的原因是,目前这个goroutine要进入Gsyscall状态了,它将要让出CPU。如果有人在等待CPU的话,会通知并唤醒等待者,马上就有CPU可用了。 96 | 97 | 接下来,将m的MCache置为空,并将m->p->m置为空,表示进入系统调用后结构体M是不需要MCache的,并且P也被剥离了,将P的状态设置为PSyscall。 98 | 99 | 有一个与entersyscall函数稍微不同的函数叫entersyscallblock,它会告诉提示这个系统调用是会阻塞的,因此会有一点点区别。它调用的releasep和handoffp。 100 | 101 | releasep将P和M完全分离,使p->m为空,m->p也为空,剥离m->mcache,并将P的状态设置为Pidle。注意这里的区别,在非阻塞的系统调用entersyscall中只是设置成Psyscall,并且也没有将m->p置为空。 102 | 103 | handoffp切换P。将P从处于syscall或者locked的M中,切换出来交给其它M。每个P中是挂了一个可执行的G的队列的,如果这个队列不为空,即如果P中还有G需要执行,则调用startm让P与某个M绑定后立刻去执行,否则将P挂到idlep队列中。 104 | 105 | 出系统调用时会调用到runtime·exitsyscall,这个函数跟进系统调用做相反的操作。它会先检查当前m的P和它状态,如果P不空且状态为Psyscall,则说明是从一个非阻塞的系统调用中返回的,这时是仍然有CPU可用的。因此将p->m设置为当前m,将p的mcache放回到m,恢复g的状态为Grunning。否则,它是从一个阻塞的系统调用中返回的,因此之前m的P已经完全被剥离了。这时会查看调用中是否还有idle的P,如果有,则将它与当前的M绑定。 106 | 107 | 如果从一个阻塞的系统调用中出来,并且出来的这一时刻又没有idle的P了,要怎么办呢?这种情况代码当前的goroutine无法继续运行了,调度器会将它的状态设置为Grunnable,将它挂到全局的就绪G队列中,然后停止当前m并调用schedule函数。 108 | 109 | ## goroutine的消亡以及状态变化 110 | 111 | goroutine的消亡比较简单,注意在函数newproc1,设置了fnstart为goroutine执行的函数,而将新建的goroutine的sched域的pc设置为了函数runtime.exit。当fnstart函数执行完返回时,它会返回到runtime.exit中。这时Go就知道这个goroutine要结束了,runtime.exit中会做一些回收工作,会将g的状态设置为Gdead等,并将g挂到P的free队列中。 112 | 113 | 从以上的分析中,其实已经基本上经历了goroutine的各种状态变化。在newproc1中新建的goroutine被设置为Grunnable状态,投入运行时设置成Grunning。在entersyscall的时候goroutine的状态被设置为Gsyscall,到出系统调用时根据它是从阻塞系统调用中出来还是非阻塞系统调用中出来,又会被设置成Grunning或者Grunnable的状态。在goroutine最终退出的runtime.exit函数中,goroutine被设置为Gdead状态。 114 | 115 | 等等,好像缺了什么?是的,Gidle始终没有出现过。这个状态好像实际上没有被用到。只有一个runtime.park函数会使goroutine进入到Gwaiting状态,但是park这个有什么作用我暂时还没看懂... 116 | 117 | goroutine的状态变迁图: 118 | 119 | ![](images/5.2.goroutine_state.jpg?raw=true) 120 | 121 | ## links 122 | * [目录]() 123 | * 上一节: [调度器相关数据结构](<05.1.md>) 124 | * 下一节: [设计与演化](<05.3.md>) -------------------------------------------------------------------------------- /zh/05.3.md: -------------------------------------------------------------------------------- 1 | # 5.3 设计与演化 2 | 其实讲一个东西,讲它是什么样是不足够的。如果能讲清楚它为什么会是这样子,则会举一反三。为了理解goroutine的本质,这里将从最基本的线程池讲起,谈谈Go调度设计背后的故事,讲清楚它为什么是这样子。 3 | 4 | ## 线程池 5 | 先看一些简单点的吧。一个常规的 线程池+任务队列 的模型如图所示: 6 | 7 | ![](images/5.3.worker.jpg?raw=true) 8 | 9 | 把每个工作线程叫worker的话,每条线程运行一个worker,每个worker做的事情就是不停地从队列中取出任务并执行: 10 | 11 | while(!empty(queue)) { 12 | q = get(queue); //从任务队列中取一个(涉及加锁等) 13 | q->callback(); //执行该任务 14 | } 15 | 16 | 当然,这是最简单的情形,但是一个很明显的问题就是一个进入callback之后,就失去了控制权。因为没有一个调度器层的东西,一个任务可以执行很长很长时间一直占用的worker线程,或者阻塞于io之类的。 17 | 18 | 也许用Go语言表述会更地道一些。好吧,那么让我们用Go语言来描述。假设我们有一些“任务”,任务是一个可运行的东西,也就是只要满足Run函数,它就是一个任务。所以我们就把这个任务叫作接口G吧。 19 | 20 | type G interface { 21 | Run() 22 | } 23 | 24 | 我们有一个全局的任务队列,里面包含很多可运行的任务。线程池的各个线程从全局的任务队列中取任务时,显然是需要并发保护的,所以有下面这个结构体: 25 | 26 | type Sched struct { 27 | allg []G 28 | lock *sync.Mutex 29 | } 30 | 31 | 以及它的变量 32 | 33 | var sched Sched 34 | 35 | 每条线程是一个worker,这里我们给worker换个名字,就把它叫M吧。前面已经说过了,worker做的事情就是不停的去任务队列中取一个任务出来执行。于是用Go语言大概可以写成这样子: 36 | 37 | func M() { 38 | for { 39 | sched.lock.Lock() //互斥地从就绪G队列中取一个g出来运行 40 | if sched.allg > 0 { 41 | g := sched.allg[0] 42 | sched.allg = sched.allg[1:] 43 | sched.lock.Unlock() 44 | g.Run() //运行它 45 | } else { 46 | sched.lock.Unlock() 47 | } 48 | } 49 | } 50 | 51 | 接下来,将整个系统启动: 52 | 53 | for i:=0; i= GOMAXPROCS { 75 | sched.lock.Lock() 76 | sched.allg = append(sched.allg, g) //把g放回到队列中 77 | sched.lock.Unlock() 78 | time.Sleep() //这个M不再干活 79 | } 80 | } 81 | 82 | 于是就变成了这样子: 83 | 84 | ![](images/5.3.m_g.jpg?raw=true) 85 | 86 | 其实这个也很好理解,就像线程池做负载调节一样,当任务队列很长后,忙不过来了,则再开几条线程出来。而如果任务队列为空了,则可以释放一些线程。 87 | 88 | ## 协程与保存上下文 89 | 大家都知道阻塞于系统调用,会白白浪费CPU。而使用异步事件或回调的思维方式又十分反人类。上面的模型既然这么简单明了,为什么不这么用呢?其实上面的东西看上去简单,但实现起来确不那么容易。 90 | 91 | 将一个正在执行的任务yield出去,再在某个时刻再弄回来继续运行,这就涉及到一个麻烦的问题,即保存和恢复运行时的上下文环境。 92 | 93 | 在此先引入协程的概念。协程是轻量级的线程,它相对线程的优势就在于协程非常轻量级,进行切换以及保存上下文环境代价非常的小。协程的具体的实现方式有多种,上面就是其中一种基于线程池的实现方式。每个协程是一个任务,可以保存和恢复任务运行时的上下文环境。 94 | 95 | 协程一类的东西一般会提供类似yield的函数。协程运行到一定时候就主动调用yield放弃自己的执行,把自己再次放回到任务队列中等待下一次调用时机等等。 96 | 97 | 其实Go语言中的goroutine就是协程。每个结构体G中有一个sched域就是用于保存自己上下文的。这样,这种goroutine就可以被换出去,再换进来。这种上下文保存在用户态完成,不必陷入到内核,非常的轻量,速度很快。保存的信息很少,只有当前的PC,SP等少量信息。只是由于要优化,所以代码看上去更复杂一些,比如要重用内存空间所以会有gfree和mhead之类的东西。 98 | 99 | ## Go1.0 100 | 在前面的代码中,线程与M是直接对应的关系,这个解耦还是不够。Go1.0中将M抽出来成为了一个结构体,startm函数是线程的入口地址,而goroutine的入口地址是go表达式中的那个函数。总体上跟上面的结构差不多,进出系统调用的时候goroutine会跟M一起进入到系统调用中,schedule中会匹配g和m,让空闲的m来运行g。如果检测到干活的数量少于GOMAXPROCS并且没有空闲着的m,则会创建新的m来运行g。出系统调用的时候,如果已经有GOMAXPROCS个m在干活了,则这个出系统调用的m会被挂起,它的g也会被挂到待运行的goroutine队列中。 101 | 102 | 在Go语言中m是machine的缩写,也就是机器的抽象。它被设计成了可以运行所有的G。比如说一个g开始在某个m上运行,经过几次进出系统调用之后,可能运行它的m挂起了,其它的m会将它从队列中取出并继续运行。 103 | 104 | 每次调度都会涉及对g和m等队列的操作,这些全局的数据在多线程情况下使用就会涉及到大量的锁操作。在频繁的系统调用中这将是一个很大的开销。为了减少系统调用开销,Go1.0在这里做了一些优化的。1.0版中,在它的Sched结构体中有一个atomic字段,类型是一个volatile的无符32位整型。 105 | 106 | // sched中的原子字段是一个原子的uint32,存放下列域 107 | // 15位 mcpu --正在占用cpu运行的m数量 (进入syscall的m是不占用cpu的) 108 | // 15位 mcpumax --最大允许这么多个m同时使用cpu 109 | // 1位 waitstop --有g等待结束 110 | // 1位 gwaiting --等待队列不为空,有g处于waiting状态 111 | // [15 bits] mcpu number of m's executing on cpu 112 | // [15 bits] mcpumax max number of m's allowed on cpu 113 | // [1 bit] waitstop some g is waiting on stopped 114 | // [1 bit] gwaiting gwait != 0 115 | 116 | 这些信息是进行系统调用和出系统调用时需要用到的,它会决定是否需要进入到调度器层面。直接用CAS操作Sched的atomic字段判断,将它们打包成一个字节使得可以通过一次原子读写获取它们而不用加锁。这将极大的减少那些大量使用系统调用或者cgo的多线程程序的contention。 117 | 118 | 除了进出系统调用以外,操作这些域只会发生于持有调度器锁的时候,因此goroutines不用担心其它goroutine会对这些字段进行操作。特别是,进出系统调用只会读mcpumax,waitstop和gwaiting。决不会写他们。因此,(持有调度器锁)写这些域时完全不用担心会发生写冲突。 119 | 120 | 总体上看,Go1.0调度设计结构比较简单,代码也比较清晰。但是也存在一些问题。这样的调度器设计限制了Go程序的并发度。测试发现有14%是的时间浪费在了runtime.futex()中。 121 | 122 | 具体地看: 123 | 124 | 1. 单个全局锁(Sched.Lock)用来保护所有的goroutine相关的操作(创建,完成,调度等)。 125 | 2. Goroutine切换。工作线程在各自之前切换goroutine,这导致延迟和额外的负担。每个M都必须可以执行任何的G. 126 | 3. 内存缓存MCache是每个M的。而当M阻塞后,相应的内存资源也被一起拿走了。 127 | 4. 过多的线程阻塞、恢复。系统调用时的工作线程会频繁地阻塞,恢复,造成过多的负担。 128 | 129 | 第一点很明显,所有的goroutine都用一个锁保护的,这个锁粒度是比较大的,只要goroutine的相关操作都会锁住调度。然后是goroutine切换,前面说了,每个M都是可以执行所有的goroutine的。举个很简单的类比,多核CPU中每个核都去执行不同线程的代码,这显然是不利于缓存的局部性的,切换开销也会变大。内存缓存和其它缓存是关联到所有的M的,而事实上它本只需要关联到运行Go代码的M(阻塞于系统调用的M是不需要mcache的)。运行着Go代码的M和所有M的比例可能高达1:100。这导致过度的资源消耗。 130 | 131 | ## Go1.1 132 | Go1.1相对于1.0一个重要的改动就是重新调用了调度器。前面已经看到,老版本中的调度器实现是存在一些问题的。解决方式是引入Processor的概念,并在Processors之上实现工作流窃取的调度器。 133 | 134 | M代表OS线程。P代表Go代码执行时需要的资源。当M执行Go代码时,它需要关联一个P,当M为idle或者在系统调用中时,它也需要P。有刚好GOMAXPROCS个P。所有的P被组织为一个数组,工作流窃取需要这个条件。GOMAXPROCS的改变涉及到stop/start the world来resize数组P的大小。 135 | 136 | gfree和grunnable从sched中移到P中。这样就解决了前面的单个全局锁保护用有goroutine的问题,由于goroutine现在被分到每个P中,它们是P局部的goroutine,因此P只管去操作自己的goroutine就行了,不会与其它P上的goroutine冲突。全局的grunnable队列也仍然是存在的,只有在P去访问全局grunnable队列时才涉及到加锁操作。mcache从M中移到P中。不过当前还不彻底,在M中还是保留着mcache域的。 137 | 138 | 加入了P后,sched.atomic也从Sched结构体中去掉了。 139 | 140 | 当一个新的G创建或者现有的G变成runnable,它将一个runnable的goroutine推到当前的P。当P完成执行G,它将G从自己的runnable goroutine中pop出去。如果链为空,P会随机从其它P中窃取一半的可运行的goroutine。 141 | 142 | 当M创建一个新G的时候,必须保证有另一个M来执行这个G。类似的,当一个M进入到系统调用时,必须保证有另一个M来执行G的代码。 143 | 144 | 2层自旋:关联了P的处于idle状态的的M自旋寻找新的G;没有关联P的M自旋等待可用的P。最多有GOMAXPROCS个自旋的M。只要有第二类M时第一类M就不会阻塞。 145 | -------------------------------------------------------------------------------- /zh/05.4.md: -------------------------------------------------------------------------------- 1 | # 5.4 死锁检测和竞态检测 2 | 3 | 检测是否所有的P都idle了 -------------------------------------------------------------------------------- /zh/05.5.md: -------------------------------------------------------------------------------- 1 | # 5.5 抢占式调度 2 | 3 | goroutine本来是设计为协程形式,但是随着调度器的实现越来越成熟,Go在1.2版中开始引入比较初级的抢占式调度。 4 | 5 | ## 从一个bug说起 6 | 7 | Go在设计之初并没考虑将goroutine设计成抢占式的。用户负责让各个goroutine交互合作完成任务。一个goroutine只有在涉及到加锁,读写通道或者主动让出CPU等操作时才会触发切换。 8 | 9 | 垃圾回收器是需要stop the world的。如果垃圾回收器想要运行了,那么它必须先通知其它的goroutine合作停下来,这会造成较长时间的等待时间。考虑一种很极端的情况,所有的goroutine都停下来了,只有其中一个没有停,那么垃圾回收就会一直等待着没有停的那一个。 10 | 11 | 抢占式调度可以解决这种问题,在抢占式情况下,如果一个goroutine运行时间过长,它就会被剥夺运行权。 12 | 13 | ## 总体思路 14 | 15 | 引入抢占式调度,会对最初的设计产生比较大的影响,Go还只是引入了一些很初级的抢占,并没有像操作系统调度那么复杂,没有对goroutine分时间片,设置优先级等。 16 | 17 | 只有长时间阻塞于系统调用,或者运行了较长时间才会被抢占。runtime会在后台有一个检测线程,它会检测这些情况,并通知goroutine执行调度。 18 | 19 | 目前并没有直接在后台的检测线程中做处理调度器相关逻辑,只是相当于给goroutine加了一个“标记”,然后在它进入函数时才会触发调度。这么做应该是出于对现有代码的修改最小的考虑。 20 | 21 | ## sysmon 22 | 23 | 前面讲Go程序的初始化过程中有提到过,runtime开了一条后台线程,运行一个sysmon函数。这个函数会周期性地做epoll操作,同时它还会检测每个P是否运行了较长时间。 24 | 25 | 如果检测到某个P状态处于Psyscall超过了一个sysmon的时间周期(20us),并且还有其它可运行的任务,则切换P。 26 | 27 | 如果检测到某个P的状态为Prunning,并且它已经运行了超过10ms,则会将P的当前的G的stackguard设置为StackPreempt。这个操作其实是相当于加上一个标记,通知这个G在合适时机进行调度。 28 | 29 | 目前这里只是尽最大努力送达,但并不保证收到消息的goroutine一定会执行调度让出运行权。 30 | 31 | ## morestack的修改 32 | 33 | 前面说的,将stackguard设置为StackPreempt实际上是一个比较trick的代码。我们知道Go会在每个函数入口处比较当前的栈寄存器值和stackguard值来决定是否触发morestack函数。 34 | 35 | 将stackguard设置为StackPreempt作用是进入函数时必定触发morestack,然后在morestack中再引发调度。 36 | 37 | 看一下StackPreempt的定义,它是大于任何实际的栈寄存器的值的: 38 | 39 | // 0xfffffade in hex. 40 | #define StackPreempt ((uint64)-1314) 41 | 42 | 然后在morestack中加了一小段代码,如果发现stackguard为StackPreempt,则相当于调用runtime.Gosched。 43 | 44 | 所以,到目前为止Go的抢占式调度还是很初级的,比如一个goroutine运行了很久,但是它并没有调用另一个函数,则它不会被抢占。当然,一个运行很久却不调用函数的代码并不是多数情况。 45 | -------------------------------------------------------------------------------- /zh/06.0.md: -------------------------------------------------------------------------------- 1 | # 6 内存管理 2 | 3 | 内存管理是非常重要的一个话题。关于编程语言是否应该支持垃圾回收就有个搞笑的争论,一派人认为,内存管理太重要了,而手动管理麻烦且容易出错,所以我们应该交给机器去管理。另一派人则认为,内存管理太重要了!所以如果交给机器管理我不能放心。争论归争论,但不管哪一派,大家对内存管理重要性的认同都是勿庸质疑的。 4 | 5 | Go是一门带垃圾回收的语言,Go语言中有指针,却没有C中那么灵活的指针操作。大多数情况下是不需要用户自己去管理内存的,但是理解Go语言是如何做内存管理对于写出优秀的程序是大有帮助的。 6 | 7 | 本章将从两个方面来看Go中的内存管理机制,一个方面是内存池,另一个方面是垃圾回收。 8 | 9 | ## links 10 | * [目录]() 11 | * 上一章: [抢占式调度](<05.5.md>) 12 | * 下一节: [内存池](<06.1.md>) 13 | -------------------------------------------------------------------------------- /zh/06.1.md: -------------------------------------------------------------------------------- 1 | # 6.1 内存池 2 | 3 | ## 概述 4 | Go的内存分配器采用了跟tcmalloc库相同的实现,是一个带内存池的分配器,底层直接调用操作系统的mmap等函数。 5 | 6 | 作为一个内存池,回忆一下跟它相关的基本部分。首先,它会向操作系统申请大块内存,自己管理这部分内存。然后,它是一个池子,当上层释放内存时它不实际归还给操作系统,而是放回池子重复利用。接着,内存管理中必然会考虑的就是内存碎片问题,如果尽量避免内存碎片,提高内存利用率,像操作系统中的首次适应,最佳适应,最差适应,伙伴算法都是一些相关的背景知识。另外,Go是一个支持goroutine这种多线程的语言,所以它的内存管理系统必须也要考虑在多线程下的稳定性和效率问题。 7 | 8 | 在多线程方面,很自然的做法就是每条线程都有自己的本地的内存,然后有一个全局的分配链,当某个线程中内存不足后就向全局分配链中申请内存。这样就避免了多线程同时访问共享变量时的加锁。 在避免内存碎片方面,大块内存直接按页为单位分配,小块内存会切成各种不同的固定大小的块,申请做任意字节内存时会向上取整到最接近的块,将整块分配给申请者以避免随意切割。 9 | 10 | Go中为每个系统线程分配一个本地的MCache(前面介绍的结构体M中的MCache域),少量的地址分配就直接从MCache中分配,并且定期做垃圾回收,将线程的MCache中的空闲内存返回给全局控制堆。小于32K为小对象,大对象直接从全局控制堆上以页(4k)为单位进行分配,也就是说大对象总是以页对齐的。一个页可以存入一些相同大小的小对象,小对象从本地内存链表中分配,大对象从中心内存堆中分配。 11 | 12 | 大约有100种内存块类别,每一类别都有自己对象的空闲链表。小于32kB的内存分配被向上取整到对应的尺寸类别,从相应的空闲链表中分配。一页内存只可以被分裂成同一种尺寸类别的对象,然后由空闲链表分配器管理。 13 | 14 | 分配器的数据结构包括: 15 | + FixAlloc: 固定大小(128kB)的对象的空闲链分配器,被分配器用于管理存储 16 | + MHeap: 分配堆,按页的粒度进行管理(4kB) 17 | + MSpan: 一些由MHeap管理的页 18 | + MCentral: 对于给定尺寸类别的共享的free list 19 | + MCache: 用于小对象的每M一个的cache 20 | 21 | 我们可以将Go语言的内存管理看成一个两级的内存管理结构,MHeap和MCache。上面一级管理的基本单位是页,用于分配大对象,每次分配都是若干连续的页,也就是若干个4KB的大小。使用的数据结构是MHeap和MSpan,用BestFit算法做分配,用位示图做回收。下面一级管理的基本单位是不同类型的固定大小的对象,更像一个对象池而不是内存池,用引用计数做回收。下面这一级使用的数据结构是MCache。 22 | 23 | ## MHeap 24 | 25 | MHeap层次用于直接分配较大(>32kB)的内存空间,以及给MCentral和MCache等下层提供空间。它管理的基本单位是MSpan。MSpan是一个表示若干连续内存页的数据结构,简化后如下: 26 | 27 | ```C 28 | struct MSpan 29 | { 30 | PageID start; // starting page number 31 | uintptr npages; // number of pages in span 32 | }; 33 | ``` 34 | 35 | 通过一个基地址+(页号*页大小),就可以定位到这个MSpan的实际的地址空间了,基地址是在MHeap中存储了的。 36 | 37 | MHeap负责将MSpan组织和管理起来,MHeap数据结构中的重要部分如图所示。 38 | 39 | ![](./images/6.1.mheap.jpg?raw=true) 40 | 41 | free是一个分配池,从free[i]出去的MSpan每个大小都i页的,总共256个槽位。再大了之后,大小就不固定了,由large链起来。 42 | 43 | 分配过程: 44 | 如果能从free[]的分配池中分配,则从其中分配。如果发生切割则将剩余部分放回free[]中。比如要分配2页大小的空间,从图上2号槽位开始寻找,直到4号槽位有可用的MSpan,则拿一个出来,切出两页,剩余的部分再放回2号槽位中。 45 | 否则从large链表中去分配,按BestFit算法去找一块可用空间。 46 | 47 | 化整为零简单,化零为整麻烦。回收的时候如果相邻的块是未使用的,要进行合并,否则一直划分下去就会产生很多碎片,找不到一个足够大小的连续空间。因为涉及到合并,回收会比分配复杂一些,所有就有什么伙伴算法,边界标识算法,位示图之类的。 48 | 49 | Go在这里使用的类似于位示图。可以看到MHeap中有一个 50 | 51 | ```C 52 | MSpan *map[1<nonempty->freelist分配。如果发现freelist空了,则说明这一块MSpan满了,将它移到MCentral->empty。 100 | 前面说过,回收比分配复杂,因为涉及到合并。这里的合并是通过引用计数实现的。从MSpan中每划出一个对象,则引用计数加一,每回收一个对象,则引用计数减一。如果减完之后引用计数为零了,则说明这整块的MSpan已经没被使用了,可以将它归还给MHeap。 101 | 102 | ## 其它 103 | 104 | 本节的内存池涉及的文件包括: 105 | + malloc.h 头文件 106 | + malloc.goc 最外层的包装 107 | + msize.c 将各种大小向上取整到相应的尺寸类别 108 | + mheap.c 对应MHeap中相关实现,还有MSpan 109 | + mcache.c 对应MCache中相关实现 110 | + mcentral.c 对应MCentral中相关实现 111 | + mem_linux.c SysAlloc等sys相关的实现 -------------------------------------------------------------------------------- /zh/06.2.md: -------------------------------------------------------------------------------- 1 | # 6.2 垃圾回收 2 | Go语言中使用的垃圾回收使用的是标记清扫算法。进行垃圾回收时会stoptheworld。不过,在当前1.3版本中,实现了精确的垃圾回收和并行的垃圾回收,大大地提高了垃圾回收的速度,进行垃圾回收时系统并不会长时间卡住。 3 | 4 | ## 标记清扫算法 5 | 标记清扫算法是一个很基础的垃圾回收算法,该算法中有一个标记初始的root区域,以及一个受控堆区。root区域主要是程序运行到当前时刻的栈和全局数据区域。在受控堆区中,很多数据是程序以后不需要用到的,这类数据就可以被当作垃圾回收了。判断一个对象是否为垃圾,就是看从root区域的对象是否有直接或间接的引用到这个对象。如果没有任何对象引用到它,则说明它没有被使用,因此可以安全地当作垃圾回收掉。 6 | 7 | 标记清扫算法分为两阶段:标记阶段和清扫阶段。标记阶段,从root区域出发,扫描所有root区域的对象直接或间接引用到的对象,将这些对上全部加上标记。在回收阶段,扫描整个堆区,对所有无标记的对象进行回收。(补图) 8 | 9 | ## 位图标记和内存布局 10 | 既然垃圾回收算法要求给对象加上垃圾回收的标记,显然是需要有标记位的。一般的做法会将对象结构体中加上一个标记域,一些优化的做法会利用对象指针的低位进行标记,这都只是些奇技淫巧罢了。Go没有这么做,它的对象和C的结构体对象完全一致,使用的是非侵入式的标记位,我们看看它是怎么实现的。 11 | 12 | 堆区域对应了一个标记位图区域,堆中每个字(不是byte,而是word)都会在标记位区域中有对应的标记位。每个机器字(32位或64位)会对应4位的标记位。因此,64位系统中相当于每个标记位图的字节对应16个堆中的字节。 13 | 14 | 虽然是一个堆字节对应4位标记位,但标记位图区域的内存布局并不是按4位一组,而是16个堆字节为一组,将它们的标记位信息打包存储的。每组64位的标记位图从上到下依次包括: 15 | 16 | 16位的 特殊位 标记位 17 | 16位的 垃圾回收 标记位 18 | 16位的 无指针/块边界 的标记位 19 | 16位的 已分配 标记位 20 | 21 | 这样设计使得对一个类型的相应的位进行遍历很容易。 22 | 23 | 前面提到堆区域和堆地址的标记位图区域是分开存储的,其实它们是以mheap.arena_start地址为边界,向上是实际使用的堆地址空间,向下则是标记位图区域。以64位系统为例,计算堆中某个地址的标记位的公式如下: 24 | 25 | 偏移 = 地址 - mheap.arena_start 26 | 标记位地址 = mheap.arena_start - 偏移/16 - 1 27 | 移位 = 偏移 % 16 28 | 标记位 = *标记位地址 >> 移位 29 | 30 | 然后就可以通过 (标记位 & 垃圾回收标记位),(标记位 & 分配位),等来测试相应的位。其中已分配的标记为1<<0,无指针/块边界是1<<16,垃圾回收的标记位为1<<32,特殊位1<<48 31 | 32 | 具体的内存布局如下图所示: 33 | ![](images/6.2.gc_bitmap.jpg?raw=true) 34 | 35 | ## 精确的垃圾回收 36 | 像C这种不支持垃圾回收的语言,其实还是有些垃圾回收的库可以使用的。这类库一般也是用的标记清扫算法实现的,但是它们都是保守的垃圾回收。为什么叫“保守”的垃圾回收呢?之所以叫“保守”是因为它们没办法获取对象类型信息,因此只能保守地假设地址区间中每个字都是指针。 37 | 38 | 无法获取对象的类型信息会造成什么问题呢?这里举两个例子来说明。先看第一个例子,假设某个结构体中是不包含指针成员的,那么对该结构体成员进行垃圾回收时,其实是不必要递归地标记结构体的成员的。但是由于没有类型信息,我们并不知道这个结构体成员不包含指针,因此我们只能对结构体的每个字节递归地标记下去,这显然会浪费很多时间。这个例子说明精确的垃圾回收可以减少不必要的扫描,提高标记过程的速度。 39 | 40 | 再看另一个例子,假设堆中有一个long的变量,它的值是8860225560。但是我们不知道它的类型是long,所以在进行垃圾回收时会把个当作指针处理,这个指针引用到了0x2101c5018位置。假设0x2101c5018碰巧有某个对象,那么这个对象就无法被释放了,即使实际上已经没任何地方使用它。这个例子说明,保守的垃圾回收某些情况下会出现垃圾无法被回收。虽然不会造成大的问题,但总是让人很不爽,都是没有类型信息惹的祸。 41 | 42 | 现在好了,Go在1.1版本中开始支持精确的垃圾回收。精确的垃圾回收首先需要的就是类型信息,上一节中讲过MSpan结构体,类型信息是存储在MSpan中的。从一个地址计算它所属的MSpan,公式如下: 43 | 44 | 页号 = (地址 - mheap.arena_start) >> 页大小 45 | MSpan = mheap->map[页号] 46 | 47 | 接下来通过MSpan->type可以得到分配块的类型。这是一个MType的结构体: 48 | 49 | ```C 50 | struct MTypes 51 | { 52 | byte compression; // one of MTypes_* 53 | bool sysalloc; // whether (void*)data is from runtime·SysAlloc 54 | uintptr data; 55 | }; 56 | ``` 57 | 58 | MTypes描述MSpan里分配的块的类型,其中compression域描述数据的布局。它的取值为MTypes_Empty,MTypes_Single,MTypes_Words,MTypes_Bytes四个中的一种。 59 | 60 | MTypes_Empty: 61 | 所有的块都是free的,或者这个分配块的类型信息不可用。这种情况下data域是无意义的。 62 | MTypes_Single: 63 | 这个MSpan只包含一个块,data域存放类型信息,sysalloc域无意义 64 | MTypes_Words: 65 | 这个MSpan包含多个块(块的种类多于7)。这时data指向一个数组[NumBlocks]uintptr,,数组里每个元素存放相应块的类型信息 66 | MTypes_Bytes: 67 | 这个MSpan中包含最多7种不同类型的块。这时data域指下面这个结构体 68 | struct { 69 | type [8]uintptr // type[0] is always 0 70 | index [NumBlocks]byte 71 | } 72 | 第i个块的类型是data.type[data.index[i]] 73 | 74 | 表面上看MTypes_Bytes好像最复杂,其实这里的复杂程度是MTypes_Empty小于MTypes_Single小于MTypes_Bytes小于MTypes_Words的。MTypes_Bytes只不过为了做优化而显得很复杂。 75 | 76 | 上一节中说过,每一块MSpan中存放的块的大小都是一样的,不过它们的类型不一定相同。如果没有使用,那么这个MSpan的类型就是MTypes_Empty。如果存一个很大块,大于这个MSpan大小的一半,因此存不了其它东西了,那么这个MSpan的类型是MTypes_Single。假设存了多种块,每一块用一个指针,本来可以直接用MTypes_Words存的。但是当类型不多时,可以把这些类型的指针集中起来放在数组中,然后存储数组索引。这是一个小的优化,可以节省内存空间。 77 | 78 | 得到的类型信息最终是什么样子的呢?其实是一个这样的结构体: 79 | 80 | struct Type 81 | { 82 | uintptr size; 83 | uint32 hash; 84 | uint8 _unused; 85 | uint8 align; 86 | uint8 fieldAlign; 87 | uint8 kind; 88 | Alg *alg; 89 | void *gc; 90 | String *string; 91 | UncommonType *x; 92 | Type *ptrto; 93 | }; 94 | 95 | 不同类型的类型信息结构体略有不同,这个是通用的部分。可以看到这个结构体中有一个gc域,精确的垃圾回收就是利用类型信息中这个gc域实现的。 96 | 97 | 从gc出去其实是一段指令码,是对这种类型的数据进行垃圾回收的指令,Go中用一个状态机来执行垃圾回收指令码。大致的框架是类似下面这样子: 98 | 99 | ```C 100 | for(;;) { 101 | switch(pc[0]) { 102 | case GC_PTR: 103 | break; 104 | case GC_SLICE: 105 | break; 106 | case GC_APTR: 107 | break; 108 | case GC_STRING: 109 | continue; 110 | case GC_EFACE: 111 | if(eface->type == nil) 112 | continue; 113 | break; 114 | case GC_IFACE: 115 | break; 116 | case GC_DEFAULT_PTR: 117 | while(stack_top.b <= end_b) { 118 | obj = *(byte**)stack_top.b; 119 | stack_top.b += PtrSize; 120 | if(obj >= arena_start && obj < arena_used) { 121 | *ptrbufpos++ = (PtrTarget){obj, 0}; 122 | if(ptrbufpos == ptrbuf_end) 123 | flushptrbuf(ptrbuf, &ptrbufpos, &wp, &wbuf, &nobj); 124 | } 125 | } 126 | case GC_ARRAY_START: 127 | continue; 128 | case GC_ARRAY_NEXT: 129 | continue; 130 | case GC_CALL: 131 | continue; 132 | case GC_MAP_PTR: 133 | continue; 134 | case GC_MAP_NEXT: 135 | continue; 136 | case GC_REGION: 137 | continue; 138 | case GC_CHAN_PTR: 139 | continue; 140 | case GC_CHAN: 141 | continue; 142 | default: 143 | runtime·throw("scanblock: invalid GC instruction"); 144 | return; 145 | } 146 | } 147 | ``` 148 | 149 | ## 小结 150 | 151 | Go语言使用标记清扫的垃圾回收算法,标记位图是非侵入式的,内存布局设计得比较巧妙。并且当前版本的Go实现了精确的垃圾回收。在精确的垃圾回收中,通过定位对象的类型信息,得到该类型中的垃圾回收的指令码,通过一个状态机解释这段指令码来执行特定类型的垃圾回收工作。 152 | 153 | 对于堆中任意地址的对象,找到它的类型信息过程为,先通过它在的内存页找到它所属的MSpan,然后通过MSpan中的类型信息找到它的类型信息。 154 | 155 | 不知道读者有没有注意一个细节,MType中的data值应该是存放Type结构体的指针,但它却是uintptr表示的。这是为什么呢? -------------------------------------------------------------------------------- /zh/06.3.md: -------------------------------------------------------------------------------- 1 | # 6.3 垃圾回收 2 | 3 | 目前Go中垃圾回收的核心函数是scanblock,源代码在文件runtime/mgc0.c中。这个函数非常难读,单个函数写了足足500多行。上面有两个大的循环,外层循环作用是扫描整个内存块区域,将类型信息提取出来,得到其中的gc域。内层的大循环是实现一个状态机,解析执行类型信息中gc域的指令码。 4 | 5 | 先说说上一节留的疑问吧。MType中的数据其实是类型信息,但它是用uintptr表示,而不是Type结构体的指针,这是一个优化的小技巧。由于内存分配是机器字节对齐的,所以地址就只用到了高位,低位是用不到的。于是低位可以利用起来存储一些额外的信息。这里的uintptr中高位存放的是Type结构体的指针,低位用来存放类型。通过 6 | 7 | ```C 8 | t = (Type*)(type & ~(uintptr)(PtrSize-1)); 9 | ``` 10 | 11 | 就可以从uintptr得到Type结构体指针,而通过 12 | 13 | ```C 14 | type & (PtrSize-1) 15 | ``` 16 | 17 | 就可以得到类型。这里的类型有TypeInfo_SingleObject,TypeInfo_Array,TypeInfo_Map,TypeInfo_Chan几种。 18 | 19 | ## 基本的标记过程 20 | 从最简单的开始看,基本的标记过程,有一个不带任何优化的标记的实现,对应于函数debug_scanblock。 21 | 22 | debug_scanblock函数是递归实现的,单线程的,更简单更慢的scanblock版本。该函数接收的参数分别是一个指针表示要扫描的地址,以及字节数。 23 | 24 | 首先要将传入的地址,按机器字节大小对齐。 25 | 然后对待扫描区域的每个地址: 26 | 找到它所属的MSpan,将地址转换为MSpan里的对象地址。 27 | 根据对象的地址,找到对应的标记位图里的标记位。 28 | 判断标记位,如果是未分配则跳过。否则加上特殊位标记(debug_scanblock中用特殊位代码的mark位)完成标记。 29 | 判断标记位中标记了无指针标记位,如果没有,则要递归地调用debug_scanblock。 30 | 31 | 这个递归版本的标记算法还是很容易理解的。其中涉及的细节在上节中已经说过了,比如任意给定一个地址,找到它的标记位信息。很明显这里仅仅使用了一个无指针位,并没有精确的垃圾回收。 32 | 33 | ## 并行的垃圾回收 34 | Go在这个版本中不仅实现了精确的垃圾回收,而且实现了并行的垃圾回收。标记算法本质上就是一个树的遍历过程,上面实现的是一个递归版本。 35 | 36 | 并行的垃圾回收需要做的第一步,就是先将算法做成非递归的。非递归版本的树的遍历需要用到一个队列。树的非递归遍历的伪代码大致是: 37 | 38 | 根结点进队 39 | while(队列不空) { 40 | 出队 41 | 访问 42 | 将子结点进队 43 | } 44 | 45 | 第二步是使上面的代码能够并行地工作,显然这时是需要一个线程安全的队列的。假设有这样一个队列,那么上面代码就能够工作了。但是,如果不加任何优化,这里的队列的并行访问非常地频繁,对这个队列加锁代价会非常高,即使是使用CAS操作也会大大降低效率。 46 | 47 | 所以,第三步要做的就是优化上面队列的数据结构。事实上,Go中并没有使用这样一个队列,为了优化,它通过三个数据结构共同来完成这个队列的功能,这三个数据结构分别是PtrTarget数组,Workbuf,lfstack。 48 | 49 | 先说Workbuf吧。听名字就知道,这个结构体的意思是工作缓冲区,里面存放的是一个数组,数组中的每个元素都是一个待处理的结点,也就是一个Obj指针。这个对象本身是已经标记了的,这个对象直接或间接引用到的对象,都是应该被标记的,它们不会被当作垃圾回收掉。Workbuf是比较大的,一般是N个内存页的大小(目前是2页,也就是8K)。 50 | 51 | PtrTarget数组也是一个缓冲区,相当于一个intermediate buffer,跟Workbuf有一点点的区别。第一,它比Workbuf小很多,大概只有32或64个元素的数组。第二,Workbuf中的对象全部是已经标记过的,而PtrTarget中的元素可能是标记的,也可能是没标记的。第三,PtrTarget里面的元素是指针而不是对象,指针是指向任意地址的,而对象是对齐到正确地址的。从一个指针变为一个对象要经过一次变换,上一节中有讲过具体细节。 52 | 53 | 垃圾回收过程中,会有一个从PtrTarget数组冲刷到Workbuf缓冲区的过程。对应于源代码中的flushptrbuf函数,这个函数作用就是对PtrTaget数组中的所有元素,如果该地址是mark了的,则将它移到Workbuf中。标记过程形成了一个环,在环的一边,对Workbuf中的对象,会将它们可能引用的区域全部放到PtrTarget中记录下来。在环的另一边,又会将PtrTarget中确定需要标记的地址刷到Workbuf中。这个过程一轮一轮地进行,推动非递归版本的树的遍历过程,也就是前面伪代码中的出队,访问,子结点进队的过程。 54 | 55 | 另一个数据结构是lfstack,这个名字的意思是lock free栈。其实它是被用作了一个无锁的链表,链表结点是以Workbuf为单位的。并行垃圾回收中,多条线程会从这个链表中取数据,每次以一个Workbuf为工作单位。同时,标记的过程中也会产生Workbuf结点放到链中。lfstack保证了对这个链的并发访问的安全性。由于现在链表结点是以Workbuf为单位的,所以保证整体的性能,lfstack的底层代码是用CAS操作实现的。 56 | 57 | 经过第三步中数据结构上的拆解,整个并行垃圾回收的架构已经呼之欲出了,这就是标记扫描的核心函数scanblock。这个函数是在多线程下并行安全的。 58 | 59 | 那么,最后一步,多线程并行。整个的gc是以runtime.gc函数为入口的,它实际调用的是gc。进入gc函数后会先stoptheworld,接着添加标记的root区域。然后会设置markroot和sweepspan的并行任务。运行mark的任务,扫描块,运行sweep的任务,最后starttheworld并切换出去。 60 | 61 | 有一个ParFor的数据结构。在gc函数中调用了 62 | 63 | ```C 64 | runtime·parforsetup(work.markfor, work.nproc, work.nroot, nil, false, markroot); 65 | runtime·parforsetup(work.sweepfor, work.nproc, runtime·mheap->nspan, nil, true, sweepspan); 66 | ``` 67 | 68 | 是设置好回调函数让线程去执行markroot和sweepspan函数。垃圾回收时会stoptheworld,其它goroutine会对发起stoptheworld做出响应,调用runtime.gchelper,这个函数会调用scanblock帮助标记过程。也会并行地做markroot和sweepspan的过程。 69 | 70 | ```C 71 | void 72 | runtime·gchelper(void) 73 | { 74 | gchelperstart(); 75 | 76 | // parallel mark for over gc roots 77 | runtime·parfordo(work.markfor); 78 | 79 | // help other threads scan secondary blocks 80 | scanblock(nil, nil, 0, true); 81 | 82 | if(DebugMark) { 83 | // wait while the main thread executes mark(debug_scanblock) 84 | while(runtime·atomicload(&work.debugmarkdone) == 0) 85 | runtime·usleep(10); 86 | } 87 | 88 | runtime·parfordo(work.sweepfor); 89 | bufferList[m->helpgc].busy = 0; 90 | if(runtime·xadd(&work.ndone, +1) == work.nproc-1) 91 | runtime·notewakeup(&work.alldone); 92 | } 93 | ``` 94 | 95 | 其中并行时也有实现工作流窃取的概念,多个worker同时去工作缓存中取数据出来处理,如果自己的任务做完了,就会从其它的任务中“偷”一些过来执行。 96 | 97 | ## 垃圾回收的时机 98 | 垃圾回收的触发是由一个gcpercent的变量控制的,当新分配的内存占已在使用中的内存的比例超过gcprecent时就会触发。比如,gcpercent=100,当前使用了4M的内存,那么当内存分配到达8M时就会再次gc。如果回收完毕后,内存的使用量为5M,那么下次回收的时机则是内存分配达到10M的时候。也就是说,并不是内存分配越多,垃圾回收频率越高,这个算法使得垃圾回收的频率比较稳定,适合应用的场景。 99 | 100 | gcpercent的值是通过环境变量GOGC获取的,如果不设置这个环境变量,默认值是100。如果将它设置成off,则是关闭垃圾回收。 -------------------------------------------------------------------------------- /zh/07.0.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/zh/07.0.md -------------------------------------------------------------------------------- /zh/07.1.md: -------------------------------------------------------------------------------- 1 | # 7.1 channel 2 | 3 | ## channel数据结构 4 | Go语言channel是first-class的,意味着它可以被存储到变量中,可以作为参数传递给函数,也可以作为函数的返回值返回。作为Go语言的核心特征之一,虽然channel看上去很高端,但是其实channel仅仅就是一个数据结构而已,结构体定义如下: 5 | 6 | struct Hchan 7 | { 8 | uintgo qcount; // 队列q中的总数据数量 9 | uintgo dataqsiz; // 环形队列q的数据大小 10 | uint16 elemsize; 11 | bool closed; 12 | uint8 elemalign; 13 | Alg* elemalg; // interface for element type 14 | uintgo sendx; // 发送index 15 | uintgo recvx; // 接收index 16 | WaitQ recvq; // 因recv而阻塞的等待队列 17 | WaitQ sendq; // 因send而阻塞的等待队列 18 | Lock; 19 | }; 20 | 21 | 让我们来看一个Hchan这个结构体。其中一个核心的部分是存放channel数据的环形队列,由qcount和elemsize分别指定了队列的容量和当前使用量。dataqsize是队列的大小。elemalg是元素操作的一个Alg结构体,记录下元素的操作,如copy函数,equal函数,hash函数等。 22 | 23 | 24 | 可能会有人疑惑,结构体中只看到了队列大小相关的域,并没有看到存放数据的域啊?如果是带缓冲区的chan,则缓冲区数据实际上是紧接着Hchan结构体中分配的。 25 | 26 | c = (Hchan*)runtime.mal(n + hint*elem->size); 27 | 28 | 另一个重要部分就是recvq和sendq两个链表,一个是因读这个通道而导致阻塞的goroutine,另一个是因为写这个通道而阻塞的goroutine。如果一个goroutine阻塞于channel了,那么它就被挂在recvq或sendq中。WaitQ是链表的定义,包含一个头结点和一个尾结点: 29 | 30 | struct WaitQ 31 | { 32 | SudoG* first; 33 | SudoG* last; 34 | }; 35 | 36 | 队列中的每个成员是一个SudoG结构体变量。 37 | 38 | struct SudoG 39 | { 40 | G* g; // g and selgen constitute 41 | uint32 selgen; // a weak pointer to g 42 | SudoG* link; 43 | int64 releasetime; 44 | byte* elem; // data element 45 | }; 46 | 47 | 该结构中主要的就是一个g和一个elem。elem用于存储goroutine的数据。读通道时,数据会从Hchan的队列中拷贝到SudoG的elem域。写通道时,数据则是由SudoG的elem域拷贝到Hchan的队列中。 48 | 49 | Hchan结构如下图所示: 50 | ![](images/7.1.channel.png?raw=true) 51 | 52 | ## 读写channel操作 53 | 54 | 先看写channel的操作,基本的写channel操作,在底层运行时库中对应的是一个runtime.chansend函数。 55 | 56 | c <- v 57 | 58 | 在运行时库中会执行: 59 | 60 | void runtime·chansend(ChanType *t, Hchan *c, byte *ep, bool *pres, void *pc) 61 | 62 | 其中c就是channel,ep是取变量v的地址。这里的传值约定是调用者负责分配好ep的空间,仅需要简单的取变量地址就够了。pres参数是在select中的通道操作使用的。 63 | 64 | 这个函数首先会区分是同步还是异步。同步是指chan是不带缓冲区的,因此可能写阻塞,而异步是指chan带缓冲区,只有缓冲区满才阻塞。 65 | 66 | 在同步的情况下,由于channel本身是不带数据缓存的,这时首先会查看Hchan结构体中的recvq链表时否为空,即是否有因为读该管道而阻塞的goroutine。如果有则可以正常写channel,否则操作会阻塞。 67 | 68 | recvq不为空的情况下,将一个SudoG结构体出队列,将传给通道的数据(函数参数ep)拷贝到SudoG结构体中的elem域,并将SudoG中的g放到就绪队列中,状态置为ready,然后函数返回。 69 | 70 | 如果recvq为空,否则要将当前goroutine阻塞。此时将一个SudoG结构体,挂到通道的sendq链表中,这个SudoG中的elem域是参数eq,SudoG中的g是当前的goroutine。当前goroutine会被设置为waiting状态并挂到等待队列中。 71 | 72 | 在异步的情况,如果缓冲区满了,也是要将当前goroutine和数据一起作为SudoG结构体挂在sendq队列中,表示因写channel而阻塞。否则也是先看有没有recvq链表是否为空,有就唤醒。 73 | 74 | 跟同步不同的是在channel缓冲区不满的情况,这里不会阻塞写者,而是将数据放到channel的缓冲区中,调用者返回。 75 | 76 | 读channel的操作也是类似的,对应的函数是runtime.chansend。一个是收一个是发,基本的过程都是差不多的。 77 | 78 | 需要注意的是几种特殊情况下的通道操作--空通道和关闭的通道。 79 | 80 | 空通道是指将一个channel赋值为nil,或者定义后不调用make进行初始化。按照Go语言的语言规范,读写空通道是永远阻塞的。其实在函数runtime.chansend和runtime.chanrecv开头就有判断这类情况,如果发现参数c是空的,则直接将当前的goroutine放到等待队列,状态设置为waiting。 81 | 82 | 读一个关闭的通道,永远不会阻塞,会返回一个通道数据类型的零值。这个实现也很简单,将零值复制到调用函数的参数ep中。写一个关闭的通道,则会panic。关闭一个空通道,也会导致panic。 83 | 84 | ## select的实现 85 | 86 | select-case中的chan操作编译成了if-else。比如: 87 | 88 | select { 89 | case v = <-c: 90 | ...foo 91 | default: 92 | ...bar 93 | } 94 | 95 | 会被编译为: 96 | 97 | if selectnbrecv(&v, c) { 98 | ...foo 99 | } else { 100 | ...bar 101 | } 102 | 103 | 类似地 104 | 105 | select { 106 | case v, ok = <-c: 107 | ... foo 108 | default: 109 | ... bar 110 | } 111 | 112 | 会被编译为: 113 | 114 | if c != nil && selectnbrecv2(&v, &ok, c) { 115 | ... foo 116 | } else { 117 | ... bar 118 | } 119 | 120 | 接下来就是看一下selectnbrecv相关的函数了。其实没有任何特殊的魔法,这些函数只是简单地调用runtime.chanrecv函数,只不过设置了一个参数,告诉当runtime.chanrecv函数,当不能完成操作时不要阻塞,而是返回失败。也就是说,所有的select操作其实都仅仅是被换成了if-else判断,底层调用的不阻塞的通道操作函数。 121 | 122 | 在Go的语言规范中,select中的case的执行顺序是随机的,而不像switch中的case那样一条一条的顺序执行。那么,如何实现随机呢? 123 | 124 | select和case关键字使用了下面的结构体: 125 | 126 | struct Scase 127 | { 128 | SudoG sg; // must be first member (cast to Scase) 129 | Hchan* chan; // chan 130 | byte* pc; // return pc 131 | uint16 kind; 132 | uint16 so; // vararg of selected bool 133 | bool* receivedp; // pointer to received bool (recv2) 134 | }; 135 | 136 | struct Select 137 | { 138 | uint16 tcase; // 总的scase[]数量 139 | uint16 ncase; // 当前填充了的scase[]数量 140 | uint16* pollorder; // case的poll次序 141 | Hchan** lockorder; // channel的锁住的次序 142 | Scase scase[1]; // 每个case会在结构体里有一个Scase,顺序是按出现的次序 143 | }; 144 | 145 | 146 | 每个select都对应一个Select结构体。在Select数据结构中有个Scase数组,记录下了每一个case,而Scase中包含了Hchan。然后pollorder数组将元素随机排列,这样就可以将Scase乱序了。 -------------------------------------------------------------------------------- /zh/07.2.md: -------------------------------------------------------------------------------- 1 | # 7.2 interface 2 | 3 | interface是Go语言中最成功的设计之一,空的interface可以被当作“鸭子”类型使用,它使得Go这样的静态语言拥有了一定的动态性,但却又不损失静态语言在类型安全方面拥有的编译时检查的优势。 4 | 5 | 依赖于接口而不是实现,优先使用组合而不是继承,这是程序抽象的基本原则。但是长久以来以C++为代表的“面向对象”语言曲解了这些原则,让人们走入了误区。为什么要将方法和数据绑死?为什么要有多重继承这么变态的设计?面向对象中最强调的应该是对象间的消息传递,却为什么被演绎成了封装继承和多态。面向对象是否实现程序程序抽象的合理途径,又或者是因为它存在我们就认为它合理了。历史原因,中间出现了太多的错误。不管怎么样,Go的interface给我们打开了一扇新的窗。 6 | 7 | 那么,Go中的interface在底层是如何实现的呢? 8 | 9 | ## Eface和Iface 10 | interface实际上就是一个结构体,包含两个成员。其中一个成员是指向具体数据的指针,另一个成员中包含了类型信息。空接口和带方法的接口略有不同,下面分别是空接口和带方法的接口是使用的数据结构: 11 | 12 | ```C 13 | struct Eface 14 | { 15 | Type* type; 16 | void* data; 17 | }; 18 | struct Iface 19 | { 20 | Itab* tab; 21 | void* data; 22 | }; 23 | ``` 24 | 25 | 先看Eface,它是interface{}底层使用的数据结构。数据域中包含了一个void\*指针,和一个类型结构体的指针。interface{}扮演的角色跟C语言中的void\*是差不多的,Go中的任何对象都可以表示为interface{}。不同之处在于,interface{}中有类型信息,于是可以实现反射。 26 | 27 | 类型信息的结构体定义如下: 28 | 29 | ```C 30 | struct Type 31 | { 32 | uintptr size; 33 | uint32 hash; 34 | uint8 _unused; 35 | uint8 align; 36 | uint8 fieldAlign; 37 | uint8 kind; 38 | Alg *alg; 39 | void *gc; 40 | String *string; 41 | UncommonType *x; 42 | Type *ptrto; 43 | }; 44 | ``` 45 | 46 | 其实在前面我们已经见过它了。精确的垃圾回收中,就是依赖Type结构体中的gc域的。不同类型数据的类型信息结构体并不完全一致,Type是类型信息结构体中公共的部分,其中size描述类型的大小,hash数据的hash值,align是对齐,fieldAlgin是这个数据嵌入结构体时的对齐,kind是一个枚举值,每种类型对应了一个编号。alg是一个函数指针的数组,存储了hash/equal/print/copy四个函数操作。UncommonType是指向一个函数指针的数组,收集了这个类型的实现的所有方法。 47 | 48 | 49 | 在reflect包中有个KindOf函数,返回一个interface{}的Type,其实该函数就是简单的取Eface中的Type域。 50 | 51 | Iface和Eface略有不同,它是带方法的interface底层使用的数据结构。data域同样是指向原始数据的,而Itab的结构如下: 52 | 53 | ```C 54 | struct Itab 55 | { 56 | InterfaceType* inter; 57 | Type* type; 58 | Itab* link; 59 | int32 bad; 60 | int32 unused; 61 | void (*fun[])(void); 62 | }; 63 | ``` 64 | 65 | Itab中不仅存储了Type信息,而且还多了一个方法表fun[]。一个Iface中的具体类型中实现的方法会被拷贝到Itab的fun数组中。 66 | 67 | ## 具体类型向接口类型赋值 68 | 69 | 将具体类型数据赋值给interface{}这样的抽象类型,中间会涉及到类型转换操作。从接口类型转换为具体类型(也就是反射),也涉及到了类型转换。这个转换过程中做了哪些操作呢?先看将具体类型转换为接口类型。如果是转换成空接口,这个过程比较简单,就是返回一个Eface,将Eface中的data指针指向原型数据,type指针会指向数据的Type结构体。 70 | 71 | 将某个类型数据转换为带方法的接口时,会复杂一些。中间涉及了一道检测,该类型必须要实现了接口中声明的所有方法才可以进行转换。这个检测是在编译过程中做的,我们可以做个测试: 72 | 73 | ```Go 74 | type I interface { 75 | String() 76 | } 77 | var a int = 5 78 | var b I = a 79 | ``` 80 | 81 | 编译会报错: 82 | 83 | cannot use a (type int) as type I in assignment: 84 | int does not implement I (missing String method) 85 | 86 | 说明具体类型转换为带方法的接口类型是在编译过程中进行检测的。 87 | 88 | 那么这个检测是如何实现的呢?在runtime下找到了iface.c文件,应该是早期版本是在运行时检测留下的,其中有一个itab函数就是判断某个类型是否实现了某个接口,如果是则返回一个Itab结构体。 89 | 90 | 类型转换时的检测就是比较具体类型的方法表和接口类型的方法表,看具体类型是实现了接口类型所声明的所有的方法。还记得Type结构体中是有个UncommonType字段的,里面有张方法表,类型所实现的方法都在里面。而在Itab中有个InterfaceType字段,这个字段中也有一张方法表,就是这个接口所要求的方法。这两处方法表都是排序过的,只需要一遍顺序扫描进行比较,应该可以知道Type中否实现了接口中声明的所有方法。最后还会将Type方法表中的函数指针,拷贝到Itab的fun字段中。 91 | 92 | 这里提到了三个方法表,有点容易把人搞晕,所以要解释一下。 93 | 94 | Type的UncommonType中有一个方法表,某个具体类型实现的所有方法都会被收集到这张表中。reflect包中的Method和MethodByName方法都是通过查询这张表实现的。表中的每一项是一个Method,其数据结构如下: 95 | 96 | ```C 97 | struct Method 98 | { 99 | String *name; 100 | String *pkgPath; 101 | Type *mtyp; 102 | Type *typ; 103 | void (*ifn)(void); 104 | void (*tfn)(void); 105 | }; 106 | ``` 107 | 108 | Iface的Itab的InterfaceType中也有一张方法表,这张方法表中是接口所声明的方法。其中每一项是一个IMethod,数据结构如下: 109 | 110 | ```c 111 | struct IMethod 112 | { 113 | String *name; 114 | String *pkgPath; 115 | Type *type; 116 | }; 117 | ``` 118 | 119 | 跟上面的Method结构体对比可以发现,这里是只有声明没有实现的。 120 | 121 | Iface中的Itab的func域也是一张方法表,这张表中的每一项就是一个函数指针,也就是只有实现没有声明。 122 | 123 | 类型转换时的检测就是看Type中的方法表是否包含了InterfaceType的方法表中的所有方法,并把Type方法表中的实现部分拷到Itab的func那张表中。 124 | 125 | ## reflect 126 | reflect就是给定一个接口类型的数据,得到它的具体类型的类型信息,它的Value等。reflect包中的TypeOf和ValueOf函数分别做这个事情。 127 | 128 | 还有像 129 | 130 | ```Go 131 | v, ok := i.(T) 132 | ``` 133 | 134 | 这样的语法,也是判断一个接口i的具体类型是否为类型T,如果是则将其值返回给v。这跟上面的类型转换一样,也会检测转换是否合法。不过这里的检测是在运行时执行的。在runtime下的iface.c文件中,有一系统的assetX2X函数,比如runtime.assetE2T,runtime.assetI2T等等。这个实现起来比较简单,只需要比较Iface中的Itab的type是否与给定Type为同一个。 -------------------------------------------------------------------------------- /zh/07.3.md: -------------------------------------------------------------------------------- 1 | # 7.3 方法调用 2 | 3 | ## 普通的函数调用 4 | 普通的函数调用跟C语言中的调用方式基本上是一样的,除了多值返回的一些细微区别,见前面章节。 5 | 6 | ## 对象的方法调用 7 | 根据[Go语言文档](http://golang.org/ref/spec#Method_expressions),对象的方法调用相当于普通函数调用的一个语法糖衣。 8 | 9 | ```go 10 | type T struct { 11 | a int 12 | } 13 | func (tv T) Mv(a int) int { return 0 } // value receiver 14 | func (tp *T) Mp(f float32) float32 { return 1 } // pointer receiver 15 | 16 | var t T 17 | ``` 18 | 19 | 表达式 20 | 21 | T.Mv 22 | 23 | 得到一个函数,这个函数等价于Mv但是带一个显示的接收者作为第一个参数,也就是 24 | 25 | func(tv T, a int) int 26 | 27 | 下面这些调用是等价的: 28 | 29 | t.Mv(7) 30 | T.Mv(t, 7) 31 | (T).Mv(t, 7) 32 | f1 := T.Mv; f1(t, 7) 33 | f2 := (T).Mv; f2(t, 7) 34 | 35 | 可以看了一下方法调用用生成的汇编代码: 36 | 37 | ```go 38 | type T int 39 | func (t T) f() { 40 | fmt.Println("hello world!\n") 41 | } 42 | 43 | func main() { 44 | var v T 45 | v.f() 46 | return 47 | } 48 | ``` 49 | 50 | 将它进行汇编: 51 | 52 | go tool 6g -S test.go 53 | 54 | 得到的汇编代码是: 55 | 56 | 0044 (sum.go:15) TEXT main+0(SB),$8-0 57 | 0045 (sum.go:15) FUNCDATA $0,gcargs·1+0(SB) 58 | 0046 (sum.go:15) FUNCDATA $1,gclocals·1+0(SB) 59 | 0047 (sum.go:16) MOVQ $0,AX 60 | 0048 (sum.go:17) MOVQ AX,(SP) 61 | 0049 (sum.go:17) CALL ,T.f+0(SB) 62 | 0050 (sum.go:18) RET , 63 | 64 | 从这段汇编代码中可以看出,方法调用跟普通函数调用完全没有区别,这里就是把v作为第一个参数调用函数T.f()。 65 | 66 | ## 组合对象的方法调用 67 | 在Go中没有继承,但是有结构体嵌入的概念。将一个带方法的类型匿名嵌入到另一个结构体中,则这个结构体也会拥有嵌入的类型的方法。 68 | 69 | 这个功能是如何实现的呢?其实很简单。当一个类型被匿名嵌入结构体时,它的方法表会被拷贝到嵌入结构体的Type的方法表中。这个过程也是在编译时就可以完成的。对组合对象的方法调用同样也仅仅是普通函数调用的语法糖衣。 70 | 71 | ## 接口的方法调用 72 | 接口的方法调用跟上述情况略有不同,不同之处在于它是根据接口中的方法表得到对应的函数指针,然后调用的,而前面是直接调用的函数地址。 73 | 74 | 对象的方法调用,等价于普通函数调用,函数地址是在编译时就可以确定的。而接口的方法调用,函数地址要在运行时才能确定。将具体值赋值给接口时,会将Type中的方法表复制到接口的方法表中,然后接口方法的函数地址才会确定下来。因此,接口的方法调用的代价比普通函数调用和对象的方法调用略高,多了几条指令。 -------------------------------------------------------------------------------- /zh/08.0.md: -------------------------------------------------------------------------------- 1 | # 8 网络 2 | 3 | 这一章我们将看一下Go的网络模块。Go在网络编程方面提倡的做法是,每来一个连接就开一个goroutine去处理。非常的用户友好,不用学习一些反人类的网络编程模式,并且性能是有保障的。这些都得益于Go的网络模块的实现。 4 | 5 | 由于goroutine的实现非常轻量,很容易就可以开很多的goroutine,这为每条连接分配一个goroutine打好了基础。Go对网络的处理,在用户层是阻塞的,实现层是非阻塞的。这一章里我们将研究Go是如何封装好epoll/kqueue,为用户提供友好的阻塞式接口的。 6 | 7 | 另一方面,我们也会看一下Go是的网络层的一些api是如何优雅进行封装的。 8 | 9 | ## links 10 | * [目录]() 11 | * 上一节: [方法调用](<07.3.md>) 12 | * 下一节: [非阻塞io](<08.1.md>) 13 | -------------------------------------------------------------------------------- /zh/08.1.md: -------------------------------------------------------------------------------- 1 | # 8.1 非阻塞io 2 | 3 | Go提供的网络接口,在用户层是阻塞的,这样最符合人们的编程习惯。在runtime层面,是用epoll/kqueue实现的非阻塞io,为性能提供了保障。 4 | 5 | ## 如何实现 6 | 7 | 底层非阻塞io是如何实现的呢?简单地说,所有文件描述符都被设置成非阻塞的,某个goroutine进行io操作,读或者写文件描述符,如果此刻io还没准备好,则这个goroutine会被放到系统的等待队列中,这个goroutine失去了运行权,但并不是真正的整个系统“阻塞”于系统调用。 8 | 9 | 后台还有一个poller会不停地进行poll,所有的文件描述符都被添加到了这个poller中的,当某个时刻一个文件描述符准备好了,poller就会唤醒之前因它而阻塞的goroutine,于是goroutine重新运行起来。 10 | 11 | 这个poller是在后台一直运行的,前面分析系统调度章节时为了简化并没有提起它。其实在proc.c文件中,runtime.main函数的第一行代码就是 12 | 13 | newm(sysmon, nil); 14 | 15 | 这个意思就是新建一个M并让它运行sysmon函数,前面说过M就是机器的抽象,它会直接开一个物理线程。sysmon里面是个死循环,每睡眠一小会儿就会调用runtime.epoll函数,这个sysmon就是所谓的poller。 16 | 17 | poller是一个比gc更高优先级的东西,何以见得呢?首先,垃圾回收只是用runtime.newproc建立出来的,它仅仅是个goroutine任务,而poller是直接用newm建立出来的,它跟startm是平级的。也就相当于gc只是线程池里的任务,而poller自身直接就是worker。然后,gc只是被触发性地发生的,是被动的。而poller却是每隔很短时间就会主动运行。 18 | 19 | ## 封装层次 20 | 21 | 从最原始的epoll系统调用,到提供给用户的网络库函数,可以分成三个封装层次。这三个层次分别是,依赖于系统的api封装,平台独立的runtime封装,提供给用户的库的封装。 22 | 23 | 最下面一层是依赖于系统部分的封装。各个平台下的实现并不一样,比如linux下是封装的epoll,freebsd下是封装的kqueue。以linux为例,实现了一组调用epoll相关系统调用的封装: 24 | 25 | int32 runtime·epollcreate(int32 size); 26 | int32 runtime·epollcreate1(int32 flags); 27 | int32 runtime·epollctl(int32 epfd, int32 op, int32 fd, EpollEvent *ev); 28 | int32 runtime·epollwait(int32 epfd, EpollEvent *ev, int32 nev, int32 timeout); 29 | void runtime·closeonexec(int32 fd); 30 | 31 | 它们都是直接使用汇编调用系统调用实现的,比如: 32 | 33 | ```asm 34 | TEXT runtime·epollcreate1(SB),7,$0 35 | MOVL 8(SP), DI 36 | MOVL $291, AX // syscall entry 37 | SYSCALL 38 | RET 39 | ``` 40 | 41 | 这些函数还要继续被封装成下面一组函数: 42 | 43 | runtime·netpollinit(void); 44 | runtime·netpollopen(int32 fd, PollDesc *pd); 45 | runtime·netpollready(G **gpp, PollDesc *pd, int32 mode); 46 | 47 | runtime·netpollinit是对poller进行初始化。 48 | runtime·netpollopen是对fd和pd进行关联,实现边沿触发通知。 49 | runtime·netpollready,使用前必须调用这个函数来表示fd是就绪的 50 | 51 | 不管是哪个平台,最终都会将依赖于系统的部分封装好,提供上面这样一组函数供runtime使用。 52 | 53 | 接下来是平台独立的poller的封装,也就是runtime层的封装。这一层封装是最复杂的,它对外提供的一组接口是: 54 | 55 | func runtime_pollServerInit() 56 | func runtime_pollOpen(fd int) (pd *PollDesc, errno int) 57 | func runtime_pollClose(pd *PollDesc) 58 | func runtime_pollReset(pd *PollDesc, mode int) (err int) 59 | func runtime_pollWait(pd *PollDesc, mode int) (err int) 60 | func runtime_pollSetDeadline(pd *PollDesc, d int64, mode int) 61 | func runtime_pollUnblock(pd *PollDesc) 62 | 63 | 这一组函数是由runtime封装好,提供给net包调用的。里面定义了一个PollDesc的结构体,将fd和对应的goroutine封装起来,从而实现当goroutine读写fd阻塞时,将goroutine变为Gwaiting。等一下回头再看实现的细节。 64 | 65 | 最后一层封装层次是提供给用户的net包。在net包中网络文件描述符都是用一个netFD结构体来表示的,其中有个成员就是pollDesc。 66 | 67 | ```c 68 | // 网络文件描述符 69 | type netFD struct { 70 | sysmu sync.Mutex 71 | sysref int 72 | 73 | // must lock both sysmu and pollDesc to write 74 | // can lock either to read 75 | closing bool 76 | 77 | // immutable until Close 78 | sysfd int 79 | family int 80 | sotype int 81 | isConnected bool 82 | sysfile *os.File 83 | net string 84 | laddr Addr 85 | raddr Addr 86 | 87 | // serialize access to Read and Write methods 88 | rio, wio sync.Mutex 89 | 90 | // wait server 91 | pd pollDesc 92 | } 93 | ``` 94 | 95 | 所有用户的net包的调用最终调用到pollDesc的上面那一组函数中,这样就实现了当goroutine读或写阻塞时会被放到等待队列。最终的效果就是用户层阻塞,底层非阻塞。 96 | 97 | ## 文件描述符和goroutine 98 | 99 | 当一个goroutine进行io阻塞时,会去被放到等待队列。这里面就关键的就是建立起文件描述符和goroutine之间的关联。pollDesc结构体就是完成这个任务的。它的结构体定义如下: 100 | 101 | ```c 102 | struct PollDesc 103 | { 104 | PollDesc* link; // in pollcache, protected by pollcache.Lock 105 | Lock; // protectes the following fields 106 | int32 fd; 107 | bool closing; 108 | uintptr seq; // protects from stale timers and ready notifications 109 | G* rg; // 因读这个fd而阻塞的G,等待READY信号 110 | Timer rt; // read deadline timer (set if rt.fv != nil) 111 | int64 rd; // read deadline 112 | G* wg; // 因写这个fd而阻塞的goroutines 113 | Timer wt; 114 | int64 wd; 115 | }; 116 | ``` 117 | 118 | 这个结构体是重用的,其中link就是将它链起来。PollDesc对象必须是类型稳定的,因为在描述符关闭/重用之后我们会得到epoll/kqueue就绪通知。结构体中有一个seq序号,稳定的通知是通过使用这个序号实现的,当deadline改变或者描述符重用时,序号会增加。 119 | 120 | runtime\_pollServerInit的实现就是调用更下层的runtime·netpollinit函数。 121 | runtime\_pollOpen从PollDesc结构体缓存中拿一个出来,设置好它的fd。之所以叫Open而不是new,就是因为PollDesc结构体是重用的。 122 | runtime\_pollClose函数调用runtime·netpollclose后将PollDesc结构体放回缓存。 123 | 124 | 这些都还没涉及到fd与goroutine交互部分,仅仅是直接对epoll的调用。从下面这个函数可以看到fd与goroutine交互部分: 125 | 126 | func runtime_pollWait(pd *PollDesc, mode int) (err int) 127 | 128 | 它会调用到netpollblock,这个函数是这样子的: 129 | 130 | ```c 131 | static void 132 | netpollblock(PollDesc *pd, int32 mode) 133 | { 134 | G **gpp; 135 | 136 | gpp = &pd->rg; 137 | if(mode == 'w') 138 | gpp = &pd->wg; 139 | if(*gpp == READY) { 140 | *gpp = nil; 141 | return; 142 | } 143 | if(*gpp != nil) 144 | runtime·throw("epoll: double wait"); 145 | *gpp = g; 146 | runtime·park(runtime·unlock, &pd->Lock, "IO wait"); 147 | runtime·lock(pd); 148 | } 149 | ``` 150 | 151 | 最后的runtime.park函数,就是将当前的goroutine(调用者)设置为waiting状态。 152 | 153 | 上面这一部分是goroutine被放到等待队列的部分,下面看它被唤醒的部分。在sysmon函数中,会不停地调用runtime.epoll,这个函数对就绪的网络连接进行poll,返回可运行的goroutine。epoll只能知道哪个fd就绪了,那么它怎么知道哪个goroutine就绪了呢?原来epoll的data域存放的就是PollDesc结构体指针。因此就可以得到其中的goroutine了。 154 | -------------------------------------------------------------------------------- /zh/08.2.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/zh/08.2.md -------------------------------------------------------------------------------- /zh/09.0.md: -------------------------------------------------------------------------------- 1 | # 9 cgo 2 | 3 | 下面是一个使用cgo的例子: 4 | 5 | 6 | package rand 7 | 8 | /* 9 | #include 10 | */ 11 | import "C" 12 | 13 | func Random() int { 14 | return int(C.random()) 15 | } 16 | 17 | func Seed(i int) { 18 | C.srandom(C.uint(i)) 19 | } 20 | 21 | 22 | rand包导入了"C",但是在Go的标准库中并没有一个"C"包。这是因为"C"是一个伪包,这是一个特殊的名字,cgo通过这个包知道它是引用C命名空间的。Go编译器使用符号"·"来区分命名空间,而C编译器使用不同的约定,因此使用C包中的名字时,Go编译器就知道应该使用C的命名约定。 23 | 24 | 在将要进入这一章之前,请读者先思考下面一些问题: 25 | 26 | 1. Go使用的是分段栈,初始栈大小很小,当发现栈不够时会动态增长。动态增长是通过进入函数时插入检测指令实现的。然而C函数不使用分段栈技术,并且假设栈是足够大的。那么Go是如何处理不让cgo调用发生栈溢出的呢? 27 | 28 | 2. Go中的goroutine都是协作式的,运行到调用runtime库时就有机会进行调度。然而C函数是不会与Go的runtime做这种交互的,所以cgo的函数不是一个协作式的,那么如何避免进入C函数的这个goroutine“失控”? 29 | 30 | 3. cgo不仅仅是从Go调用C,还包括从C中调用Go函数。这里面又有哪些技术难点?举个简单的例子,C中调用Go函数f,而f中是使用了go建立新的goroutine的,但是在C中是不支持Go的runtime的。 -------------------------------------------------------------------------------- /zh/09.1.md: -------------------------------------------------------------------------------- 1 | # 9.1 预备知识 2 | cgo内部实现相关的知识是比较偏底层的,同时与Go系统调用约定以及的goroutine的调度都有一定的关联,因此这里先写一些预备知识。 3 | 4 | 本节的内容可能需要前面第三章和第五章的一些基础,同时也作为前面没有提到的一些细节的继续补充。 5 | 6 | ## m的g0栈 7 | Go的运行时库中使用了几个重要的结构体,其中M是机器的抽象。每个M可以运行各个goroutine,在结构体M的定义中有一个相对特殊的goroutine叫g0(还有另一个比较特殊的gsignal,与本节内容无关暂且不讲)。那么这个g0特殊在什么地方呢? 8 | 9 | g0的特殊之处在于它是带有调度栈的goroutine,下文就将其称为“m的g0栈“。Go在执行调度相关代码时,都是使用的m的g0栈。当一个g执行的是调度相关的代码时,它并不是直接在自己的栈中执行,而是先切换到m的g0栈然后再执行代码。 10 | 11 | m的g0栈是一个特殊的栈,g0的分配和普通goroutine的分配过程不同,g0是在m建立时就生成的,并且给它分配的栈空间比较大,可以假定它的大小是足够大而不必使用分段栈。而普通的goroutine是在runtime.newproc时建立,并且初始栈空间分配得很小(4K),会在需要时增长。不仅如此,m的g0栈同时也是这个m对应的物理线程的栈。 12 | 13 | 这样就相当于拥有了一个“无穷”大小的非分段栈,于是回答了前面提的那个问题:Go使用的是分段栈,初始栈大小很小,当发现栈不够时会动态增长。动态增长是通过进入函数时插入检测指令实现的。然而C函数不使用分段栈技术,并且假设栈是足够大的。调用cgo代码时,使用的是m的g0栈,这是一个足够大的不会发生分段的栈。 14 | 15 | 函数newm是新那一个结构体M,其中调用runtime.allocm分配M的空间。它的g0域是这个分配的: 16 | 17 | ` mp->g0 = runtime·malg(8192);` 18 | 19 | 等等!好像有哪里不对?这个栈并不是真正的“无穷”大的,它只有8K并且不会增长?那么如果调用的C函数使用超过8K的栈大小会发生什么事情呢?让我们先试一下,我们建立一个文件test.go,内容如下: 20 | 21 | package main 22 | 23 | /* 24 | #include "stdio.h" 25 | 26 | void test(int n) { 27 | char dummy[1024]; 28 | 29 | printf("in c test func iterator %d\n", n); 30 | if(n <= 0) { 31 | return; 32 | } 33 | dummy[n] = '\a'; 34 | test(n-1); 35 | } 36 | #cgo CFLAGS: -g 37 | */ 38 | import "C" 39 | 40 | func main() { 41 | C.test(C.int(20)) 42 | } 43 | 44 | 函数test被递归调用多次之后,使用的栈空间是超过8K的。然后?程序运行正常,什么也没发生。为什么呢?先卖个关子,到后面再解释原因。 45 | 46 | ## 进入系统调用 47 | Go的运行时库对系统调用作了特殊处理,所有涉及到调用系统调用之前,都会先调用runtime.entersyscall,而在出系统调用函数之后,会调用runtime.exitsyscall。这样做原因跟调度器相关,目的是始终维持GOMAXPROCS的数量,当进入到系统调用时,runtime.entersyscall会将P的M剥离并将它设置为PSyscall状态,告知系统此时其它的P有机会运行,以保证始终是GOMAXPROCS个P在运行。 48 | 49 | runtime.entersyscall函数会立刻返回,它仅仅是起到一个通知的作用。那么这跟cgo又有什么关系呢?这个关系可大着呢!在执行cgo函数调用之前,其实系统会先调用runtime.entersyscall。这是一个很关键的处理,Go把cgo的C函数调用像系统调用一样独立出去了,不让它影响运行时库。这就回答了前面提出的第二个问题:Go中的goroutine都是协作式的,运行到调用runtime库时就有机会进行调度。然而C函数是不会与Go的runtime做这种交互的,所以cgo的函数不是一个协作式的,那么如何避免进入C函数的这个goroutine“失控”?答案就在这里。将C函数像处理系统调用一样隔离开来,这个goroutine也就不必参与调度了。而其它部分的goroutine正常的运行不受影响。 50 | 51 | ## 退出系统调用 52 | 退出系统调用跟进入系统调用是一个相反的过程,runtime.exitsyscall函数会查看当前仍然有可用的P,则让它继续运行,否则这个goroutine就要被挂起了。 53 | 54 | 对于cgo的代码也是同样的作用,出了cgo的C函数调用之后会调用runtime.exitsyscall。 55 | 56 | -------------------------------------------------------------------------------- /zh/09.2.md: -------------------------------------------------------------------------------- 1 | # 9.2 cgo关键技术 2 | 上一节我们看了一些预备知识,解答了前面的一点疑惑。这一节我们将接着从宏观上分析cgo实现中使用到的一些关键技术。而对于其中一些细节部分将留到下一节具体分析。 3 | 4 | 整个cgo的实现依赖于几个部分,依赖于cgo命令生成桩文件,依赖于6c和6g对Go这一端的代码进行编译,依赖gcc对C那一端编译成动态链接库,同时,还依赖于运行时库实现Go和C互操作的一些支持。 5 | 6 | cgo命令会生成一些桩文件,这些桩文件是给6c和6g命令使用的,它们是Go和C调用之间的桥梁。原始的C文件会使用gcc编译成动态链接库的形式使用。 7 | 8 | ## cgo命令 9 | 10 | gc编译器在编译源文件时,如果识别出go源文件中的 11 | 12 | import "C" 13 | 14 | 字段,就会先调用cgo命令。cgo提取出相应的C函数接口部分,生成桩文件。比如我们写一个go文件test.go,内容如下: 15 | 16 | ```c 17 | package main 18 | 19 | /* 20 | #include "stdio.h" 21 | 22 | void test(int n) { 23 | char dummy[10240]; 24 | 25 | printf("in c test func iterator %d\n", n); 26 | if(n <= 0) { 27 | return; 28 | } 29 | dummy[n] = '\a'; 30 | test(n-1); 31 | } 32 | #cgo CFLAGS: -g 33 | */ 34 | import "C" 35 | 36 | func main() { 37 | C.test(C.int(2)) 38 | } 39 | ``` 40 | 41 | 对它执行cgo命令: 42 | 43 | go tool cgo test.go 44 | 45 | 在当前目录下会生成一个_obj的文件夹,文件夹里会包含下列文件: 46 | 47 | . 48 | ├── _cgo_.o 49 | ├── _cgo_defun.c 50 | ├── _cgo_export.c 51 | ├── _cgo_export.h 52 | ├── _cgo_flags 53 | ├── _cgo_gotypes.go 54 | ├── _cgo_main.c 55 | ├── test.cgo1.go 56 | └── test.cgo2.c 57 | 58 | ## 桩文件 59 | cgo生成了很多文件,其中大多数作用都是包装现有的函数,或者进行声明。比如在test.cgo2.c中,它生成了一个函数来包装test函数: 60 | 61 | ```c 62 | void 63 | _cgo_1b9ecf7f7656_Cfunc_test(void *v) 64 | { 65 | struct { 66 | int p0; 67 | char __pad4[4]; 68 | } __attribute__((__packed__)) *a = v; 69 | test(a->p0); 70 | } 71 | ``` 72 | 73 | 在_cgo_defun.c中是封装另一个函数来调用它: 74 | 75 | ```c 76 | void 77 | ·_Cfunc_test(struct{uint8 x[8];}p) 78 | { 79 | runtime·cgocall(_cgo_1b9ecf7f7656_Cfunc_test, &p); 80 | } 81 | ``` 82 | 83 | test.cgo1.go文件中包含一个main函数,它调用封装后的函数: 84 | 85 | ```c 86 | func main() { 87 | _Cfunc_test(_Ctype_int(2)) 88 | } 89 | ``` 90 | 91 | cgo做这些封装原因来自两方面,一方面是Go运行时调用cgo代码时要做特殊处理,比如runtime.cgocall。另一方面是由于Go和C使用的命名空间不一样,需要加一层转换,像·_Cfunc_test中的·字符是Go使用的命令空间区分,而在C这边使用的是_cgo_1b9ecf7f7656_Cfunc_test。 92 | 93 | cgo会识别任意的C.xxx关键字,使用gcc来找到xxx的定义。C中的算术类型会被转换为精确大小的Go的算术类型。C的结构体会被转换为Go结构体,对其中每个域进行转换。无法表示的域将会用byte数组代替。C的union会被转换成一个结构体,这个结构体中包含第一个union成员,然后可能还会有一些填充。C的数组被转换成Go的数组,C指针转换为Go指针。C的函数指针会被转换为Go中的uinptr。C中的void指针转换为Go的unsafe.Pointer。所有出现的C.xxx类型会被转换为_C_xxx。 94 | 95 | 如果xxx是数据,那么cgo会让C.xxx引用那个C变量(先做上面的转换)。为此,cgo必须引入一个Go变量指向C变量,链接器会生成初始化指针的代码。例如,gmp库中: 96 | 97 | mpz_t zero; 98 | 99 | cgo会引入一个变量引用C.zero: 100 | 101 | var _C_zero *C.mpz_t 102 | 103 | 然后将所有引用C.zero的实例替换为(*_C_zero)。 104 | 105 | cgo转换中最重要的部分是函数。如果xxx是一个C函数,那么cgo会重写C.xxx为一个新的函数_C_xxx,这个函数会在一个标准pthread中调用C的xxx。这个新的函数还负责进行参数转换,转换输入参数,调用xxx,然后转换返回值。 106 | 107 | 参数转换和返回值转换与前面的规则是一致的,除了数组。数组在C中是隐式地转换为指针的,而在Go中要显式地将数组转换为指针。 108 | 109 | 处理垃圾回收是个大问题。如果是Go中引用了C的指针,不再使用时进行释放,这个很容易。麻烦的是C中使用了Go的指针,但是Go的垃圾回收并不知道,这样就会很麻烦。 110 | 111 | ## 运行时库部分 112 | 运行时库会对cgo调用做一些处理,就像前面说过的,执行C函数之前会运行runtime.entersyscall,而C函数执行完返回后会调用runtime.exitsyscall。让cgo的运行仿佛是在另一个pthread中执行的,然后函数执行完毕后将返回值转换成Go的值。 113 | 114 | 比较难处理的情况是,在cgo调用的C函数中,发生了C回调Go函数的情况,这时处理起来会比较复杂。因为此时是没有Go运行环境的,所以必须再进行一次特殊处理,回到Go的goroutine中调用相应的Go函数代码,完成之后继续回到C的运行环境。看上去有点复杂,但是cgo对于在C中调用Go函数也是支持的。 115 | 116 | 从宏观上来讲cgo的关键技术就是这些,由cgo命令生成一些桩代码,负责C类型和Go类型之间的转换,命名空间处理以及特殊的调用方式处理。而运行时库部分则负责处理好C的运行环境,类似于给C代码一个非分段的栈空间并让它脱离与调度系统的交互。 117 | 118 | -------------------------------------------------------------------------------- /zh/09.3.md: -------------------------------------------------------------------------------- 1 | # 9.3 Go调用C 2 | 3 | 从这里开始,将深入挖掘关于运行时库部分对于cgo的支持。还记得前面那个test.go吗?这里将继续以它为例子进行分析。 4 | 5 | 从Go中调用C的函数test,cgo生成的代码调用是runtime.cgocall(_cgo_Cfunc_test, frame): 6 | 7 | ```c 8 | void 9 | ·_Cfunc_test(struct{uint8 x[8];}p) 10 | { 11 | runtime·cgocall(_cgo_1b9ecf7f7656_Cfunc_test, &p); 12 | } 13 | ``` 14 | 15 | 其中cgocall的第一个参数_cgo_Cfunc_test是一个由cgo生成并由gcc编译的函数: 16 | 17 | ```c 18 | void 19 | _cgo_1b9ecf7f7656_Cfunc_test(void *v) 20 | { 21 | struct { 22 | int p0; 23 | char __pad4[4]; 24 | } __attribute__((__packed__)) *a = v; 25 | test(a->p0); 26 | } 27 | ``` 28 | 29 | runtime.cgocall将g锁定到m,调用entersyscall,这样不会阻塞其它的goroutine或者垃圾回收,然后调用runtime.asmcgocall(_cgo_Cfunc_test, frame)。 30 | 31 | ```c 32 | void 33 | runtime·cgocall(void (*fn)(void*), void *arg) 34 | { 35 | runtime·lockOSThread(); 36 | runtime·entersyscall(); 37 | runtime·asmcgocall(fn, arg); 38 | runtime·exitsyscall(); 39 | 40 | endcgo(); 41 | } 42 | ``` 43 | 44 | 将g锁定到m是保证如果在cgo内又回调了Go代码,切换回来时还是在同一个栈中的。关于C调用Go,具体到下一节再分析。 45 | 46 | runtime.entersyscall宣布代码进入了系统调用,这样调度器知道在我们运行外部代码,于是它可以创建一个新的M来运行goroutine。调用asmcgocall是不会分裂栈并且不会分配内存的,因此可以安全地在"syscall call"时调用,不用考虑GOMAXPROCS计数。 47 | 48 | runtime.asmcgocall是用汇编实现的,它会切换到m的g0栈,然后调用_cgo_Cfunc_test函数。由于m的g0栈不是分段栈,因此切换到m->g0栈(这个栈是操作系统分配的栈)后,可以安全地运行gcc编译的代码以及执行_cgo_Cfunc_test(frame)函数。 49 | 50 | _cgo_Cfunc_test使用从frame结构体中取得的参数调用实际的C函数test,将结果记录在frame中,然后返回到runtime.asmcgocall。 51 | 52 | 重获控制权之后,runtime.asmcgocall切回之前的g(m->curg)的栈,并且返回到runtime.cgocall。 53 | 54 | 当runtime.cgocall重获控制权之后,它调用exitsyscall,然后将g从m中解锁。exitsyscall后m会阻塞直到它可以运行Go代码而不违反$GOMAXPROCS限制。 55 | 56 | 以上就是Go调用C时,运行时库方面所做的事情,是不是很简单呢?因为总结起来就两点,第一点是runtime.entersyscall,让cgo产生的外部代码脱离goroutine调度系统。第二点就是切换m的g0栈,这样就不必担忧分段栈方面的问题。 57 | 58 | 前面讲到m的g0栈时,留了个疑问的。那就是新建M的函数newm只给m的g0栈分配了8K内存,好像并不是一个“无穷”的栈,怎么回事呢?这里回答这个问题......不过我会再额外提两个新问题,希望读者跟着思考(好贱哦,哈哈)。 59 | 60 | 其实m的g0栈的大小并不在调用newm时分配的8K。在newm函数的最后一步是调用runtime·newosproc,这个函数会调用到操作系统的系统调用,分配一条系统线程。并且做了一个后处理过程--它将m的g0栈指针改掉了!m的g0栈指针会被重新设置为线程的栈,所以前面说m的g0栈是一个“无穷”的栈是正确的,那个分配8K内存的地方只是一个烟雾弹迷惑人的。 61 | 62 | 好吧,提两个疑问结束这一节内容: 63 | 1. m的g0栈对于每个m是有一个的,cgo调用会切换到这个栈中进行。那么,如果有多次cgo调用同时发生,共用同一个m的栈岂不会冲突?怎么处理? 64 | 2. 这一节只分配到了Go调用C,那么如果Go调用C的代码中,又回调了Go函数,这时系统是如何处理的? -------------------------------------------------------------------------------- /zh/09.4.md: -------------------------------------------------------------------------------- 1 | # 9.4 C调用Go 2 | 3 | cgo不仅仅支持从Go调用C,它还同样支持从C中调用Go的函数,虽然这种情况相对前者较少使用。 4 | 5 | ```go 6 | //export GoF 7 | func GoF(arg1, arg2 int, arg3 string) int64 { 8 | } 9 | ``` 10 | 11 | 使用export标记可以将Go函数导出提供给C调用: 12 | 13 | ```c 14 | extern int64 GoF(int arg1, int arg2, GoString arg3); 15 | ``` 16 | 17 | 下面让我们看看它是如何实现的。假定上面的函数GoF是在Go语言的一个包p内的,为了能够让gcc编译的C代码调用Go的函数p.GoF,cgo生成下面一个函数: 18 | 19 | ```c 20 | GoInt64 GoF(GoInt p0, GoInt p1, GoString p2) 21 | { 22 | struct { 23 | GoInt p0; 24 | GoInt p1; 25 | GoString p2; 26 | GoInt64 r0; 27 | } __attribute__((packed)) a; 28 | a.p0 = p0; 29 | a.p1 = p1; 30 | a.p2 = p2; 31 | crosscall2(_cgoexp_95935062f5b1_GoF, &a, 40); 32 | return a.r0; 33 | } 34 | ``` 35 | 36 | 这个函数由cgo生成,提供给gcc编译。函数名不是p.GoF,因为gcc没有包的概念。由gcc编译的C函数可以调用这个GoF函数。 37 | 38 | GoF调用crosscall2(_cgoexp_GoF, frame, framesize)。crosscall2是用汇编代码实现的,它是一个两参数的适配器,作用是从gcc函数调用6c函数(6c和gcc使用的调用协议还是有些区别的)。crosscall2实现了从一个ABI的gcc函数调用,到6c的函数调用ABI。所以上面代码中实际上相当于调用_cgoexp_GoF(frame,framesize)。注意此时是仍然运行在mg的g0栈并且不受GOMAXPROCS限制的。因此,这个代码不能直接调用任意的Go代码并且不能分配内存或者用尽m->g0的栈。 39 | 40 | _cgoexp_GoF调用runtime.cgocallback(p.GoF, frame, framesize): 41 | 42 | ```go 43 | #pragma textflag 7 44 | void 45 | _cgoexp_95935062f5b1_GoF(void *a, int32 n) 46 | { 47 | runtime·cgocallback(·GoF, a, n); 48 | } 49 | ``` 50 | 51 | 这个函数是由6c编译的,而不是gcc,因此可以引用到比如runtime.cgocallback和p.GoF这种名字。 52 | 53 | runtime·cgocallback也是一个用汇编实现的函数。它从m->g0的栈切换回原来的goroutine的栈,并在这个栈中调用runtime.cgocallbackg(p.GoF, frame, framesize)。 54 | 55 | 这中间会涉及到一些保存栈寄存器之类的细节操作比较复杂。因为这个过程相当于我们接管了m->curg的执行,但是却并没有完全恢复到之前的运行环境(只是借m->curg这个goroutine运行Go代码),所以我们需要保存当前环境到以便之后再次返回到m->g0栈。 56 | 57 | 好了,runtime.cgocallbackg现在是运行在一个真实的goroutine栈中(不是m->g0栈)。不过现在我们只是切换到了goroutine栈,此刻还是处于syscall状态的。因此这个函数会先调用runtime.exitsyscall,接着才是执行Go代码。当它调用runtime.exitsyscall,这会阻塞这条goroutine直到满足$GOMAXPROCS限制条件。一旦从exitsyscall返回,则可以安全地执行像调用内存分配或者是调用Go的回调函数p.GoF。 58 | 59 | ```go 60 | void 61 | runtime·cgocallbackg(FuncVal *fn, void *arg, uintptr argsize) 62 | { 63 | runtime·exitsyscall(); // coming out of cgo call 64 | // Invoke callback. 65 | reflect·call(fn, arg, argsize); 66 | runtime·entersyscall(); // going back to cgo call 67 | } 68 | ``` 69 | 70 | 后面的过程就不用分析了,跟前面的过程是一个正好相反的过程。在runtime.cgocallback重获控制权之后,它切换回m->g0栈,从栈中恢复之前的m->g0.sched.sp值,然后返回到_cgoexp_GoF。_cgoexp_GoF立即返回到crosscall2,它会恢复被调者为gcc保存的寄存器并返回到GoF,最后返回到C的调用函数中。 71 | 72 | ## 小结 73 | 无论是Go调用C,还是C调用Go,其需要解决的核心问题其实都是提供一个C/Go的运行环境来执行相应的代码。Go的代码执行环境就是goroutine以及Go的runtime,而C的执行环境需要一个不使用分段的栈,并且执行C代码的goroutine需要暂时地脱离调度器的管理。要达到这些要求,运行时提供的支持就是切换栈,以及runtime.entersyscall。 74 | 75 | 在Go中调用C函数时,runtime.cgocall中调用entersyscall脱离调度器管理。runtime.asmcgocall切换到m的g0栈,于是得到C的运行环境。 76 | 77 | 在C中调用Go函数时,crosscall2解决gcc编译到6c编译之间的调用协议问题。cgocallback切换回goroutine栈。runtime.cgocallbackg中调用exitsyscall恢复Go的运行环境。 78 | -------------------------------------------------------------------------------- /zh/10.0.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/zh/10.0.md -------------------------------------------------------------------------------- /zh/10.1.md: -------------------------------------------------------------------------------- 1 | # 10.1 内存模型 2 | 3 | 内存模型是非常重要的,理解Go的内存模型会就可以明白很多奇怪的竞态条件问题,"The Go Memory Model"的原文在[这里](http://golang.org/ref/mem),读个四五遍也不算多。 4 | 5 | 这里并不是要翻译这篇文章,英文原文是精确的,但读起来却很晦涩,尤其是happens-before的概念本身就是不好理解的,很容易跟时序问题混淆。大多数读者第一遍读Go的内存模型时基本上看不懂它在说什么。所以我要做的事情用不怎么精确但相对通俗的语言解释一下。 6 | 7 | 先用一句话总结,Go的内存模型描述的是"在一个groutine中对变量进行读操作能够侦测到在其他goroutine中对该变量的写操作"的条件。 8 | 9 | ## 内存模型相关bug一例 10 | 11 | 为了证明这个重要性,先看一个例子。下面一小段代码: 12 | 13 | ```Go 14 | package main 15 | 16 | import ( 17 | "sync" 18 | "time" 19 | ) 20 | 21 | func main() { 22 | var wg sync.WaitGroup 23 | var count int 24 | var ch = make(chan bool, 1) 25 | for i := 0; i < 10; i++ { 26 | wg.Add(1) 27 | go func() { 28 | ch <- true 29 | count++ 30 | time.Sleep(time.Millisecond) 31 | count-- 32 | <-ch 33 | wg.Done() 34 | }() 35 | } 36 | wg.Wait() 37 | } 38 | ``` 39 | 40 | 以上代码有没有什么问题?这里把buffered channel作为semaphore来使用,表面上看最多允许一个goroutine对count进行++和--,但其实这里是有bug的。根据Go语言的内存模型,对count变量的访问并没有形成临界区。编译时开启竞态检测可以看到这段代码有问题: 41 | 42 | ```bash 43 | go run -race test.go 44 | ``` 45 | 46 | 编译器可以检测到16和18行是存在竞态条件的,也就是count并没像我们想要的那样在临界区执行。继续往下看,读完这一节,回头再来看就可以明白为什么这里有bug了。 47 | 48 | ## happens-before 49 | 50 | happens-before是一个术语,并不仅仅是Go语言才有的。简单的说,通常的定义如下: 51 | 52 | 假设A和B表示一个多线程的程序执行的两个操作。如果A happens-before B,那么A操作对内存的影响 将对执行B的线程(且执行B之前)可见。 53 | 54 | 无论使用哪种编程语言,有一点是相同的:如果操作A和B在相同的线程中执行,并且A操作的声明在B之前,那么A happens-before B。 55 | 56 | ```c 57 | int A, B; 58 | void foo() 59 | { 60 | // This store to A ... 61 | A = 5; 62 | // ... effectively becomes visible before the following loads. Duh! 63 | B = A * A; 64 | } 65 | ``` 66 | 67 | 还有一点是,在每门语言中,无论你使用那种方式获得,happens-before关系都是可传递的:如果A happens-before B,同时B happens-before C,那么A happens-before C。当这些关系发生在不同的线程中,传递性将变得非常有用。 68 | 69 | 刚接触这个术语的人总是容易误解,这里必须澄清的是,happens-before并不是指时序关系,并不是说A happens-before B就表示操作A在操作B之前发生。它就是一个术语,就像光年不是时间单位一样。具体地说: 70 | 71 | 1. A happens-before B并不意味着A在B之前发生。 72 | 2. A在B之前发生并不意味着A happens-before B。 73 | 74 | 这两个陈述看似矛盾,其实并不是。如果你觉得很困惑,可以多读几篇它的定义。后面我会试着解释这点。记住,happens-before 是一系列语言规范中定义的操作间的关系。它和时间的概念独立。这和我们通常说”A在B之前发生”时表达的真实世界中事件的时间顺序不同。 75 | 76 | ### A happens-before B并不意味着A在B之前发生 77 | 78 | 这里有个例子,其中的操作具有happens-before关系,但是实际上并不一定是按照那个顺序发生的。下面的代码执行了(1)对A的赋值,紧接着是(2)对B的赋值。 79 | 80 | ```c 81 | int A = 0; 82 | int B = 0; 83 | void main() 84 | { 85 | A = B + 1; // (1) 86 | B = 1; // (2) 87 | } 88 | ``` 89 | 根据前面说明的规则,(1) happens-before (2)。但是,如果我们使用gcc -O2编译这个代码,编译器将产生一些指令重排序。有可能执行顺序是这样子的: 90 | 91 | 将B的值取到寄存器 92 | 将B赋值为1 93 | 将寄存器值加1后赋值给A 94 | 95 | 也就是到第二条机器指令(对B的赋值)完成时,对A的赋值还没有完成。换句话说,(1)并没有在(2)之前发生! 96 | 97 | 那么,这里违反了happens-before关系了吗?让我们来分析下,根据定义,操作(1)对内存的影响必须在操作(2)执行之前对其可见。换句话说,对A的赋值必须有机会对B的赋值有影响. 98 | 99 | 但是在这个例子中,对A的赋值其实并没有对B的赋值有影响。即便(1)的影响真的可见,(2)的行为还是一样。所以,这并不能算是违背happens-before规则。 100 | 101 | ### A在B之前发生并不意味着A happens-before B 102 | 103 | 下面这个例子中,所有的操作按照指定的顺序发生,但是并能不构成happens-before 关系。假设一个线程调用pulishMessage,同时,另一个线程调用consumeMessage。 由于我们并行的操作共享变量,为了简单,我们假设所有对int类型的变量的操作都是原子的。 104 | 105 | ```c 106 | int isReady = 0; 107 | int answer = 0; 108 | void publishMessage() 109 | { 110 | answer = 42; // (1) 111 | isReady = 1; // (2) 112 | } 113 | void consumeMessage() 114 | { 115 | if (isReady) // (3) <-- Let's suppose this line reads 1 116 | printf("%d\n", answer); // (4) 117 | } 118 | ``` 119 | 120 | 根据程序的顺序,在(1)和(2)之间存在happens-before 关系,同时在(3)和(4)之间也存在happens-before关系。 121 | 122 | 除此之外,我们假设在运行时,isReady读到1(是由另一个线程在(2)中赋的值)。在这中情形下,我们可知(2)一定在(3)之前发生。但是这并不意味着在(2)和(3)之间存在happens-before 关系! 123 | 124 | happens-before 关系只在语言标准中定义的地方存在,这里并没有相关的规则说明(2)和(3)之间存在happens-before关系,即便(3)读到了(2)赋的值。 125 | 126 | 还有,由于(2)和(3)之间,(1)和(4)之间都不存在happens-before关系,那么(1)和(4)的内存交互也可能被重排序 (要不然来自编译器的指令重排序,要不然来自处理器自身的内存重排序)。那样的话,即使(3)读到1,(4)也会打印出“0“。 127 | 128 | ## Go关于同步的规则 129 | 130 | 我们回过头来再看看"The Go Memory Model"中关于happens-before的部分。 131 | 132 | 如果满足下面条件,对变量v的读操作r可以侦测到对变量v的写操作w: 133 | 134 | 1. r does not happen before w. 135 | 2. There is no other write w to v that happens after w but before r. 136 | 137 | 为了保证对变量v的读操作r可以侦测到某个对v的写操作w,必须确保w是r可以侦测到的唯一的写操作。也就是说当满足下面条件时可以保证读操作r能侦测到写操作w: 138 | 139 | 1. w happens-before r. 140 | 2. Any other write to the shared variable v either happens-before w or after r. 141 | 142 | 关于channel的happens-before在Go的内存模型中提到了三种情况: 143 | 144 | 1. 对一个channel的发送操作 happens-before 相应channel的接收操作完成 145 | 2. 关闭一个channel happens-before 从该Channel接收到最后的返回值0 146 | 3. 不带缓冲的channel的接收操作 happens-before 相应channel的发送操作完成 147 | 148 | 先看一个简单的例子: 149 | 150 | ```go 151 | var c = make(chan int, 10) 152 | var a string 153 | func f() { 154 | a = "hello, world" // (1) 155 | c <- 0 // (2) 156 | } 157 | func main() { 158 | go f() 159 | <-c // (3) 160 | print(a) // (4) 161 | } 162 | ``` 163 | 164 | 上述代码可以确保输出"hello, world",因为(1) happens-before (2),(4) happens-after (3),再根据上面的第一条规则(2)是 happens-before (3)的,最后根据happens-before的可传递性,于是有(1) happens-before (4),也就是a = "hello, world" happens-before print(a)。 165 | 166 | 再看另一个例子: 167 | 168 | ```go 169 | var c = make(chan int) 170 | var a string 171 | func f() { 172 | a = "hello, world" // (1) 173 | <-c // (2) 174 | } 175 | func main() { 176 | go f() 177 | c <- 0 // (3) 178 | print(a) // (4) 179 | } 180 | ``` 181 | 182 | 根据上面的第三条规则(2) happens-before (3),最终可以保证(1) happens-before (4)。 183 | 184 | 如果我把上面的代码稍微改一点点,将c变为一个带缓存的channel,则print(a)打印的结果不能够保证是"hello world"。 185 | 186 | ```go 187 | var c = make(chan int, 1) 188 | var a string 189 | func f() { 190 | a = "hello, world" // (1) 191 | <-c // (2) 192 | } 193 | func main() { 194 | go f() 195 | c <- 0 // (3) 196 | print(a) // (4) 197 | } 198 | ``` 199 | 200 | 因为这里不再有任何同步保证,使得(2) happens-before (3)。可以回头分析一下本节最前面的例子,也是没有保证happens-before条件。 201 | 202 | ## links 203 | * [目录]() 204 | * 上一节: [杂项] 205 | * 下一节: [pprof] 206 | -------------------------------------------------------------------------------- /zh/10.4.md: -------------------------------------------------------------------------------- 1 | # 10.4 系统调用 2 | 3 | 本节中我们将以系统调用为线索去观察Go的内部实现。 4 | 5 | 这里先补充一下操作系统提供系统调用的机制。应用层是无法访问最底层的硬件资源的,操作系统将硬件资源管理起来,提供给应用层。系统调用就是操作系统内核提供给应用层的唯一的访问方式,应用层告诉内核需要什么,由操作系统去执行,执行完成之后返回给应用层。 6 | 7 | 以darwin为例,系统调用是通过汇编指令int 0x80完成的。在调用这条指令之前,应用层会先设置好系统调用的参数,其中最重要的一个参数就是系统调用号。每个系统调用都有一个编号,内核通过这个编号来区别是哪一个系统调用。剩下的参数就是特定系统调用需要的参数。比如amd64下linux的read,write,open,close对应的系统调用编号分别是0,1,2,3。 8 | 9 | Go的syscall包中提供了很多的系统调用的函数封装,像Open,Exec,Socket等等,其实他们底层使用的都是一个类似Syscall的函数。这是一个汇编写的函数,实现依赖于具体的平台和机器,比如说下面是在syscall_darwin_386.s中的定义: 10 | 11 | ```asm 12 | TEXT ·Syscall(SB),NOSPLIT,$0-32 13 | CALL runtime·entersyscall(SB) 14 | MOVL 4(SP), AX // syscall entry 15 | // slide args down on top of system call number 16 | LEAL 8(SP), SI 17 | LEAL 4(SP), DI 18 | CLD 19 | MOVSL 20 | MOVSL 21 | MOVSL 22 | INT $0x80 23 | JAE ok 24 | MOVL $-1, 20(SP) // r1 25 | MOVL $-1, 24(SP) // r2 26 | MOVL AX, 28(SP) // errno 27 | CALL runtime·exitsyscall(SB) 28 | RET 29 | ok: 30 | MOVL AX, 20(SP) // r1 31 | MOVL DX, 24(SP) // r2 32 | MOVL $0, 28(SP) // errno 33 | CALL runtime·exitsyscall(SB) 34 | RET 35 | ``` 36 | 37 | 其中寄存器AX中存放的是系统调用号,设置好调用参数,然后执行INT $0x80指令进入系统调用,等待函数返回。在syscall包中还有跟Syscall很类似的函数RawSyscall,它们的区别就是RawSyscall中没有runtime.entersyscall和runtime.exitsyscall。那么,这两个函数是做什么的呢? 38 | 39 | 系统调用可以分为阻塞的和非阻塞的,像Getgid这种能立刻返回的就是非阻塞的,而默认情况下IO相关的系统调用基本上是阻塞的。非阻塞的系统调用函数是调用的RawSyscall,而阻塞的是调用的Syscall。关键点就在于runtime.entersyscall和runtime.exitsyscall这两个函数。Go为了最有效地利用CPU资源,不会让阻塞于系统调用的goroutine一直等待系统调用返回而白白浪费CPU。runtime·entersyscall函数就是将goroutine切换成Gsyscall状态,脱离调度,然后找一个其它的goroutine执行。 40 | 41 | entersyscall会将goroutine的sp和pc保存到g->sched中,然后将g->status设置为Gsyscall。 将m->mcache置为空,将当前的p从m中脱离,将p的状态设置为Psyscall。P和G是一起进入到syscall状态的,从M中脱离。这意味着什么呢?前面说过,M对应的是OS线程,P获得M后才能执行G。M不会被挂起也就是说OS线程是可以继续工作的。 42 | 43 | -------------------------------------------------------------------------------- /zh/10.7.md: -------------------------------------------------------------------------------- 1 | # 10.6 运行时符号信息 2 | 3 | 使用过Go语言都会注意到程序panic时会有traceback,不仅有函数调用,还有文件名和行号等信息,这个是如何做到的呢? 4 | 5 | 编译器在编译的时候会生成一些额外信息,运行时符号信息就是这样生成的。以traceback信息中的行号为例,编译器在编译的时候会记录下函数地址对应的源文件行号,也就是`pc->line`的一张表,后面简称pcln,其中pc是program counter的缩写。 6 | 7 | 如果将pcln的概念推广,其它不仅仅可以记录行号,还可以记录下许多其它信息。比如pcsp表,记录栈的大小;或者记录下栈内的存活的变量信息,这样可以方便垃圾回收的处理。将pcln概念进行推广,是一个pc-value的表,其中value可以是文件名或行号或其它任何信息。 8 | 9 | 每个函数都可以拥有一些元数据和PC-Value表。运行时符号信息由编译器在编译的时候生成,存放在可执行文件中,当程序被执行时,这张表被加载到内存,用于程序运行时辅助Go的运行时库执行一些处理。 10 | 11 | 一个函数符号表的形式就是一张program counter的查找表,形式类式于: 12 | 13 | N pc0 func0 pc1 func1 pc2 func2 ... pc(N-1) func(N-1) pcN 14 | 15 | 表中包含记录项数信息N,接下来表的每一项,由一个pc和一个函数元数据指针构成。给定一个pc值,通过二分搜索可以很快地查找到对应的值。每个funcN值是它在函数符号表的偏移值,函数符号表是一个如下结构体的数组: 16 | 17 | struct Func 18 | { 19 | uintptr entry; // start pc 20 | int32 name; // name (offset to C string) 21 | int32 args; // size of arguments passed to function 22 | int32 frame; // size of function frame, including saved caller PC 23 | int32 pcsp; // pcsp table (offset to pcvalue table) 24 | int32 pcfile; // pcfile table (offset to pcvalue table) 25 | int32 pcln; // pcln table (offset to pcvalue table) 26 | int32 nfuncdata; // number of entries in funcdata list 27 | int32 npcdata; // number of entries in pcdata list 28 | }; 29 | 30 | 每个函数元数据中,存储了函数的入口地址,pc-value表包含了当前栈指针偏移,文件信息,行号信息。 31 | 32 | 除了每个函数的元数据,还有一个每个函数的元数据指针以及每个函数的pc-value表。Go汇编中使用了一些伪指令: 33 | 34 | FUNCDATA $2, $sym(SB) 35 | 36 | 表明funcdata中的第2项是一个指向sym的指针。类似地 37 | 38 | PCDATA $3, $45 39 | 40 | 表明当前program counter偏移3关联的值是45。 41 | 42 | 在运行时,运行时库可以通过Func,给定一个偏移来检索funcdata,或者通过program counter,给定一个偏移来检索pcdata。 43 | 44 | 在内存中,结构体Func紧接着就是pcdata表和funcdata表。 45 | 46 | ## links 47 | * [目录]() 48 | * 上一节: [timer] 49 | * 下一节: [signal处理] -------------------------------------------------------------------------------- /zh/README.md: -------------------------------------------------------------------------------- 1 | # 《深入解析Go》 2 | 3 | 因为自己对Go底层的东西比较感兴趣,所以抽空在写一本开源的书籍《深入解析Go》。写这本书不表示我能力很强,而是我愿意分享,和大家一起分享对Go语言的内部实现的一些研究。 4 | 5 | 我一直认为知识是用来分享的,让更多的人分享自己拥有的一切知识这个才是人生最大的快乐。 6 | 7 | 这本书目前我放在Github上,时间有限、能力有限,所以希望更多的朋友参与到这个开源项目中来。 8 | -------------------------------------------------------------------------------- /zh/SUMMARY.md: -------------------------------------------------------------------------------- 1 | * [如何研究Go内部实现](01.0.md) 2 | * [从源代码安装Go](01.1.md) 3 | * [本书的组织结构](01.2.md) 4 | * [基本技巧](01.3.md) 5 | * [基本数据结构](02.0.md) 6 | * [基本类型](02.1.md) 7 | * [slice](02.2.md) 8 | * [map的实现](02.3.md) 9 | * [nil](02.4.md) 10 | * [函数调用协议](03.0.md) 11 | * [Go调用汇编和C](03.1.md) 12 | * [多值返回](03.2.md) 13 | * [go关键字](03.3.md) 14 | * [defer关键字](03.4.md) 15 | * [连续栈](03.5.md) 16 | * [闭包的实现](03.6.md) 17 | * [Go语言程序初始化过程](04.0.md) 18 | * [系统初始化](04.1.md) 19 | * [main.main之前的准备](04.2.md) 20 | * [goroutine调度](05.0.md) 21 | * [调度器相关数据结构](05.1.md) 22 | * [goroutine的生老病死](05.2.md) 23 | * [设计与演化](05.3.md) 24 | * [死锁检测和竞态检测] 25 | * [抢占式调度](05.5.md) 26 | * [内存管理](06.0.md) 27 | * [内存池](06.1.md) 28 | * [垃圾回收上篇](06.2.md) 29 | * [垃圾回收下篇](06.3.md) 30 | * [高级数据结构的实现](07.0.md) 31 | * [channel](07.1.md) 32 | * [interface](07.2.md) 33 | * [方法调用](07.3.md) 34 | * [网络](08.0.md) 35 | * [非阻塞io](08.1.md) 36 | * [net包] 37 | * [cgo](09.0.md) 38 | * [预备知识](09.1.md) 39 | * [cgo关键技术](09.2.md) 40 | * [Go调用C](09.3.md) 41 | * [C调用Go](09.4.md) 42 | * [杂项] 43 | * [内存模型](10.1.md) 44 | * [pprof] 45 | * [底层同步机制] 46 | * [系统调用] 47 | * [timer] 48 | * [运行时符号信息] 49 | * [signal处理] 50 | -------------------------------------------------------------------------------- /zh/images/1.1.mac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/zh/images/1.1.mac.png -------------------------------------------------------------------------------- /zh/images/2.2.map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/zh/images/2.2.map.png -------------------------------------------------------------------------------- /zh/images/3.2.funcall.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/zh/images/3.2.funcall.jpg -------------------------------------------------------------------------------- /zh/images/5.2.goroutine_state.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/zh/images/5.2.goroutine_state.jpg -------------------------------------------------------------------------------- /zh/images/5.3.m_g.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/zh/images/5.3.m_g.jpg -------------------------------------------------------------------------------- /zh/images/5.3.m_g_p.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/zh/images/5.3.m_g_p.jpg -------------------------------------------------------------------------------- /zh/images/5.3.scheduler.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/zh/images/5.3.scheduler.jpg -------------------------------------------------------------------------------- /zh/images/5.3.steal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/zh/images/5.3.steal.jpg -------------------------------------------------------------------------------- /zh/images/5.3.syscall.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/zh/images/5.3.syscall.jpg -------------------------------------------------------------------------------- /zh/images/5.3.worker.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/zh/images/5.3.worker.jpg -------------------------------------------------------------------------------- /zh/images/6.1.mcentral.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/zh/images/6.1.mcentral.jpg -------------------------------------------------------------------------------- /zh/images/6.1.mheap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/zh/images/6.1.mheap.jpg -------------------------------------------------------------------------------- /zh/images/6.2.gc_bitmap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/zh/images/6.2.gc_bitmap.jpg -------------------------------------------------------------------------------- /zh/images/7.1.channel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/zh/images/7.1.channel.png -------------------------------------------------------------------------------- /zh/images/epoll.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiancaiamao/go-internals/74221306b0e3111ab15768da44b32b11e3051d19/zh/images/epoll.jpg -------------------------------------------------------------------------------- /zh/preface.md: -------------------------------------------------------------------------------- 1 | * 1.[如何研究Go内部实现](01.0.md) 2 | - 1.1. [从源代码安装Go](01.1.md) 3 | - 1.2. [本书的组织结构](01.2.md) 4 | - 1.3. [基本技巧](01.3.md) 5 | * 2.[基本数据结构](02.0.md) 6 | - 2.1. [基本类型](02.1.md) 7 | - 2.2. [slice](02.2.md) 8 | - 2.3. [map的实现](02.3.md) 9 | - 2.4. [nil](02.4.md) 10 | * 3.[函数调用协议](03.0.md) 11 | - 3.1. [Go调用汇编和C](03.1.md) 12 | - 3.2. [多值返回](03.2.md) 13 | - 3.3. [go关键字](03.3.md) 14 | - 3.4. [defer关键字](03.4.md) 15 | - 3.5. [连续栈](03.5.md) 16 | - 3.6. [闭包的实现](03.6.md) 17 | * 4.[Go语言程序初始化过程](04.0.md) 18 | - 4.1 [系统初始化](04.1.md) 19 | - 4.2 [main.main之前的准备](04.2.md) 20 | * 5.[goroutine调度](05.0.md) 21 | - 5.1 [调度器相关数据结构](05.1.md) 22 | - 5.2 [goroutine的生老病死](05.2.md) 23 | - 5.3 [设计与演化](05.3.md) 24 | - 5.4 [死锁检测和竞态检测] 25 | - 5.5 [抢占式调度](05.5.md) 26 | * 6.[内存管理](06.0.md) 27 | - 6.1. [内存池](06.1.md) 28 | - 6.2. [垃圾回收上篇](06.2.md) 29 | - 6.3. [垃圾回收下篇](06.3.md) 30 | * 7.[高级数据结构的实现] 31 | - 7.1. [channel](07.1.md) 32 | - 7.2. [interface](07.2.md) 33 | - 7.3. [方法调用](07.3.md) 34 | * 8.[网络](08.0.md) 35 | - 8.1. [非阻塞io](08.1.md) 36 | - 8.2. [net包] 37 | * 9.[cgo](09.0.md) 38 | - 9.1. [预备知识](09.1.md) 39 | - 9.2. [cgo关键技术](09.2.md) 40 | - 9.3. [Go调用C](09.3.md) 41 | - 9.4. [C调用Go](09.4.md) 42 | * 10.[杂项] 43 | - 10.1. [内存模型](10.1.md) 44 | - 10.2. [pprof] 45 | - 10.3. [底层同步机制] 46 | - 10.4. [系统调用] 47 | - 10.5. [timer] 48 | - 10.6. [运行时符号信息](10.7.md) 49 | - 10.7. [signal处理] 50 | 51 | * 附录A [参考资料](ref.md) 52 | * 附录B [Go的源代码目录结构](ref2.md) 53 | * 附寻C [Go是如何编译它自己的](ref3.md) 54 | -------------------------------------------------------------------------------- /zh/ref.md: -------------------------------------------------------------------------------- 1 | # 附录A 参考资料 2 | 3 | 这本书的内容基本上是我对Go1.1源代码的研究,以及网上找到的一些资料的整理。参考资料如下: 4 | 5 | 1. [Russ cox的博客](http://research.swtch.com) 6 | 2. [wiki](http://code.google.com/p/try-catch-finally/wiki/GoInternals) 7 | 3. [调度器](http://www.douban.com/note/251142022/) 8 | 4. [tcmalloc内存管理](http://shiningray.cn/tcmalloc-thread-caching-malloc.html) 9 | 5. [Tw's Blog](http://totorow.herokuapp.com) 10 | 6. [刑星的博客](http://www.mikespook.com/) 11 | 7. [探究GO中各个目录的功能](http://blog.studygolang.com/2012/12/%e6%8e%a2%e7%a9%b6go%e4%b8%ad%e5%90%84%e4%b8%aa%e7%9b%ae%e5%bd%95%e7%9a%84%e5%8a%9f%e8%83%bd/) 12 | 8. [jra’s thoughts](http://blog.nella.org/?p=849) 13 | 9. [Morsing's Blog](http://morsmachine.dk/go-scheduler) 14 | 10. [我的迦南地](http://blog.sina.com.cn/u/2615392497) 15 | 11. [Analysis of the Go runtime scheduler](http://www.cs.columbia.edu/~aho/cs6998/reports/12-12-11_DeshpandeSponslerWeiss_GO.pdf) 16 | 12. [Go 1.2 Runtime Symbol Information](https://docs.google.com/document/d/1lyPIbmsYbXnpNj57a261hgOYVpNRcgydurVQIyZOz_o/pub) 17 | 13. [goroutine与调度器](http://www.bigendian123.com/) 18 | 14. [Closures in Go](http://derkthedaring.com/closures-in-go) 19 | 15. [Golang Internals, Part 4: Object Files and Function Metadata](http://blog.altoros.com/golang-part-4-object-files-and-function-metadata.html) 20 | -------------------------------------------------------------------------------- /zh/ref2.md: -------------------------------------------------------------------------------- 1 | # 附录B Go的源代码目录结构 2 | 下载Go源码后,根目录结构如下: 3 | 4 | |– AUTHORS — 文件,官方 Go语言作者列表 5 | |– CONTRIBUTORS — 文件,第三方贡献者列表 6 | |– LICENSE — 文件,Go语言发布授权协议 7 | |– PATENTS — 文件,专利 8 | |– README — 文件,README文件,大家懂的。提一下,经常有人说:Go官网打不开啊,怎么办?其实,在README中说到了这个。该文件还提到,如果通过二进制安装,需要设置GOROOT环境变量;如果你将Go放在了/usr/local/go中,则可以不设置该环境变量(Windows下是C:\go)。当然,建议不管什么时候都设置GOROOT。另外,确保$GOROOT/bin在PATH目录中。 9 | |– VERSION — 文件,当前Go版本 10 | |– api — 目录,包含所有API列表,方便IDE使用 11 | |– doc — 目录,Go语言的各种文档,官网上有的,这里基本会有,这也就是为什么说可以本地搭建”官网”。这里面有不少其他资源,比如gopher图标之类的。 12 | |– favicon.ico — 文件,官网logo 13 | |– include — 目录,Go 基本工具依赖的库的头文件 14 | |– lib — 目录,文档模板 15 | |– misc — 目录,其他的一些工具,相当于大杂烩,大部分是各种编辑器的Go语言支持,还有cgo的例子等 16 | |– robots.txt — 文件,搜索引擎robots文件 17 | |– src — 目录,Go语言源码:基本工具(编译器等)、标准库 18 | `– test — 目录,包含很多测试程序(并非_test.go方式的单元测试,而是包含main包的测试),包括一些fixbug测试。可以通过这个学到一些特性的使用。 19 | 20 | 下面详细介绍一些目录(可能分功能介绍) 21 | 22 | ## 一、api目录 23 | 24 | |– README 25 | |– go1.txt 26 | `– next.txt 27 | 28 | 通过阅读README知道,go1.txt可以通过go tool api命令生成。而通过go1.txt可以做成编辑器的api自动提示,比如Vim:VimForGo 29 | next.txt是一些将来可能加入的API 30 | 31 | ## 二、Go基本工具(cmd) 32 | 33 | ### 1、include目录 34 | 该目录包含以下文件(文件夹) 35 | 36 | ar.h bio.h bootexec.h fmt.h libc.h mach.h plan9 u.h ureg_amd64.h ureg_arm.h ureg_x86.h utf.h 37 | 38 | 其中,plan9目录是针对Plan 9操作系统的,从里面的文件名知道,跟include跟目录下的是一个意思。 39 | 40 | 386 libc.h mach.h ureg_amd64.h ureg_arm.h ureg_x86.h (386目录下就只有一个u.h头文件) 41 | 42 | 1)u.h 43 | 根据Rob Pike在How to Use the Plan 9 C Compiler上的介绍以及文件的源码,知道u.h文件定义了一些依赖架构(architecture-dependent)的类型(这样使得该类型独立于架构,不过依赖于编译器),如用于setjmp系统调用的jmp_buf,以及类型int8、uint8等。 44 | 45 | 该文件直接来源于plan9。地址:[http://code.swtch.com/plan9port/src/tip/include/u.h](http://code.swtch.com/plan9port/src/tip/include/u.h)。所有plan9 C程序必须在开始出包含该头文件(因为其他文件引用了该文件中的类型定义) 46 | 47 | 2)ureg.h 48 | 包括:ureg_amd64.h ureg_arm.h ureg_x86.h 49 | 50 | 三种架构的定义。该文件来源于Inferno操作系统,相应的源码分别是:[http://code.google.com/p/inferno-os/source/browse/utils/libmach/ureg5/6/8.h](http://code.google.com/p/inferno-os/source/browse/utils/libmach/ureg5/6/8.h)。 51 | 52 | 该文件定义了一个类型(struct):Ureg。定义了在系统栈上寄存器的布局 53 | 54 | 在ureg_x86.h中对各个字段有注释: 55 | 56 | ```c 57 | struct Ureg 58 | { 59 | uint32 di; /* general registers */ 60 | uint32 si; /* ... */ 61 | uint32 bp; /* ... */ 62 | uint32 nsp; 63 | uint32 bx; /* ... */ 64 | uint32 dx; /* ... */ 65 | uint32 cx; /* ... */ 66 | uint32 ax; /* ... */ 67 | uint32 gs; /* data segments */ 68 | uint32 fs; /* ... */ 69 | uint32 es; /* ... */ 70 | uint32 ds; /* ... */ 71 | uint32 trap; /* trap type */ 72 | uint32 ecode; /* error code (or zero) */ 73 | uint32 pc; /* pc */ 74 | uint32 cs; /* old context */ 75 | uint32 flags; /* old flags */ 76 | union { 77 | uint32 usp; 78 | uint32 sp; 79 | }; 80 | uint32 ss; /* old stack segment */ 81 | }; 82 | ``` 83 | 84 | 3)libc.h/utf.h/fmt.h 85 | 在严格的标准C中,头文件按相关功能分组在一个单独文件中:一个头文件用于字符串处理,一个头文件用于内存管理,一个头文件用于I/O处理,没有头文件是用于系统调用的。plan9采用了不同的方式,一个C库由strings函数、内存操作函数、一些格式化IO程序,加上所有和这些相关的系统调用。为了使用这些功能,需要包含libc.h头文件。该文件从Inferno和Plan9中提取出来的。 86 | 87 | Inferno:http://code.google.com/p/inferno-os/source/browse/include/kern.h 88 | Plan 9:http://code.swtch.com/plan9port/src/tip/include/libc.h 89 | 90 | 该文件开头有几行注释: 91 | 92 | /* 93 | * Lib9 is miscellany from the Plan 9 C library that doesn’t 94 | * fit into libutf or into libfmt, but is still missing from traditional 95 | * Unix C libraries. 96 | */ 97 | 98 | 在该文件中包含了utf.h和fmt.h 99 | 100 | 在Plan 9中使用nil表示指针的零值,这也就是为什么Go中采用nil了。nil的定义在libc.h中: 101 | 102 | #ifndef nil 103 | #define nil ((void*)0) 104 | #endif 105 | 106 | 对于solaris,nil在u.h中有定义 107 | 108 | 另外,libc中声明了很多系统调用 109 | 110 | 包含了该文件之后,可以直接使用print、fprint之类的,而不需要包含标准IO库,这是因为libc.h中包含了fmt.h,而fmt.h中提供了这些print函数。当然,如果需要使用printf,得导入stdio.h 111 | 112 | utf.h是实际上引用了src/lib9/utf/utf.h,提供了对UNICODE字符集相关操作。 113 | 114 | 题外话: 115 | 116 | Plan 9支持的每一种CPU架构都给其一个单个字母或数字的名称:k表示SPARC,q表示Motorola Power PC 630和640,v表示MIPS,0表示little-endian MIPS,1表示Motorola 68000,2表示Motorola 68020和68040,5表示Acorn ARM 7500,6表示AMD64,7表示DEC Alpha,8表示Intel 386,9表示AMD 2900。可以看出,Go中5/6/8的由来了。 117 | 对于为什么取这样的名字,How to Use the Plan 9 C Compiler 中Heterogeneity有解释。 118 | 119 | 注意:在看源码过程中可能会看到 120 | 121 | ARGBEGIN{ 122 | }ARGEND 123 | 124 | 这是在libc.h中定义的宏。这是一些处理命令行参数的宏。其他宏还有: 125 | 126 | ARGF() 127 | EARGF(x) 128 | ARGC() 129 | 130 | 4)bio.h 131 | 上面提到,libc.h中包含了print等,这些IO是没有buffer的。而bio.h提供了buffer I/O,这是推荐使用的方式。这个和ANSI标准I/O,stdio.h类似。 132 | 133 | 根据官方说法,Bio更小、更高效,特别是buffer-at-a-time或line-at-a-time I/O,即使character-at-a-time I/O也比stdio更快。 134 | 135 | 和其他系统明显不同的是,Plan 9中I/O的接口的文本不是ASCII编码,而是UTF(ISO叫做UTF-8)编码。一个字符在Plan 9中称为rune,也叫做Code-point。(Go中沿用了该叫法) 136 | 137 | 看一下utf.h中的一个枚举类型 138 | 139 | { 140 | UTFmax = 4, /* maximum bytes per rune */ 141 | Runesync = 0x80, /* cannot represent part of a UTF sequence ( Runeself = 0x80, /* rune and UTF sequences are the same ( Runeerror = 0xFFFD, /* decoding error in UTF */ 142 | Runemax = 0x10FFFF, /* maximum rune value */ 143 | }; 144 | 145 | 引用一段解释: 146 | 147 | The library defines several symbols relevant to the representation of characters. Any byte with unsigned value less than Runesync will not appear in any multi-byte encoding of a character. Utfrune compares the character being searched against Runesync to see if it is sufficient to call strchr or if the byte stream must be interpreted. Any byte with unsigned value less than Runeself is represented by a single byte with the same value. Finally, when errors are encountered converting to runes from a byte stream, the library returns the rune value Runeerror and advances a single byte. This permits programs to find runes embedded in binary data. 148 | 149 | 关于UTF8的操作在utf.h文件中声明了 150 | 151 | 该文件来源于Inferno操作系统 152 | http://code.google.com/p/inferno-os/source/browse/include/bio.h 153 | 154 | 5)ar.h 155 | 该文件来源于Inferno操作系统 156 | http://code.google.com/p/inferno-os/source/browse/utils/include/ar.h 157 | 158 | iar是一个压缩命令,该压缩的文件格式通过ar.h头文件描述。 159 | 160 | 关于该文件的具体说明,可以查看: http://www.vitanuova.com/inferno/man/10/ar.html 161 | 162 | 6)bootexec.h/mach.h 163 | bootexec.h 文件来源于Inferno操作系统 164 | http://code.google.com/p/inferno-os/source/browse/utils/libmach/bootexec.h 165 | 但注释掉了一些东西 166 | 167 | 该文件定义了一些架构私有的引导执行程序的文件头格式(引导程序)。这是Plan 9(Inferno的祖先)操作系统的说明:The header format of a bootable executable is defined by each manufacturer. Header file /sys/include/bootexec.h contains structures describing the headers currently supported. 168 | 169 | mach.h文件来源于Inferno操作系统: 170 | http://code.google.com/p/inferno-os/source/browse/utils/libmach/a.out.h 171 | http://code.google.com/p/inferno-os/source/browse/utils/libmach/mach.h 172 | 173 | 该文件定义了一些特定架构的应用程序数据。目前支持的架构: 174 | 175 | /* 176 | * Supported architectures: 177 | * mips, 178 | * 68020, 179 | * i386, 180 | * amd64, 181 | * sparc, 182 | * sparc64, 183 | * mips2 (R4000) 184 | * arm 185 | * powerpc, 186 | * powerpc64 187 | * alpha 188 | */ 189 | 190 | 该文件中列出了详细的支持的架构类型(可执行文件) 191 | 192 | bootexec.h是针对引导程序的;mach.h是针对应用程序的。 193 | 194 | ### 2、src下的lib9/libbio/libmach 195 | 由include目录中文件的名字知道,这三个目录分别是libc.h、bio.h和mach.h三个头文件的实现。具体代码有兴趣可以看看。 196 | 197 | 这些都是Plan 9或Inferno操作系统的库 198 | 199 | ### 3、src/cmd 包含了各种工具的源码 200 | 目录如下: 201 | 202 | 5a 5c 5g 5l 6a 6c 6g 6l 8a 8c 8g 8l addr2line api cc cgo dist fix gc go godoc gofmt ld nm objdump pack vet yacc 203 | 204 | 一个目录对应一个工具 205 | 除了go/godoc/gofmt/dist,其他工具的使用方式: 206 | go tool 工具名 xxx 207 | 208 | 注:这些工具基本来自Plan 9上已有的工具。工具帮助文档:http://plan9.bell-labs.com/sys/man/1/INDEX.html 209 | 210 | 经过前面的介绍,看到这些名字,应该大概猜到是啥了。 211 | 212 | 我们看一下Plan 9中文件后缀的问题 213 | 214 | 前面我们知道,AMD64上,标示是6,我们以这个为例。 215 | 216 | 根据Plan 9命名规则,AMD64上的C编译器是6c,汇编器是6a,链接器或装载器是6l。c文件编译后生成的对象文件后缀是.6,链接后默认的可执行文件名是6.out。 217 | 218 | 5/6/8这一序列中,跟Plan 9是一致的,另外,新增了一个g,表示Go编译器。通过这些工具编译Go文件生成的中间文件对象的后缀和C文件编译后是一样的,以.5/6/8结尾。 219 | 220 | 说到这里提醒一下,目前Go编译不建议直接通过5g/6g/8g这样的进行,而是使用go这个工具(网上很多Go1正式版发布之前的文章用的是6g这样的工具) 221 | 222 | 5/6/8这一序列中,每个目录下的都有一个doc.go文件,这个文件大概说明了该工具的作用。这一序列工具具体的源码,感兴趣的可以阅读。 223 | 224 | 1)cc/gc/ld分别是C编译器、Go编译器和链接器 225 | 这三个可以看成是对5/6/8序列的抽象(不依赖具体架构) 226 | 227 | 2)api 可以生产所有Go包的API列表。 228 | GOROOT/api中的go1.txt就可以通过这个工具生产 229 | 230 | 3)cgo 允许通过调用C代码创建Go包 231 | 232 | 4)fix 找到用旧API写的Go程序,然后用新API重写他们。 233 | 这个可用于Go升级了,处理用之前版本Go写的应用程序。 234 | 235 | 5)go 管理Go源代码的工具,很好用很重要的一个工具。 236 | 应该总是使用go这个工具,而不是使用6g这样的工具。当然,如果需要生产中间对象,可以使用6g这样的工具。 237 | 238 | 6)godoc 提取并生产Go程序文档(包括Go本身) 239 | 7)gofmt 格式化Go程序代码 240 | 8)nm 是Plan 9中的nm工具 241 | 。详细说明:http://plan9.bell-labs.com/magic/man2html/1/nm 。查看符号表用的 242 | 243 | 9)pack 是Plan 9中的ar工具,这个用来归档目标文件。 244 | pkg中的.a文件就是pack生成的。详细说明:http://plan9.bell-labs.com/magic/man2html/1/ar 245 | 246 | 10)vet 用于检查并报告Go源码中可疑的结构。 247 | 比如调用Printf,它的参数和格式化字符串提供的不一致,如:fmt.Printf(“%s is %s”, name),这样会被检查出来。 248 | 249 | 11)yacc Go版本的yacc。 250 | http://plan9.bell-labs.com/magic/man2html/1/yacc。这是一个经典的生成语法分析器的工具。更多详细说明,可以查阅相关资料。Yacc 与 Lex 快速入门 251 | 252 | 以上工具目录中都有doc.go文件,用于生成文档。http://golang.org/cmd/可以查看。 253 | 254 | 12)addr2line linux下有这个命令。 255 | 这是一个addr2line的模拟器,只是为了使pprof能够在mac上工作。关于addr2line,可以查看linux的man手册,也可以看addr2line探秘 256 | 257 | 13)objdump linux下有这个命令。 258 | 这是一个objdump的模拟器,只是为了使pprof能够在mac上工作。关于objdump,可以查看linux的man手册 259 | 260 | 14)dist 这是一个重要的工具。它是一个引导程序,负责构建Go其他基本工具。通过源码安装Go时,会先安装该工具。 261 | 注:安装完之后,pkg/tool/$GOOS_$GOARCH下面的pprof工具是从misc下面copy过来的 262 | 263 | ## 四、安装脚本 264 | 通过源码安装Go相当简单(安装速度也很快),因为它提供了方便的脚本。脚本在src目录下 265 | 266 | all.bash/all.bat — 会执行make脚本和run脚本 267 | make.bash/make.bat — 安装Go 268 | run.bash/run.bat — 测试标准库 269 | 270 | 所以,通过源码安装Go,一般cd到src目录执行./all.bash。如果不想测试标准库,可以直接./make.bash,这样会比较快。 271 | 272 | Make.dist 被其他Makefile文件引用,比如cmd下面的很多工具中的Makefile文件。这个文件的作用是:运行go tool dist去安装命令,同时在安装过程中会打印出执行了该文件的目录名。可见,在源码安装Go的过程中,打印出的大部分信息就是这个文件的作用。 273 | 274 | ## 五、src/pkg Go标准库源码 275 | 276 | 1) runtime目录 277 | 278 | |── alg.c Type结构体中的alg,类型操作。 279 | |── append_test.go 280 | |── arch_386.h 281 | |── arch_amd64.h 282 | |── arch_arm.h 283 | |── asm_386.s 284 | |── asm_amd64.s 285 | |── asm_arm.s 286 | |── atomic_386.c 287 | |── atomic_amd64.c 288 | |── atomic_arm.c 289 | |── callback_windows_386.c 290 | |── callback_windows_amd64.c 291 | |── cgo 292 | |   ├── asm_386.s 293 | |   ├── asm_amd64.s 294 | |   ├── asm_arm.s 295 | |   ├── callbacks.c 296 | |   ├── cgo.go 297 | |   ├── cgo_arm.c 298 | |   ├── freebsd.c 299 | |   ├── gcc_386.S 300 | |   ├── gcc_amd64.S 301 | |   ├── gcc_arm.S 302 | |   ├── gcc_darwin_386.c 303 | |   ├── gcc_darwin_amd64.c 304 | |   ├── gcc_freebsd_386.c 305 | |   ├── gcc_freebsd_amd64.c 306 | |   ├── gcc_freebsd_arm.c 307 | |   ├── gcc_linux_386.c 308 | |   ├── gcc_linux_amd64.c 309 | |   ├── gcc_linux_arm.c 310 | |   ├── gcc_netbsd_386.c 311 | |   ├── gcc_netbsd_amd64.c 312 | |   ├── gcc_netbsd_arm.c 313 | |   ├── gcc_openbsd_386.c 314 | |   ├── gcc_openbsd_amd64.c 315 | |   ├── gcc_setenv.c 316 | |   ├── gcc_util.c 317 | |   ├── gcc_windows_386.c 318 | |   ├── gcc_windows_amd64.c 319 | |   ├── iscgo.c 320 | |   ├── libcgo.h 321 | |   ├── netbsd.c 322 | |   ├── openbsd.c 323 | |   └── setenv.c 324 | |── cgocall.c 325 | |── cgocall.h 326 | |── chan.c 通道的实现 327 | |── compiler.go 328 | |── complex.c 329 | |── cpuprof.c pprof相关 330 | |── debug 331 | |   ├── debug.c 332 | |   ├── garbage.go 333 | |   ├── garbage_test.go 334 | |   ├── stack.go 335 | |   └── stack_test.go 336 | |── debug.go 337 | |── defs1_linux.go 338 | |── defs2_linux.go 339 | |── defs_arm_linux.go 340 | |── defs_darwin.go 341 | |── defs_darwin_386.h 342 | |── defs_darwin_amd64.h 343 | |── defs_freebsd.go 344 | |── defs_freebsd_386.h 345 | |── defs_freebsd_amd64.h 346 | |── defs_freebsd_arm.h 347 | |── defs_linux.go 348 | |── defs_linux_386.h 349 | |── defs_linux_amd64.h 350 | |── defs_linux_arm.h 351 | |── defs_netbsd.go 352 | |── defs_netbsd_386.go 353 | |── defs_netbsd_386.h 354 | |── defs_netbsd_amd64.go 355 | |── defs_netbsd_amd64.h 356 | |── defs_netbsd_arm.go 357 | |── defs_netbsd_arm.h 358 | |── defs_openbsd.go 359 | |── defs_openbsd_386.h 360 | |── defs_openbsd_amd64.h 361 | |── defs_plan9_386.h 362 | |── defs_plan9_amd64.h 363 | |── defs_windows.go 364 | |── defs_windows_386.h 365 | |── defs_windows_amd64.h 366 | |── env_plan9.c 367 | |── env_posix.c 368 | |── error.go 369 | |── extern.go 370 | |── float.c 371 | |── gc_test.go 372 | |── hashmap.c map容器的底层实现 373 | |── hashmap.h 374 | |── hashmap_fast.c 375 | |── iface.c interface的底层实现 376 | |── lfstack.c 垃圾回收中用到了这个文件,lock free stack的缩写。垃圾回收中,利用来实现PtrBuffer的并发安全性 377 | |── lock_futex.c 378 | |── lock_sema.c 379 | |── malloc.goc malloc相关的封装,提供了runtime.mallocgc 380 | |── malloc.h 381 | |── malloc1.go 382 | |── mallocrand.go 383 | |── mallocrep.go 384 | |── mallocrep1.go 385 | |── mcache.c 内存管理实现相关,MCache层次的数据结构和操作 386 | |── mcentral.c 内存管理实现相关,MCentral层次的数据结构和操作 387 | |── mem.go 内存信息统计 388 | |── mem_darwin.c 389 | |── mem_freebsd.c 390 | |── mem_linux.c 内存分配相关,依赖于系统部分的分配函数。最下层接口,这里提供SysAlloc分配大块内存,提供runtime内存池使用 391 | |── mem_netbsd.c 392 | |── mem_openbsd.c 393 | |── mem_plan9.c 394 | |── mem_windows.c 395 | |── memclr_arm.s 396 | |── memmove_386.s 397 | |── memmove_amd64.s 398 | |── memmove_arm.s 399 | |── memmove_linux_amd64_test.go 400 | |── mfinal.c 401 | |── mfinal_test.go 402 | |── mfixalloc.c 403 | |── mgc0.c 垃圾回收最核心的部分都是在这一个文件里实现的 404 | |── mgc0.go 405 | |── mgc0.h 406 | |── mheap.c 内存管理实现相关,MHeap层次的数据结构和操作 407 | |── mkversion.c 408 | |── mprof.goc pprof相关 409 | |── msize.c 410 | |── netpoll.goc 411 | |── netpoll_epoll.c 封装了依赖到系统的epoll,对Go提供runtime.netpoll相关的函数 412 | |── netpoll_kqueue.c 413 | |── netpoll_stub.c 414 | |── os_darwin.c 415 | |── os_darwin.h 416 | |── os_freebsd.c 417 | |── os_freebsd.h 418 | |── os_freebsd_arm.c 419 | |── os_linux.c goroutine相关,依赖于系统的部分。runtime.newosproc,runtime.minit等函数都在这里实现 420 | |── os_linux.h 421 | |── os_linux_386.c 422 | |── os_linux_arm.c 423 | |── os_netbsd.c 424 | |── os_netbsd.h 425 | |── os_netbsd_386.c 426 | |── os_netbsd_amd64.c 427 | |── os_netbsd_arm.c 428 | |── os_openbsd.c 429 | |── os_openbsd.h 430 | |── os_plan9.c 431 | |── os_plan9.h 432 | |── os_plan9_386.c 433 | |── os_plan9_amd64.c 434 | |── os_windows.c 435 | |── os_windows.h 436 | |── os_windows_386.c 437 | |── os_windows_amd64.c 438 | |── panic.c 439 | |── parfor.c 垃圾回收相关,提供了并行方面的支持 440 | |── pprof 441 | |   ├── pprof.go 442 | |   └── pprof_test.go 443 | |── print.c 444 | |── proc.c goroutine调度器 445 | |── proc.p 446 | |── proc_test.go 447 | |── race 448 | |   ├── README 449 | |   ├── doc.go 450 | |   ├── race.go 451 | |   ├── race_darwin_amd64.syso 452 | |   ├── race_linux_amd64.syso 453 | |   ├── race_test.go 454 | |   ├── race_windows_amd64.syso 455 | |   └── testdata 456 | |   ├── atomic_test.go 457 | |   ├── cgo_test.go 458 | |   ├── cgo_test_main.go 459 | |   ├── chan_test.go 460 | |   ├── comp_test.go 461 | |   ├── finalizer_test.go 462 | |   ├── io_test.go 463 | |   ├── map_test.go 464 | |   ├── mop_test.go 465 | |   ├── mutex_test.go 466 | |   ├── regression_test.go 467 | |   ├── rwmutex_test.go 468 | |   ├── select_test.go 469 | |   ├── slice_test.go 470 | |   ├── sync_test.go 471 | |   └── waitgroup_test.go 472 | |── race.c 473 | |── race.go 474 | |── race.h 475 | |── race0.c 476 | |── race_amd64.s 477 | |── rt0_darwin_386.s 478 | |── rt0_darwin_amd64.s 479 | |── rt0_freebsd_386.s 480 | |── rt0_freebsd_amd64.s 481 | |── rt0_freebsd_arm.s 482 | |── rt0_linux_386.s 483 | |── rt0_linux_amd64.s 生成可执行文件的入口,包括main函数等 484 | |── rt0_linux_arm.s 485 | |── rt0_netbsd_386.s 486 | |── rt0_netbsd_amd64.s 487 | |── rt0_netbsd_arm.s 488 | |── rt0_openbsd_386.s 489 | |── rt0_openbsd_amd64.s 490 | |── rt0_plan9_386.s 491 | |── rt0_plan9_amd64.s 492 | |── rt0_windows_386.s 493 | |── rt0_windows_amd64.s 494 | |── rune.c 495 | |── runtime-gdb.py 496 | |── runtime.c 497 | |── runtime.h 几乎大部分的数据结构都是在这个文件中定义的 498 | |── runtime1.goc 499 | |── runtime_linux_test.go 500 | |── runtime_test.go 501 | |── sema.goc 502 | |── signal_386.c 503 | |── signal_amd64.c 504 | |── signal_arm.c 505 | |── signal_darwin_386.h 506 | |── signal_darwin_amd64.h 507 | |── signal_freebsd_386.h 508 | |── signal_freebsd_amd64.h 509 | |── signal_freebsd_arm.h 510 | |── signal_linux_386.h 511 | |── signal_linux_amd64.h 512 | |── signal_linux_arm.h 513 | |── signal_netbsd_386.h 514 | |── signal_netbsd_amd64.h 515 | |── signal_netbsd_arm.h 516 | |── signal_openbsd_386.h 517 | |── signal_openbsd_amd64.h 518 | |── signal_unix.c 519 | |── signal_unix.h 520 | |── signals_darwin.h 521 | |── signals_freebsd.h 522 | |── signals_linux.h 523 | |── signals_netbsd.h 524 | |── signals_openbsd.h 525 | |── signals_plan9.h 526 | |── signals_windows.h 527 | |── sigqueue.goc 528 | |── slice.c slice的底层实现 529 | |── softfloat64.go 530 | |── softfloat64_test.go 531 | |── softfloat_arm.c 532 | |── stack.c runtime.newstack函数是这里实现的 533 | |── stack.h 这个文件里的注释很重要,讲了Go里使用的栈结构 534 | |── string.goc 535 | |── string_test.go 536 | |── symtab.c 537 | |── symtab_test.go 538 | |── sys_darwin_386.s 539 | |── sys_darwin_amd64.s 540 | |── sys_freebsd_386.s 541 | |── sys_freebsd_amd64.s 542 | |── sys_freebsd_arm.s 543 | |── sys_linux_386.s 544 | |── sys_linux_amd64.s 封装了重要的系统调用,提供runtime.xx的函数,比如runtime.read 545 | |── sys_linux_arm.s 546 | |── sys_netbsd_386.s 547 | |── sys_netbsd_amd64.s 548 | |── sys_netbsd_arm.s 549 | |── sys_openbsd_386.s 550 | |── sys_openbsd_amd64.s 551 | |── sys_plan9_386.s 552 | |── sys_plan9_amd64.s 553 | |── sys_windows_386.s 554 | |── sys_windows_amd64.s 555 | |── syscall_windows.goc 556 | |── syscall_windows_test.go 557 | |── time.goc 558 | |── time_plan9_386.c 559 | |── traceback_arm.c 560 | |── traceback_x86.c 561 | |── type.go 562 | |── type.h 类型系统,里面有Type结构体的定义。interface以及reflect也依赖于这里一些东西 563 | |── typekind.h 类型系统,提供了Type中的kind编号 564 | |── vdso_linux_amd64.c 565 | |── vlop_386.s 566 | |── vlop_arm.s 567 | |── vlop_arm_test.go 568 | |── vlrt_386.c 569 | |── vlrt_arm.c 570 | |── zasm_darwin_amd64.h 571 | |── zgoarch_amd64.go 572 | |── zgoos_darwin.go 573 | |── zmalloc_darwin_amd64.c 574 | |── zmprof_darwin_amd64.c 575 | |── znetpoll_darwin_amd64.c 576 | |── zruntime1_darwin_amd64.c 577 | |── zruntime_defs_darwin_amd64.go 578 | |── zsema_darwin_amd64.c 579 | |── zsigqueue_darwin_amd64.c 580 | |── zstring_darwin_amd64.c 581 | |── ztime_darwin_amd64.c 582 | |── zversion.go 583 | 584 | -------------------------------------------------------------------------------- /zh/ref3.md: -------------------------------------------------------------------------------- 1 | Frequently on mailing list or IRC channel there are requests for documentation on the details of the Go compiler, runtime and internals. Currently the canonical source of documentation about Go’s internals is the source, which I encourage everyone to read. Having said that, the Go build process has been stable since the Go 1.0 release, so documenting it here will probably remain relevant for some time. 2 | 3 | This post walks through the nine steps of the Go build process, starting with the source and ending with a fully tested Go installation. For simplicity, all paths mentioned are relative to the root of the source checkout, $GOROOT/src. 4 | 5 | For background you should also read Installing Go from source on the golang.org website. 6 | 7 | Step 1. all.bash 8 | 9 | % cd $GOROOT/src 10 | % ./all.bash 11 | The first step is a bit anticlimactic as all.bash just calls two other shell scripts; make.bash and run.bash. If you’re using Windows or Plan 9 the process is the same, but the scripts end in .bat or .rc respectively. For the rest of this post, please substitute the the extension appropriate for your operating system. 12 | 13 | Step 2. make.bash 14 | 15 | . ./make.bash --no-banner 16 | make.bash is sourced from all.bash so that calls to exit will terminate the build process properly. make.bash has three main jobs, the first job is to validate the environment Go is being compiled in is sane. The sanity checks have been built up over the last few years and generally try to avoid building with known broken tools, or in environments where the build will fail. 17 | 18 | Step 3. cmd/dist 19 | 20 | gcc -O2 -Wall -Werror -ggdb -o cmd/dist/dist -Icmd/dist cmd/dist/*.c 21 | Once the sanity checks are complete, make.bash compiles cmd/dist. cmd/dist replaces the Makefile based system which existed before Go 1 and manages the small amounts of code generation in pkg/runtime. cmd/dist is a C program which allows it to leverage the system C compiler and headers to handle most of the host platform detection issues. cmd/dist always detects your host’s operating system and architecture, $GOHOSTOS and $GOHOSTARCH. These may differ from any value of $GOOS and $GOARCH you may have set if you are cross compiling. In fact, the Go build process is always building a cross compiler, but in most cases the host and target platform are the same. Next, make.bash invokes cmd/dist with the bootstrap argument which compiles the supporting libraries, lib9, libbio and libmach, used by the compiler suite, then the compilers themselves. These tools are also written in C and are compiled by the system C compiler. 22 | 23 | echo "# Building compilers and Go bootstrap tool for host, $GOHOSTOS/$GOHOSTARCH." 24 | buildall="-a" 25 | if [ "$1" = "--no-clean" ]; then 26 | buildall="" 27 | fi 28 | ./cmd/dist/dist bootstrap $buildall -v # builds go_bootstrap 29 | Using the compiler suite, cmd/dist then compiles a version of the go tool, go_bootstrap. The go_bootstrap tool is not the full go tool, for example pkg/net is stubbed out which avoids a dependency on cgo. The list of files to be compiled, and their dependencies is encoded in the cmd/dist tool itself, so great care is taken to avoid introducing new build dependencies for cmd/go. 30 | 31 | Step 4. go_bootstrap 32 | 33 | Now that go_bootstrap is built, the final stage of make.bash is to use go_bootstrap to compile the complete Go standard library, including a replacement version of the full go tool. 34 | 35 | echo "# Building packages and commands for $GOOS/$GOARCH." 36 | "$GOTOOLDIR"/go_bootstrap install -gcflags "$GO_GCFLAGS" \ 37 | -ldflags "$GO_LDFLAGS" -v std 38 | Step 5. run.bash 39 | 40 | Now that make.bash is complete, execution falls back to all.bash, which invokes run.bash. run.bash‘s job is to compile and test the standard library, the runtime, and the language test suite. 41 | 42 | bash run.bash --no-rebuild 43 | The --no-rebuild flag is used because make.bash and run.bash can both invoke go install -a std, so to avoid duplicating the previous effort, --no-rebuild skips the second go install. 44 | 45 | # allow all.bash to avoid double-build of everything 46 | rebuild=true 47 | if [ "$1" = "--no-rebuild" ]; then 48 | shift 49 | else 50 | echo '# Building packages and commands.' 51 | time go install -a -v std 52 | echo 53 | fi 54 | Step 6. go test -a std 55 | 56 | echo '# Testing packages.' 57 | time go test std -short -timeout=$(expr 120 \* $timeout_scale)s 58 | echo 59 | Next run.bash is to run the unit tests for all the packages in the standard library, which are written using the testing package. Because code in $GOPATH and $GOROOT live in the same namespace, we cannot use go test ... as this would also test every package in $GOPATH, so an alias, std, was created to address the packages in the standard library. Because some tests take a long time, or consume a lot of memory, some tests filter themselves with the -short flag. 60 | 61 | Step 7. runtime and cgo tests 62 | 63 | The next section of run.bash runs a set of tests for platforms that support cgo, runs a few benchmarks, and compiles miscellaneous programs that ship with the Go distribution. Over time this list of miscellaneous programs has grown as it was found that when they were not included in the build process, they would inevitably break silently. 64 | 65 | Step 8. go run test 66 | 67 | (xcd ../test 68 | unset GOMAXPROCS 69 | time go run run.go 70 | ) || exit $? 71 | The penultimate stage of run.bash invokes the compiler and runtime tests in the test folder directly under $GOROOT. These are tests of the low level details of the compiler and runtime itself. While the tests exercise the specification of the language, the test/bugs and test/fixedbugs sub directories capture unique tests for issues which have been found and fixed. The test driver for all these tests is $GOROOT/test/run.go which is a small Go program that runs each .go file inside the test directory. Some .go files contain directives on the first line which instruct run.go to expect, for example, the program to fail, or to emit a certain output sequence. 72 | 73 | Step 9. go tool api 74 | 75 | echo '# Checking API compatibility.' 76 | go tool api -c $GOROOT/api/go1.txt,$GOROOT/api/go1.1.txt \ 77 | -next $GOROOT/api/next.txt -except $GOROOT/api/except.txt 78 | The final step of run.bash is to invoke the api tool. The api tool’s job is to enforce the Go 1 contract; the exported symbols, constants, functions, variables, types and methods that made up the Go 1 API when it shipped in 2012. For Go 1 they are spelled out in api/go1.txt, and Go 1.1, api/go1.1.txt. An additional file, api/next.txt identifies the symbols that make up the additions to the standard library and runtime since Go 1.1. Once Go 1.2 ships, this file will become the contract for Go 1.2, and there will be a new next.txt. There is also a small file, except.txt, which contains exceptions to the Go 1 contract which have been approved. Additions to the file are not expected to be taken lightly. 79 | 80 | Additional tips and tricks 81 | 82 | You’ve probably figured out that make.bash is useful for building Go without running the tests, and likewise, run.bash is useful for building and testing the Go runtime. This distinction is also useful as the former can be used when cross compiling Go, and the latter is useful if you are working on the standard library. --------------------------------------------------------------------------------