共计 3248 个字符,预计需要花费 9 分钟才能阅读完成。
前言
本文拉开垃圾回收局部尾声(预报:会切入一些关键点剖析,杜绝市面千篇一律的内容)。因为 Go 协程的栈是 Go 运行时治理的,并调配于堆上,不禁操作系统治理,所以咱们先来看看协程栈的内存如何被 Go 运行治理和回收的。本篇文章先从初步意识协程栈开始。
为了对协程栈有个初步的意识,咱们先来回顾数据结构中栈的概念,再来看看内存栈的概念作用,最初咱们再来通过比照过程中的栈内存和线程中的栈内存来对协程中的栈内存有个初步的认知,全文大抵构造如下:
- 回顾数据结构中栈的概念
- 内存治理中栈的概念
- 了解过程栈和线程栈
- 意识协程栈
数据结构中栈的概念
栈是一种先进后出 (Frist In Last Out) 的数据结构。第一个元素所在位置为栈底,最初一个元素所在位置为栈顶。栈顶增加一个元素的过程为压栈(入栈),栈顶移出一个元素的过程为出栈(弹栈)。如下图所示:
内存治理中栈的概念
栈内存
什么是栈内存?
栈内存是计算机对间断内存的采取的「线性调配」治理形式,便于高效存储指令运行过程中的长期变量等。
- 栈内存调配逻辑:current – alloc
- 栈内存开释逻辑:current + alloc
- 栈内存的调配过程:看起来像不像数据结构「栈」的压栈过程。
- 栈内存的开释过程:看起来像不像数据结构「栈」的出栈过程。
什么是栈空间?
栈空间是一个固定值,决定了栈内存最大可调配的内存范畴,也就是决定了栈顶的下限。
栈内存的作用?
总的来说就是存放程序运行过程中,指令传输的、生产的各种长期变量的值,和函数递归调用过程的别要信息,以及过程、线程、协程切换的上下文信息。
- 寄存函数执行过程中的参数的值
- 寄存函数执行过程中的局部变量的值
- 产生函数调用时,寄存调用函数栈帧的栈基 BP 的值(下篇文章具体讲)
- 产生函数调用时,寄存调用函数下一个待执行指令的地址(下篇文章具体讲)
- 等等
接着我有两个问题:
- 谁决定了栈空间的大小范畴?
- 谁决定了代码在运行过程中,从栈空间调配或开释多少内存?
咱们别离从「过程栈」和「线程栈」、「协程栈」视角看看以上两个问题。
过程栈
什么是过程栈?
答:位于过程虚拟内存的用户空间,以用户空间的高地址开始地位作为栈底,向地址调配。如下图所示:
谁决定了栈空间 (过程栈) 的大小范畴?
答:操作系统的配置决定,可通过 `ulimit -s` 查看。示例如下:
啊
(TIGERB) 🤔 ➜ go1.16 git:((go1.16)) ✗ ulimit -s
8192 // 正文:单位 kb,8m
(TIGERB) 🤔 ➜ go1.16 git:((go1.16)) ✗ ulimit -a
-t: cpu time (seconds) unlimited
-f: file size (blocks) unlimited
-d: data seg size (kbytes) unlimited
-s: stack size (kbytes) 8192 // 正文:单位 kb,8m
-c: core file size (blocks) 0
-v: address space (kbytes) unlimited
-l: locked-in-memory size (kbytes) unlimited
-u: processes 1392
-n: file descriptors 256
谁决定了代码在运行过程中,从栈空间 (过程栈) 调配或开释多少内存?
答:编译器决定。具体解释如下:
代码编译时,编译器会插入调配和开释栈内存的指令,比方像上面这段简略的程序一样:
一段简略的加法示例代码:
// 源代码
package main
func main() {
a := 1
b := 2
c := a + b
// 略...
}
编译以上代码时,编译器会插入调配 (SUB) 和开释 (ADD) 栈内存的指令:
// 汇编伪代码
SUB 24, SP // 栈上调配 24 字节内存 3*8byte(调配栈内存指令)略...
ADD 24, SP // 栈上开释 24 字节内存 3*8byte(开释栈内存指令)略...
最初汇编代码转换为二进制代码:
// 二进制伪代码 轻易乱写的
011100011000000101...
过程栈总结
「过程栈」位于虚拟内存的用户空间,过程栈的栈底为用户空间局部高地址的开始地位。过程栈的栈空间大小为固定值,由操作系统的配置决定。过程运行过程中栈内存的调配和开释的机会和大小值由编译器决定。
线程栈
什么是线程栈?
答:创立一个线程时,应用 malloc 从堆上调配一块间断内存作为线程的栈空间。
伪代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define STACK_SIZE 1024 * 1024 // 栈空间大小
int main() {
pthread_t thread;
void* stack = malloc(STACK_SIZE); // 堆上申请一块内存
// ...
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstack(&attr, stack, STACK_SIZE); // 设置线程栈
int ret = pthread_create(&thread, &attr, thread_func, NULL); // 创立线程
// ...
}
谁决定了栈空间 (线程栈) 的大小范畴?
答:创立线程的运行时。
谁决定了代码在运行过程中,从栈空间 (线程栈) 调配或开释多少内存?
答:同过程,编译器决定。
协程栈
什么是协程栈?
答:应用 `go` 要害自创立一个协程时,Go 运行时从堆上调配一块间断内存作为协程的栈空间。
谁决定了协程栈的栈空间的大小范畴?
答:Go 运行时决定,g0 为 8KB,g 为 2KB
创立 g0
函数代码片段:
// src/runtime/proc.go::1720
// 创立 m
func allocm(_p_ *p, fn func(), id int64) *m {
// ... 略
if iscgo || mStackIsSystemAllocated() {mp.g0 = malg(-1)
} else {
// 创立 g0 并申请 8KB 栈内存
// 依赖的 malg 函数
mp.g0 = malg(8192 * sys.StackGuardMultiplier)
}
// ... 略
}
创立 g
函数代码片段:
// src/runtime/proc.go::3999
// 创立一个带有工作 fn 的 goroutine
func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) *g {
// ... 略
newg := gfget(_p_)
if newg == nil {
// 全局队列、本地队列找不到 g 则 创立一个全新的 goroutine
// _StackMin = 2048
// 申请 2KB 栈内存
// 依赖的 malg 函数
newg = malg(_StackMin)
casgstatus(newg, _Gidle, _Gdead)
allgadd(newg)
}
// ... 略
}
以上都依赖 malg
函数代码片段,其作用是创立一个全新g
:
// src/runtime/proc.go::3943
// 创立一个指定栈内存的 g
func malg(stacksize int32) *g {newg := new(g)
if stacksize >= 0 {
// ... 略
systemstack(func() {
// 调配栈内存
newg.stack = stackalloc(uint32(stacksize))
})
// ... 略
}
return newg
}
谁决定了代码在运行过程中,从协程栈的栈空间调配或开释多少内存?
答:同过程、线程,都由编译器决定。
总结
类型 | 创立机会 | 谁决定栈空间大小 | 内存地位 | 谁来调配和开释栈内存 |
---|---|---|---|---|
过程栈 | 过程启动时 | 操作系统配置,ulimit -s |
虚拟内存的用户空间栈区 | 编译器,汇编 SUB 、ADD 指令 |
线程栈 | 创立线程时 | 创立线程的运行时,pthread_attr_setstack |
虚拟内存的用户空间过程堆区域 | 编译器,汇编 SUB 、ADD 指令 |
协程栈 | 应用 go 关键字运行函数时 |
Go 运行时,malg(栈内存) g0 8KB、g 2KB |
虚拟内存的用户空间过程堆区域 | 编译器,汇编 SUB 、ADD 指令 |