深入理解golang 的栈

栏目: Go · 发布时间: 6年前

内容简介:先回顾下linux的内存空间布局当启动一个C实现的thread时,C标准库会负责分配一块内存作为这个线程的栈。标准库分配这块内存,告诉内核它的位置并让内核处理这个线程 的执行。在linux系统中,可通过

线程栈(thread stacks)介绍

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

深入理解golang 的栈

简书_stack02.png

当启动一个C实现的thread时,C标准库会负责分配一块内存作为这个线程的栈。标准库分配这块内存,告诉内核它的位置并让内核处理这个线程 的执行。

在linux系统中,可通过 ulimit -s 查看系统栈大小(8M)。

ulimit -s 10240 可修改栈大小为10M。

深入理解golang 的栈

简书_stack01.png

这里最大的一个问题是, 分配大数组 ,或者 循环递归函数 时,默认的栈空间不够用,会导致 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)

分段栈(segmented stacks)是Go语言最初用来处理栈的方案。

当创建一个goroutine时,Go运行时会分配一段 8K 字节的内存用于栈供goroutine运行使 用。

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

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实现了精确的垃圾回收,运行时知道每一块内存对应的对象的类型信息。在复制之后,会进行指针的调整。具体做法是, 对当前栈帧之前的每一个栈帧,对其中的每一个指针,检测指针指向的地址,如果指向地址是落在旧栈范围内的,则将它加上一个偏移使它指向新栈的相应地址。这个偏移值等于新栈基地址减旧栈基地址


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

垃圾收集

垃圾收集

琼斯 / 谢之易 / 人民邮电出版社 / 2004-4-1 / 45.00元

书围绕着动态内存自动回收的话题,介绍了垃圾收集机制,详细分析了各种算法和相关技术。 本书共12章。第1章首先介绍计算机存储器管理的演化和自动内存回收的需求,并引入了本书所使用的术语和记法。第2章介绍了3种“经典”的垃圾收集技术:引用计数(reference counting)、标记-清扫(mark-sweep)和节点复制(copying)。 随后的4章更详细地讨论了上述这些垃圾收集方式......一起来看看 《垃圾收集》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

html转js在线工具
html转js在线工具

html转js在线工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换