goroutine的分时调度解析
2009 年 7 月 4 日
goruntine是内建于golang的协程技术,被誉为轻量级线程。操作系统的内核线程是一般都支持分时调度功能,而这里通过源码分析goruntine的分时调度机制。
实现原理
go的分时切换原理很简单,只涉及两点:
- 设置超时标志位,通过定时器定时检测goruntine的运行时长,如果超过一定的时间则对goruntine设置标志位代表需要被挂起,这个标志位其实就是结构体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中将变为一个特别大的值stackPreempt
stackPreempt的值代表的地址位于64位地址空间中的极高处,代码如下
uintptrMask = 1<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和 ,如果rsp更小(blow equal)则跳转到地址0x4af3e0执行runtime.morestack_noctxt.
- 当挂起标志位 值为stackPreempt时就会执行runtime.morestack_noctxt
根据观察发现,只有当函数比较复杂时,编译器才会在函数头加入超时调度的代码,所以上面fun1调用了fun2是为了增加fun1的复杂度
至于为什么 [rcx+0x10]是,因为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 } 复制代码