GO的连续栈
Go语言支持goroutine,每个goroutine需要能够运行,所以它们都有自己的栈
所以栈的空间是随着需要自动增长的,每个goroutine开始只分配很小的栈空间
所以开了千万个goroutine也不会耗尽内存
continuous stack
基本原理
1.调用栈默认大小是4K(1.7修改为2K),最大栈1G,超出将panic
2.每次执行函数调用时,runtime就会进行检测,当前栈的大小不够用,会触发中断
3.runtime会保存此时函数运行的上下文
4.分配一个新的足够大的栈空间,进行2倍指数扩容将旧栈的内容拷贝到新栈中,并做一些设置,使得当函数恢复运行时,函数会在新分配的栈中继续执行
捕获栈空间不足,实行中断
1.比较栈指针寄存器和G结构体的stackguard
2.SP大于g->stackguard,runtime.morestack(保存M,连接旧栈新栈),runtime.newstack(新栈)
每个goroutine对应一个结构体G,大致相当于进程控制块的概念
结构体中存了stackbase和stackguard,用于确定这个goroutine使用的栈空间信息
如果栈指针寄存器(SP)值超越了stackguard就需要扩展栈空间
汇编如下:
1 | 000000 00000 (test.go:3) TEXT "".main+0(SB),$0-0 |
如果SP大于g->stackguard了,则会调用runtime.morestack函数.开始中断,新建栈空间
runtime.morestack是用汇编实现的,做的事情大致是将一些信息存在M结构体中,这些信息包括当前栈桢,参数,当前函数调用,函数返回地址(两个返回地址,一个是runtime.morestack的函数地址,一个是f的返回地址)。通过这些信息可以把新栈和旧栈链起来
1 | void runtime.morestack() { |
然后调用runtime.newstack,建立新的栈空间
PS:newstack是切换到m->g0的栈中去调用的。m->g0是调度器栈,go的运行时库的调度器使用的都是m->g0。
旧栈数据复制到新栈
1.runtime.morestack会调用于runtime.newstack
2.newstack:分配一个足够大的新的空间,将旧的栈中的数据复制到新的栈中
复制数据的过程中要考虑指针失效问题
某个指针引用旧栈的地址,搬到新栈后,旧栈会被释放,指针失效
所以要修改指针地址:
1.当前栈帧之前的每一个栈帧,对其中的每一个指针,检测指针指向的地址
2.如果指向地址是落在旧栈范围内的,则将它加上一个偏移使它指向新栈的相应地址。这个偏移值等于新栈基地址减旧栈基地址。
恢复运行:
1.切换到当前g
2.runtime.oldstack从过去保存旧栈的信息拿到相关信息
3.runtime.gogo根据旧的信息恢复上下文,运行
整个过程就完成了。
流程
1.比较SP与g的stackguard,SP大于g->stackguard,开始连续栈扩容
2.调用runtime.morestack,主要功能是保存当前的栈的一些信息
3.调用runtime.newstack,函数的主要功能是分配空间,装饰此空间,复制数据到新空间,修改指针
4.gogocall的方式切换到新分配的栈,获取旧的信息,恢复上下文,继续运行
栈缩容:是在垃圾回收的里处理的,当检测到栈只使用了不到1/4时,栈缩小为原来的1/2