线程栈(thread stacks)介绍

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


ulimit -sulimit -s 10240
Segmentation fault
//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 开始使用的是连续栈,而这之前使用的分段栈

分段栈(Segmented Stacks)

8K
morestack
morestack函数

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

  +---------------+
  |               |
  |   unused      |
  |   stack       |
  |   space       |
  +---------------+
  |    Foobar     |
  |               |
  +---------------+
  |               |
  |  lessstack    |
  +---------------+
  | Stack info    |
  |               |-----+
  +---------------+     |
                        |
                        |
  +---------------+     |
  |    Foobar     |     |
  |               | <---+
  +---------------+
  | rest of stack |
  |               |
lessstack函数
lessstack
分段栈的问题

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

连续栈(continuous stacks)

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

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

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

stackbasestackguardg->stackguardstackguard
旧栈数据复制到新栈

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