15-go栈管理

22 篇文章 1 订阅
3 篇文章 0 订阅

Linux下线程栈最大大小可以通过ulimit -s查看,单位KB。

go@ubuntu:~$ ulimit -s 
8192
go@ubuntu:~$ 

可以通过ulimit -s 指定全局栈大小。也可以通过 pthread_attr_setstacksize()指定当前线程的栈大小。
这两种方式前者会造成系统内存的浪费,后者需要评估线程大小,使用起来并不方便。
为了解决这个问题,gcc给出了split stacks方法。在运行期间动态调整栈大小。
但是这个方法有两个比较严重的性能问题:

  1. 拆分热点:反复调用的函数,会反复进行栈空间的申请、释放操作。
  2. 重复申请释放内存:栈内存的申请、释放在涉及栈大小调整时,总是会发生。
    go对split stacks进行了优化,提出了连续栈方案。(Contiguous stacks)。

1. go栈

goroutine的栈大小通过stack结构描述。

  • stack.lo: 栈空间的低地址
  • stack.hi: 栈空间的高地址
  • stackguard0: stack.lo+StackGuard,用于stack overlow的检测
  • StackGuard: 保护区大小,常量Linux上为880字节
  • StackSmall: 常量大小为128字节,用于小函数调用的优化
  • StackPreempt: -1314用于抢占判断
  • StackLimit:752字节
type stack struct {
	lo uintptr  
	hi uintptr
}

type g struct {
    stack       stack   // offset known to runtime/cgo
	stackguard0 uintptr // offset known to liblink
	stackguard1 uintptr // offset known to liblink
	。。。
}


const (
	STACKSYSTEM = 0
	StackSystem = STACKSYSTEM
	StackBig    = 4096
	StackSmall  = 128
)

const (
	StackPreempt = -1314 // 0xfff...fade
)

// Initialize StackGuard and StackLimit according to target system.
var StackGuard = 880*stackGuardMultiplier() + StackSystem
var StackLimit = StackGuard - StackSystem - StackSmall

func stackGuardMultiplier() int {
	// On AIX, a larger stack is needed for syscalls.
	if GOOS == "aix" {
		return 2
	}
	return stackGuardMultiplierDefault
}

2. 栈伸缩判断

src\runtime\stack.go
每个goroutine都将g->stackguard设置为指向栈底向上偏移StackGuard字节的位置。每个函数都比较栈顶指针(SP)和g->stackguard,判断是否会发生栈溢出。 为了对带有微小栈的函数减少一些指令,对于栈小于StackSmall=128字节的函数进行了优化处理。对于栈帧巨大的函数,不进行检查,直接进行扩栈调用morestack
在判断栈空间是否需要扩容的时候,可以根据被调用函数栈帧的大小, 分为以下3种情况:

  1. 栈帧 < StackSmall(128字节)
    直接调用函数,不会进行栈检查。也不会触发扩栈。
    可以看到函数声明带有nosplit标记
"".my_func_1 STEXT nosplit size=73 args=0x8 locals=0x70
        0x0000 00000 (stackCheck.go:13) TEXT    "".my_func_1(SB), NOSPLIT|ABIInternal, $112-8
        0x0000 00000 (stackCheck.go:13) SUBQ    $112, SP
        0x0004 00004 (stackCheck.go:13) MOVQ    BP, 104(SP)
        0x0009 00009 (stackCheck.go:13) LEAQ    104(SP), BP
  1. StackSamll(128字节) < 栈帧 < StackBig
    进行栈帧检查,需要时才进行扩栈。
    可以看到调用了CALL runtime.morestack_noctxt(SB),之后又JMP到函数开始重新执行。
"".my_func_3 STEXT size=99 args=0x8 locals=0x7d8
        0x0000 00000 (stackCheck.go:23) TEXT    "".my_func_3(SB), ABIInternal, $2008-8
        0x0000 00000 (stackCheck.go:23) MOVQ    (TLS), CX
        0x0009 00009 (stackCheck.go:23) LEAQ    -1880(SP), AX
        0x0011 00017 (stackCheck.go:23) CMPQ    AX, 16(CX)
        0x0015 00021 (stackCheck.go:23) JLS     92
        0x0017 00023 (stackCheck.go:23) SUBQ    $2008, SP
        0x001e 00030 (stackCheck.go:23) MOVQ    BP, 2000(SP)
        0x0026 00038 (stackCheck.go:23) LEAQ    2000(SP), BP
        ...
        0x005c 00092 (stackCheck.go:23) CALL    runtime.morestack_noctxt(SB)
        0x0061 00097 (stackCheck.go:23) JMP     0
  1. 栈帧 >= StackBig
    不进行栈帧检查,直接扩栈。
    可以看到调用了CALL runtime.morestack_noctxt(SB),之后又JMP到函数开始重新执行。
    尤其,这里还进行了抢占判断0x000d 00013 (stackCheck.go:27) CMPQ SI, $-1314,将g.stack.hiStackPreempt=-1314进行比较判断是否需要扩栈(抢占在扩栈中进行)。
"".my_func_4 STEXT size=126 args=0x8 locals=0x1390
        0x0000 00000 (stackCheck.go:27) TEXT    "".my_func_4(SB), ABIInternal, $5008-8
        0x0000 00000 (stackCheck.go:27) MOVQ    (TLS), CX
        0x0009 00009 (stackCheck.go:27) MOVQ    16(CX), SI
        0x000d 00013 (stackCheck.go:27) CMPQ    SI, $-1314
        0x0014 00020 (stackCheck.go:27) JEQ     119
        0x0016 00022 (stackCheck.go:27) LEAQ    880(SP), AX
        0x001e 00030 (stackCheck.go:27) SUBQ    SI, AX
        0x0021 00033 (stackCheck.go:27) CMPQ    AX, $5760
        0x0027 00039 (stackCheck.go:27) JLS     119
        0x0029 00041 (stackCheck.go:27) SUBQ    $5008, SP
        0x0030 00048 (stackCheck.go:27) MOVQ    BP, 5000(SP)
        ...
        0x0077 00119 (stackCheck.go:27) CALL    runtime.morestack_noctxt(SB)
        0x007c 00124 (stackCheck.go:27) JMP     0        

4. 栈伸缩实现

在汇编代码中src\runtime\asm_amd64.s定义了栈伸缩函数runtime·morestack_noctxt(),实际实现为调用runtime·morestack(),最终调用运行时函数runtime·newstack(SB)

// morestack but not preserving ctxt.
TEXT runtime·morestack_noctxt(SB),NOSPLIT,$0
	MOVL	$0, DX
	JMP	runtime·morestack(SB)
	
TEXT runtime·morestack(SB),NOSPLIT,$0-0
	// Cannot grow scheduler stack (m->g0).
	// Cannot grow signal stack (m->gsignal).
	// Call newstack on m->g0's stack.
	MOVQ	m_g0(BX), BX
	MOVQ	BX, g(CX)
	MOVQ	(g_sched+gobuf_sp)(BX), SP
	CALL	runtime·newstack(SB)
	CALL	runtime·abort(SB)	// crash if newstack returns
	RET

newstack()

src\runtime\stack.go

  1. 判断是否需要抢占,通过gp.stackguard0) == stackPreempt进行判断。以下几种情形不会触发抢占,调用gogo()回到g继续执行。
  • M被锁定
  • M正在分配内存
  • M设置了当前不能抢占
  • M的状态不是运行中
  1. 如果设置了抢占标记,但是出于gc扫描中,则放弃抢占,回到G继续执行。
  2. 执行抢占,抢占后会直接执行新的G。
  3. 没有抢占发生,则将栈扩展到原来的2倍大小。调用gogo()继续执行当前的g。

func newstack() {
	thisg := getg()

	gp := thisg.m.curg

	// 1. 判断是否需要抢占,通过```gp.stackguard0) == stackPreempt```进行判断。
	preempt := atomic.Loaduintptr(&gp.stackguard0) == stackPreempt

    // 2. 需要进行抢占,执行gogo()
	if preempt {
		if thisg.m.locks != 0 || thisg.m.mallocing != 0 || thisg.m.preemptoff != "" || thisg.m.p.ptr().status != _Prunning {
			// Let the goroutine keep running for now.
			// gp->preempt is set, so it will be preempted next time.
			gp.stackguard0 = gp.stack.lo + _StackGuard
			gogo(&gp.sched) // never return
		}
	}

	if preempt {
		// Act like goroutine called runtime.Gosched.
		casgstatus(gp, _Gwaiting, _Grunning)
		// 抢占g
		gopreempt_m(gp) // never return
	}

	// 新栈大小为旧栈的2倍。
	oldsize := gp.stack.hi - gp.stack.lo
	newsize := oldsize * 2
	// 最大栈大小为1GB。
	if newsize > maxstacksize {
		throw("stack overflow")
	}

	casgstatus(gp, _Grunning, _Gcopystack)

    // 复制旧栈到新栈
	copystack(gp, newsize, true)

    // 修改G状态为 _Grunning
	casgstatus(gp, _Gcopystack, _Grunning)
	gogo(&gp.sched)
}

gogo()

gogo直接跳转到g的执行位置继续执行,不会再执行gogo()函数调用之后的代码。

// func gogo(buf *gobuf)
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB), NOSPLIT, $16-8
	MOVQ	buf+0(FP), BX		// gobuf
	MOVQ	gobuf_g(BX), DX
	MOVQ	0(DX), CX		// make sure g != nil
	get_tls(CX)
	MOVQ	DX, g(CX)
	MOVQ	gobuf_sp(BX), SP	// restore SP
	MOVQ	gobuf_ret(BX), AX
	MOVQ	gobuf_ctxt(BX), DX
	MOVQ	gobuf_bp(BX), BP
	MOVQ	$0, gobuf_sp(BX)	// clear to help garbage collector
	MOVQ	$0, gobuf_ret(BX)
	MOVQ	$0, gobuf_ctxt(BX)
	MOVQ	$0, gobuf_bp(BX)
	MOVQ	gobuf_pc(BX), BX
	JMP	BX

抢占g

  1. 修改G状态为可以运行。_Grunning->_Grunnable
  2. 调用dropg(),将m与g解除关系。
  3. 调用globrunqput(),将g放到全局待执行列表sched.runq
  4. 调用schedule()进行goroutine调度,来继续运行其它可运行的G.
func gopreempt_m(gp *g) {
	if trace.enabled {
		traceGoPreempt()
	}
	goschedImpl(gp)
}


func goschedImpl(gp *g) {
	status := readgstatus(gp)
	if status&^_Gscan != _Grunning {
		dumpgstatus(gp)
		throw("bad g status")
	}
	casgstatus(gp, _Grunning, _Grunnable)
	dropg()
	lock(&sched.lock)
	globrunqput(gp)
	unlock(&sched.lock)

	schedule()
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值