深刻理解golang 的栈

线程栈(thread stacks)介绍

先回顾下linux的内存空间布局linux


 
简书_stack02.png

当启动一个C实现的thread时,C标准库会负责分配一块内存做为这个线程的栈。标准库分配这块内存,告诉内核它的位置并让内核处理这个线程 的执行。
在linux系统中,可经过 ulimit -s查看系统栈大小(8M)。
ulimit -s 10240可修改栈大小为10M。golang

 
 

 

这里最大的一个问题是,分配大数组,或者循环递归函数时,默认的栈空间不够用,会致使Segmentation fault错误。centos

//testMaxStack.cpp #include <stdio.h> int main() { printf("init ok\n"); char a[8192*1024]; // 8M空间 printf("run over\n"); } //执行结果 [app@VM_114_13_centos c]$ ulimit -s 8192 [app@VM_114_13_centos c]$ g++ testMaxStack.cpp [app@VM_114_13_centos c]$ ./a.out Segmentation fault 

解决方法有两个:数组

  • ulimit -s 10240调整标准库给全部线程栈分配的内存块的大小。可是全线提升栈大小意味着每一个线程都会提升栈的内存使用量,这样一来,你将用光全部内存。
  • 为每一个线程单独肯定栈大小。这样一来你就不得不完成这样的任务:根据每一个线程的须要,估算它们的栈内存的大小。这将是建立线程的难度超出咱们的指望。

Go是如何应对这个问题的

Go使用的解决方案相似第二种方法。
goroutine 初始时只给栈分配很小的空间,而后随着使用过程当中的须要自动地增加。这就是为何Go能够开千千万万个goroutine而不会耗尽内存。
Go 1.4开始使用的是连续栈,而这以前使用的分段栈ruby

分段栈(Segmented Stacks)

分段栈(segmented stacks)是Go语言最初用来处理栈的方案。
当建立一个goroutine时,Go运行时会分配一段8K字节的内存用于栈供goroutine运行使 用。app

每一个go函数在函数入口处都会有一小段代码,这段代码会检查是否用光了已分配的栈空间,若是用光了,这段代码会调用morestack函数。less

morestack函数

morestack函数会分配一段新内存用做栈空间,接下来它会将有关栈的各类数据信息写入栈底的一个struct中(下图中Stack info),包括上一段栈的地址。而后重启goroutine,从致使栈空间用光的那个函数(下图中的Foobar)开始执行。这就是所谓的“栈分裂 (stack split)”。函数

+---------------+
  | | | unused | | stack | | space | +---------------+ | Foobar | | | +---------------+ | | | lessstack | +---------------+ | Stack info | | |-----+ +---------------+ | | | +---------------+ | | Foobar | | | | <---+ +---------------+ | rest of stack | | | 
lessstack函数

在新栈的底部,插入了一个栈入口函数lessstack。设置这个函数用于从那个致使咱们用光栈空间的函数(Foobar)返回时用的。当那个函数(Foobar)返回时,咱们回到lessstack(这个栈帧),lessstack会查找 stack底部的那个struct,并调整栈指针(stack pointer),使得咱们返回到前一段栈空间。这样作以后,咱们就能够将这个新栈段(stack segment)释放掉,并继续执行咱们的程序了。布局

分段栈的问题

栈缩小是一个相对代价高昂的操做。若是在一个循环中调用的函数遇到栈分裂 (stack split),进入函数时会增长栈空间(morestack 函数),返回并释放栈段(lessstack 函数)。性能方面开销很大。性能

连续栈(continuous stacks)

go如今使用的是这套解决方案。
goroutine在栈上运行着,当用光栈空间,它遇到与旧方案中相同的栈溢出检查。可是与旧方案采用的保留一个返 回前一段栈的link不一样,新方案建立一个两倍于原stack大小的新stack,并将旧栈拷贝到其中
这意味着当栈实际使用的空间缩小为原先的 大小时,go运行时不用作任何事情。
栈缩小是一个无任何代价的操做(栈的收缩是垃圾回收的过程当中实现的.当检测到栈只使用了不到1/4时,栈缩小为原来的1/2)。
此外,当栈再次增加时,运行时也无需作任何事情,咱们只须要重用以前分配的空闲空间便可。

如何捕获到函数的栈空间不足

Go语言和C不一样,不是使用栈指针寄存器和栈基址寄存器肯定函数的栈的。

在Go的运行时库中,每一个goroutine对应一个结构体G,大体至关于进程控制块的概念。这个结构体中存了stackbasestackguard,用于肯定这个goroutine使用的栈空间信息。每一个Go函数调用的前几条指令,先比较栈指针寄存器跟g->stackguard,检测是否发生栈溢出。若是栈指针寄存器值超越了stackguard就须要扩展栈空间。

旧栈数据复制到新栈

旧栈数据复制到新栈的过程,要考虑指针失效问题。
Go实现了精确的垃圾回收,运行时知道每一块内存对应的对象的类型信息。在复制以后,会进行指针的调整。具体作法是,对当前栈帧以前的每个栈帧,对其中的每个指针,检测指针指向的地址,若是指向地址是落在旧栈范围内的,则将它加上一个偏移使它指向新栈的相应地址。这个偏移值等于新栈基地址减旧栈基地址

连接:https://www.jianshu.com/p/7ec9acca6480

相关文章
相关标签/搜索