摘要
goruntine是内建于golang的协程技术,被誉为轻量级线程。操作系统的内核线程是一般都支持分时调度功能,而这里通过源码分析goruntine的分时调度机制。
实现原理
go的分时切换原理很简单,只涉及两点:
- 设置超时标志位,通过定时器定时检测goruntine的运行时长,如果超过一定的时间则对goruntine设置标志位代表需要被挂起,这个标志位其实就是结构体g的成员stackguard0(后面以<g.stackguard0>来表示)。
- 超时调度,即主动挂起逻辑,每次调用go函数前都会去检查自己所在的goruntine的标志位,是否需要被挂起。简而言之就是每个go函数前面都被插入了检测goruntine运行超时的代码。
分析手段
- dlv调试
- 源码
预备知识
- 每个goruntine有一个结构体g来维持状态,可以说一个g可以代表一个goruntine。
- 以下分析基于windows上64位的go。
设置超时标志位
根据函数调用链 “runtime.sysmon->runtime.retake->runtime.preemptone”可以看到:
- 在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_)
}
复制代码
- 在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
复制代码
超时调度
进入挂起流程
- 先看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
- 找到挂起流程的入口 源代码是:
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
复制代码
可以看到:
- 前两行汇编等价于getg获取当前goruntine的g。
- 第三第四行就是拿rsp和 <g.stackguard0>,如果rsp更小(blow equal)则跳转到地址0x4af3e0执行runtime.morestack_noctxt.
- 当挂起标志位<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
}
复制代码
有疑问加站长微信联系(非本文作者)