Go(栈空间管理)

434 阅读4分钟

这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战

栈空间一般由编译器自动进行管理,负责维护函数调用所需要的东西(参数、返回地址,返回值,局部变量)

栈和堆都是对不同功能、不同区域的内存的划分

栈空间底层的内存管理实际上在前面的内存分配中已经提到了

设计原理

从底层往上说哈

寄存器

CPU中的重要组件与资源

  • 读写速度最快
  • 存储能力有限

栈操作至少会使用两个以上的寄存器

  • SP:存储栈顶地址
  • BP:存储调用的基地址指针

栈空间都是从高地址往低地址扩展的

  • 申请栈空间指的是,获取一个新的栈顶地址,然后往下写
    • 修改值的方式获得栈空间,比起堆空间的申请调度当然是快很多的
    • 什么是stack overflow呢?

线程栈

用于存储用户在该线程上进行函数调用时的参数、返回值、返回地址、局部变量等

  • 大小取决于架构
    • 一般来说使用的空间并不多(除非巨深的函数调用
  • 更多的用户自己分配的空间、执行上下文是放在G里面的
    • G自己也会涉及到函数调用,所以G内部是有自己的栈空间的

逃逸分析

众所周知,C C++是可以由程序员自己控制变量的存储位置(堆还是栈),可能会出现以下情况

  • 栈上指针指向堆,堆对象回收后,出现悬挂指针
  • 堆上指针指向栈,栈对象回收后,出现堆的悬挂指针,有安全问题
int *dangling_pointer() {
    int i = 2;
    return &i;
}

i作为堆对象,返回值(位于栈上)为指向其的指针,i回收后,该指针指向就非法了,获取到该指针返回值的函数比较危险

go中使用逃逸分析的方式,由编译器决定哪一些对象需要分配到栈上、哪些分配到堆上

通过改变变量分配位置的方式,维护以下两个不变性

  • 指向栈对象的指针只能在栈中
  • 栈指针指向的对象只能在栈中
  • 当栈对象失效(回收后),指向该对象的指针不能存活

在上面那个例子中,i会被分到栈上面

分析过程

  • 编译器构建AST(抽象语法树)
  • 构建变量之间的带权重的有向图
  • 根据不变性进行对象的分配

栈操作

栈结构:

type stack struct {
	lo uintptr
	hi uintptr
}

函数调用时需要在栈上分配空间,因此编译阶段需要在调用函数前插入 runtime.morestack 或者 runtime.morestack_nocktxt

  • 这样的话就可以在进行函数调用之前进行栈空间的检测以及分配
  • 在创建新的G之前也会调用runtime.stackalloc进行栈空间的分配

整个栈管理的过程分为四个部分

  • 栈初始化
  • 栈分配
  • 栈扩容
  • 栈缩容

栈初始化

主要有两个全局变量

  • runtime.stackpool:全局栈缓存 ,分配小于32KB的栈空间
  • runtime.stackLarge:大栈缓存,分配大于32KB的栈空间

这两个全局变量都和mspan密不可分,栈、堆可以理解为同一种内存管理单元分配下的不同组合

初始化时会调用runtime.stackinit初始化这些全局变量

使用全局变量进行内存分配的话,不同线程(P)之间分配空间时带来的锁竞争问题会极大降低效率

  • 每个线程的mcache中带有栈缓存来减少全局的锁竞争(32KB以下栈空间的分配)

栈分配

在进行G的初始化的时候,会为G分配大小足够的栈空间:

  • 栈空间较小,从mcache、全局栈缓存进行分配
    • mcache不足,会继续走内存申请流程
  • 栈空间较大,从全局的stackLarge中获取
  • 栈空间较大且stackLarge没有空间了,会从堆上申请一片足够大的内存空间

栈扩容

还记得最开始的时候在每次调用函数之前插入的morestack吗,函数调用前,检测当前G的栈空间是否足够,不足的话需要进行扩容

  • 保存一定信息
  • 调用newstack创建新的栈
    • 如果需要的栈空间大于最大栈空间:stack overflow
    • 调用stackalloc进行栈空间的分配
    • 开始进行栈空间的拷贝
      • 调整stack栈顶、栈底地址,计算变动信息
      • 拷贝空间
      • 调整原有所有指向栈变量的指针地址

栈缩容

流程与扩容类似,其缩容条件是:

  • 新栈大小为原有的一半
  • 新栈空间大于2KB