深入理解golang 的栈

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

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


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

查看所有标签

猜你喜欢:

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

Python Algorithms

Python Algorithms

Magnus Lie Hetland / Apress / 2010-11-24 / USD 49.99

Python Algorithms explains the Python approach to algorithm analysis and design. Written by Magnus Lie Hetland, author of Beginning Python, this book is sharply focused on classical algorithms, but it......一起来看看 《Python Algorithms》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码