阅读 544

goroutine的分时调度解析

摘要

goruntine是内建于golang的协程技术,被誉为轻量级线程。操作系统的内核线程是一般都支持分时调度功能,而这里通过源码分析goruntine的分时调度机制。

实现原理

go的分时切换原理很简单,只涉及两点:

  1. 设置超时标志位,通过定时器定时检测goruntine的运行时长,如果超过一定的时间则对goruntine设置标志位代表需要被挂起,这个标志位其实就是结构体g的成员stackguard0(后面以<g.stackguard0>来表示)。
  2. 超时调度,即主动挂起逻辑,每次调用go函数前都会去检查自己所在的goruntine的标志位,是否需要被挂起。简而言之就是每个go函数前面都被插入了检测goruntine运行超时的代码。

分析手段

  1. dlv调试
  2. 源码

预备知识

  1. 每个goruntine有一个结构体g来维持状态,可以说一个g可以代表一个goruntine。
  2. 以下分析基于windows上64位的go。

设置超时标志位

根据函数调用链 “runtime.sysmon->runtime.retake->runtime.preemptone”可以看到:

  1. 在runtime.retake判断g所在的P运行时间是否超时
} else if s == _Prunning {
	// Preempt G if it's running for too long.
	t := int64(_p_.schedtick)
	if int64(pd.schedtick) != t {
		pd.schedtick = uint32(t)
		pd.schedwhen = now
		continue
	}
	if pd.schedwhen+forcePreemptNS > now {
		continue
	}
	preemptone(_p_)
}
复制代码
  1. 在runtime.preemptone中将<g.stackguard0>变为一个特别大的值stackPreempt

stackPreempt的值代表的地址位于64位地址空间中的极高处,代码如下

uintptrMask = 1<<(8*sys.PtrSize) - 1

// Goroutine preemption request.
// Stored into g->stackguard0 to cause split stack check failure.
// Must be greater than any real sp.
// 0xfffffade in hex.
stackPreempt = uintptrMask & -1314

复制代码

超时调度

进入挂起流程

  1. 先看getg的实现.
// func getg() *g 
proc.go:3241    0x437cea        65488b0c2528000000      mov rcx, qword ptr gs:[0x28]
proc.go:3241    0x437cf3        488b8900000000               mov rcx, qword ptr [rcx]
复制代码

使用go关键字写一段代码就会执行runtime.newproc,runtime.newproc 就调用了getg

然后通过dlv调试可以分析出getg的源码

runtime.newproc的源代码位于runtime/proc.go

  1. 找到挂起流程的入口 源代码是:
func fun1() int {
    return fun2()
}

func fun2() int {
    i := 0
	for i < 100 {
        i++
    }
    return i
}

复制代码
//fun1的汇编代码
main.go:26      0x4af350        65488b0c2528000000      mov rcx, qword ptr gs:[0x28]
main.go:26      0x4af359        488b8900000000          mov rcx, qword ptr [rcx]
main.go:26      0x4af360        483b6110                cmp rsp, qword ptr [rcx+0x10]
main.go:26      0x4af364        767a                    jbe 0x4af3e0                                1->1
main.go:26      0x4af366        4883ec68                sub rsp, 0x68
main.go:26      0x4af36a        48896c2460              mov qword ptr [rsp+0x60], rbp
main.go:26      0x4af36f        488d6c2460              lea rbp, ptr [rsp+0x60]
main.go:27      0x4af374        0f57c0                  xorps xmm0, xmm0
main.go:27      0x4af377        0f11442438              movups xmmword ptr [rsp+0x38], xmm0
main.go:27      0x4af37c        488d442438              lea rax, ptr [rsp+0x38]
main.go:27      0x4af381        4889442430              mov qword ptr [rsp+0x30], rax
main.go:27      0x4af386        8400                    test byte ptr [rax], al
main.go:27      0x4af388        488d0d71260100          lea rcx, ptr [_image_base__+793088]
main.go:27      0x4af38f        48894c2438              mov qword ptr [rsp+0x38], rcx
main.go:27      0x4af394        488d0d55480400          lea rcx, ptr [_image_base__+998384]
main.go:27      0x4af39b        48894c2440              mov qword ptr [rsp+0x40], rcx
main.go:27      0x4af3a0        8400                    test byte ptr [rax], al
main.go:27      0x4af3a2        eb00                    jmp 0x4af3a4
main.go:27      0x4af3a4        4889442448              mov qword ptr [rsp+0x48], rax
main.go:27      0x4af3a9        48c744245001000000      mov qword ptr [rsp+0x50], 0x1
main.go:27      0x4af3b2        48c744245801000000      mov qword ptr [rsp+0x58], 0x1
main.go:27      0x4af3bb        48890424                mov qword ptr [rsp], rax
main.go:27      0x4af3bf        48c744240801000000      mov qword ptr [rsp+0x8], 0x1
main.go:27      0x4af3c8        48c744241001000000      mov qword ptr [rsp+0x10], 0x1
main.go:27      0x4af3d1        e80aa0ffff              call $fmt.Println
main.go:28      0x4af3d6        488b6c2460              mov rbp, qword ptr [rsp+0x60]
main.go:28      0x4af3db        4883c468                add rsp, 0x68
main.go:28      0x4af3df        c3                      ret
main.go:26      0x4af3e0        e82b6afaff              call $runtime.morestack_noctxt            1->2
main.go:26      0x4af3e5        e966ffffff              jmp $main.fun1
复制代码

可以看到:

  1. 前两行汇编等价于getg获取当前goruntine的g。
  2. 第三第四行就是拿rsp和 <g.stackguard0>,如果rsp更小(blow equal)则跳转到地址0x4af3e0执行runtime.morestack_noctxt.
  3. 当挂起标志位<g.stackguard0> 值为stackPreempt时就会执行runtime.morestack_noctxt

根据观察发现,只有当函数比较复杂时,编译器才会在函数头加入超时调度的代码,所以上面fun1调用了fun2是为了增加fun1的复杂度

至于为什么 [rcx+0x10]是<g.stackguard0>,因为g的第一个成员stack占用16字节的空间, 故stackguard0起始地址为0x10

分析挂起调度过程

以下就是超时调度的调用链(shedule的代码太过复杂,根据注释应该就是这了): runtime.morestack_noctxt(runtime/asm_amd64.s)->runtime.morestack(runtime/asm_amd64.s)->runtime.newstack(runtime/stack.go)->runtime.gopreempt_m(runtime/proc.go)->runtime.goschedImpl(runtime/proc.go)->runtime.schedule(runtime/proc.go)

//func newstack() 片段
if preempt {
	if gp == thisg.m.g0 {
		throw("runtime: preempt g0")
	}
	if thisg.m.p == 0 && thisg.m.locks == 0 {
		throw("runtime: g is running but p is not")
	}
	// Synchronize with scang.
	casgstatus(gp, _Grunning, _Gwaiting)
	if gp.preemptscan {
		for !castogscanstatus(gp, _Gwaiting, _Gscanwaiting) {
			// Likely to be racing with the GC as
			// it sees a _Gwaiting and does the
			// stack scan. If so, gcworkdone will
			// be set and gcphasework will simply
			// return.
		}
		if !gp.gcscandone {
			// gcw is safe because we're on the
			// system stack.
			gcw := &gp.m.p.ptr().gcw
			scanstack(gp, gcw)
			gp.gcscandone = true
		}
		gp.preemptscan = false
		gp.preempt = false
		casfrom_Gscanstatus(gp, _Gscanwaiting, _Gwaiting)
		// This clears gcscanvalid.
		casgstatus(gp, _Gwaiting, _Grunning)
		gp.stackguard0 = gp.stack.lo + _StackGuard
		gogo(&gp.sched) // never return
	}

	// Act like goroutine called runtime.Gosched.
	casgstatus(gp, _Gwaiting, _Grunning)
	gopreempt_m(gp) // never return
}

复制代码
关注下面的标签,发现更多相似文章
评论