goroutine 相关知识8

传统的Linux进程内存布局

user stack大小固定,Linux 默认8M,运行时内存占用超过上限,程序会崩溃掉并报告segment错误

  • 可以调大内核 stack size 参数,简单但影响系统所有thread
  • 创建线程时显式传入所需要内存块大小,需要开发者精确计算每个thread的大小, 负担比较高
  • 既不影响所有thread又不给开发者增加太多,比如在函数调用处插桩检查当前栈的空间是否能够满足新函数的执行

满足直接执行,否则创建新的栈空间并将老的栈拷贝到新的栈然后再执行

但当前Linux kernel thread模型却不能满足,只能在用户空间实现,并且有不小的难度

go runtime解决了这个问题, 每个routine(g0除外)初始化时stack都=2KB, 运行过程中会根据不同的场景做动态的调整

栈扩容和缩容

协程栈的内存布局和一些重要的术语

  • stack.lo: 栈空间低地址
  • stack.hi: 栈空间高地址
  • stackguard0: stack.lo + StackGuard, 用于stack overlow的检测
  • StackGuard: 保护区大小,Linux=880字节
  • StackSmall: 128字节,用于小函数调用的优化

在判断栈空间是否需要扩容的时候,可以根据被调用函数栈帧的大小, 分为以下两种情况:

  • 小于StackSmall SP小于stackguard0, 执行栈扩容,否则直接执行

  • 大于StackSamll SP - Function’s Stack Frame Size + StackSmall 小于stackguard0, 执行栈扩容,否则直接执行

runtime中还有个StackBig的常量,默认4096,被调用函数栈帧大小大于StackBig的时候, 一定会发生栈的扩容

package main

func main() {
	a, b := 1, 2
	_ = add1(a, b)
	_ = add2(a, b)
}

func add1(x, y int) int {
	return x + y
}

func add2(x, y int) int {
	_ = make([]byte, 200)
	return x + y
}
go tool compile -N -l -S stack.go > stack.s 
"".main t=1 size=112 args=0x0 locals=0x30
	// 栈大小为48,无参数
	0x0000 00000 (stack.go:3)	TEXT	"".main(SB), $48-0
	// 通过thread local storage获取当前g(g为goroutine的的数据结构)
	0x0000 00000 (stack.go:3)	MOVQ	(TLS), CX
	// 比较SP和g.stackguard0
	0x0009 00009 (stack.go:3)	CMPQ	SP, 16(CX)
	// 小于g.stackguard0,jump到105执行栈的扩容
	0x000d 00013 (stack.go:3)	JLS	105
	// 继续执行
	0x000f 00015 (stack.go:3)	SUBQ	$48, SP
	0x0013 00019 (stack.go:3)	MOVQ	BP, 40(SP)
	0x0018 00024 (stack.go:3)	LEAQ	40(SP), BP
	// 用于垃圾回收
	0x001d 00029 (stack.go:3)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (stack.go:3)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (stack.go:4)	MOVQ	$1, "".a+32(SP)
	0x0026 00038 (stack.go:4)	MOVQ	$2, "".b+24(SP)
	// 将a放入AX寄存器
	0x002f 00047 (stack.go:5)	MOVQ	"".a+32(SP), AX
	// 参数a压栈
	0x0034 00052 (stack.go:5)	MOVQ	AX, (SP)
	// 将b放入AX寄存器
	0x0038 00056 (stack.go:5)	MOVQ	"".b+24(SP), AX
	// 参数b压栈
	0x003d 00061 (stack.go:5)	MOVQ	AX, 8(SP)
	0x0042 00066 (stack.go:5)	PCDATA	$0, $0
	// 调用add1
	0x0042 00066 (stack.go:5)	CALL	"".add1(SB)
	// 将a放入AX寄存器
	0x0047 00071 (stack.go:6)	MOVQ	"".a+32(SP), AX
	// 参数a压栈
	0x004c 00076 (stack.go:6)	MOVQ	AX, (SP)
	// 将b放入AX寄存器
	0x0050 00080 (stack.go:6)	MOVQ	"".b+24(SP), AX
	// 参数b压栈
	0x0055 00085 (stack.go:6)	MOVQ	AX, 8(SP)
	0x005a 00090 (stack.go:6)	PCDATA	$0, $0
	// 调用add2
	0x005a 00090 (stack.go:6)	CALL	"".add2(SB)
	0x005f 00095 (stack.go:7)	MOVQ	40(SP), BP
	0x0064 00100 (stack.go:7)	ADDQ	$48, SP
	0x0068 00104 (stack.go:7)	RET
	0x0069 00105 (stack.go:7)	NOP
	0x0069 00105 (stack.go:3)	PCDATA	$0, $-1
	// 调用runtime.morestack_noctxt执行栈扩容
	0x0069 00105 (stack.go:3)	CALL	runtime.morestack_noctxt(SB)
	// 返回到函数开始处继续执行
	0x006e 00110 (stack.go:3)	JMP	0
    ...
"".add1 t=1 size=28 args=0x18 locals=0x0
	// 栈大小为0,参数为24字节, 栈帧小于StackSmall不进行栈空间判断直接执行
	0x0000 00000 (stack.go:9)	TEXT	"".add1(SB), $0-24
	0x0000 00000 (stack.go:9)	FUNCDATA	$0, gclocals·54241e171da8af6ae173d69da0236748(SB)
	0x0000 00000 (stack.go:9)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0000 00000 (stack.go:9)	MOVQ	$0, "".~r2+24(FP)
	0x0009 00009 (stack.go:10)	MOVQ	"".x+8(FP), AX
	0x000e 00014 (stack.go:10)	MOVQ	"".y+16(FP), CX
	0x0013 00019 (stack.go:10)	ADDQ	CX, AX
	0x0016 00022 (stack.go:10)	MOVQ	AX, "".~r2+24(FP)
	0x001b 00027 (stack.go:10)	RET
"".add2 t=1 size=151 args=0x18 locals=0xd0
	// 栈大小为208字节,参数为24字节
	0x0000 00000 (stack.go:13)	TEXT	"".add2(SB), $208-24
	// 获取当前g
	0x0000 00000 (stack.go:13)	MOVQ	(TLS), CX
	// 栈大小大于StackSmall, 计算 SP - FramSzie + StackSmall 并放入AX寄存器
	0x0009 00009 (stack.go:13)	LEAQ	-80(SP), AX
	// 比较上面计算出来的值和g.stackguard0
	0x000e 00014 (stack.go:13)	CMPQ	AX, 16(CX)
	// 小于g.stackguard0, jump到141执行栈的扩容
	0x0012 00018 (stack.go:13)	JLS	141
	// 继续执行
	0x0014 00020 (stack.go:13)	SUBQ	$208, SP
	0x001b 00027 (stack.go:13)	MOVQ	BP, 200(SP)
	0x0023 00035 (stack.go:13)	LEAQ	200(SP), BP
	0x002b 00043 (stack.go:13)	FUNCDATA	$0, gclocals·54241e171da8af6ae173d69da0236748(SB)
	0x002b 00043 (stack.go:13)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x002b 00043 (stack.go:13)	MOVQ	$0, "".~r2+232(FP)
	0x0037 00055 (stack.go:14)	MOVQ	$0, ""..autotmp_0(SP)
	0x003f 00063 (stack.go:14)	LEAQ	""..autotmp_0+8(SP), DI
	0x0044 00068 (stack.go:14)	XORPS	X0, X0
	0x0047 00071 (stack.go:14)	DUFFZERO	$247
	0x005a 00090 (stack.go:14)	LEAQ	""..autotmp_0(SP), AX
	0x005e 00094 (stack.go:14)	TESTB	AL, (AX)
	0x0060 00096 (stack.go:14)	JMP	98
	0x0062 00098 (stack.go:15)	MOVQ	"".x+216(FP), AX
	0x006a 00106 (stack.go:15)	MOVQ	"".y+224(FP), CX
	0x0072 00114 (stack.go:15)	ADDQ	CX, AX
	0x0075 00117 (stack.go:15)	MOVQ	AX, "".~r2+232(FP)
	0x007d 00125 (stack.go:15)	MOVQ	200(SP), BP
	0x0085 00133 (stack.go:15)	ADDQ	$208, SP
	0x008c 00140 (stack.go:15)	RET
	0x008d 00141 (stack.go:15)	NOP
	0x008d 00141 (stack.go:13)	PCDATA	$0, $-1
	// 调用runtime.morestack_noctxt完成栈扩容
	0x008d 00141 (stack.go:13)	CALL	runtime.morestack_noctxt(SB)
	// jump到函数开始的地方继续执行
	0x0092 00146 (stack.go:13)	JMP	0
    ...

被调用函数栈帧小于StackSmall时没有执行栈空间大小判断而是直接执行,在一定程度上优化了小函数的调用

于StackSmall的,执行栈空间大小判断,栈空间不足时,调用runtime.morestack_noctxt完成栈的扩容,然后再重新开始执行函数

go 1.3 前,栈扩容用分段栈(Segemented Stack),在栈空间不够的时候新申请一个栈空间用于被调用函数的执行, 执行后销毁新申请的栈空间并回到老的栈空间继续执行,当函数出现频繁调用(递归)时可能会引发hot split

1.3后用连续栈(Contiguous Stack),栈空间不足的时候申请一个2倍于当前大小的新栈,并把所有数据拷贝到新栈, 接下来的所有调用执行都发生在新栈上

栈缩容

long running 的 goroutine 由于某次函数调用中引发了栈的扩容, 被调用函数返回后很大部分空间都未被利用,需要进行栈收缩,节约内存提高利用率

栈收缩不在函数调用时发生,由 GC 时主动触发

基本过程是计算当前使用的空间,小于栈空间的1/4的话, 执行栈的收缩,将栈收缩为现在的1/2,否则直接返回

栈缩容的目标是提高内存利用率,但在缩容过程中会存在栈拷贝和写屏障(write barrier),会有些性能影响

GODEBUG=gcshrinkstackoff=1 可关闭栈缩容, 需要承担栈持续增长的风险,在关闭前需要慎重考虑

转载于:https://my.oschina.net/zhangthe9/blog/3022425

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
作为Go语言初学者,以下是一些重要的知识点和建议,可以帮助你开始学习和使用Go语言: 1. 安装和设置Go环境:首先,确保在计算机上安装了Go语言的最新版本,并设置好相关的环境变量。可以从官方网站(https://golang.org)下载安装程序并按照说明进行安装。 2. 了解基本语法和数据类型:学习Go语言的基本语法,包括变量声明、函数定义、条件语句、循环语句等。掌握Go语言的基本数据类型,如整型、浮点型、字符串、布尔型等。 3. 学习函数和包:函数是Go语言的基本构建块之一。了解如何定义和调用函数,并理解函数的参数和返回值。此外,了解如何使用包(package)来组织和重用代码。 4. 并发编程:Go语言内置了强大的并发编程支持。学习使用goroutine和channel进行并发编程,以实现高效的并发处理和协作。 5. 错误处理:Go语言鼓励使用显式的错误处理机制。学习使用错误类型和错误处理函数来处理可能发生的错误,并避免潜在的错误。 6. 标准库和第三方库:探索Go语言的标准库,了解如何使用其中的功能和工具。此外,也要了解常用的第三方库,它们提供了许多有用的功能和工具,可以加快开发速度。 7. 学习常用的工具和技术:Go语言有许多强大的工具和技术可用于开发。学习使用Go工具链(如go build、go run、go test等),以及版本管理工具(如git)和构建工具(如Makefile)等。 8. 实践和项目:通过实践和参与项目,将所学的知识应用到实际中。可以尝试解决一些小型的编程问题,或者参与开源项目,以提高自己的编程能力和经验。 9. 阅读文档和教程:Go语言有丰富的文档和教程资源可供学习。阅读官方文档、博客文章、书籍和在线教程,可以帮助你更深入地理解和掌握Go语言的知识和技巧。 10. 加入社区和交流:加入Go语言的社区,与其他开发者交流和分享经验。参加本地的Go语言用户组、在线论坛或社交媒体群组,可以获取更多资源、解答问题,并与其他Go开发者建立联系。 记住,持续学习和实践是掌握任何编程语言的关键。通过不断地编写代码、阅读文档和参与项目,你将逐渐掌握和提高Go语言的技能。祝你在学习Go语言的过程中取得成功!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值