Golang源码学习:监控线程 (2)

runtime.clone:准备系统调用clone的参数;从父线程栈复制mp,gp,fn到寄存器;调用clone;父线程返回;子线程设置sp,m.procid,tls,互相绑定mp与gp。调用mstart作为子线程的开始执行。

mstart->mstart1:调用 _g_.m.mstartfn 指向的函数,也就是sysmon,此时监控工作正式开始。

抢占调度 主体代码

sysmon开始会检查死锁,接下来是函数主体,一个无限循环,每隔一个短时间执行一次。其工作包含网络轮询、抢占调度、垃圾回收。

sysmon中抢占调度代码 func sysmon() { lock(&sched.lock) sched.nmsys++ checkdead() unlock(&sched.lock) lasttrace := int64(0) idle := 0 // how many cycles in succession we had not wokeup somebody delay := uint32(0) // 睡眠时间,开始是20微秒;idle大于50后,翻倍增长;但最大为10毫秒 for { if idle == 0 { // start with 20us sleep... delay = 20 } else if idle > 50 { // start doubling the sleep after 1ms... delay *= 2 } if delay > 10*1000 { // up to 10ms delay = 10 * 1000 } usleep(delay) now := nanotime() ...... // retake P's blocked in syscalls // and preempt long running G's if retake(now) != 0 { idle = 0 } else { idle++ } ...... } } retake

preemptone:抢占运行时间过长的G。

handoffp:尝试为过长时间处在_Psyscall的P关联一个M继续调度。

func retake(now int64) uint32 { n := 0 lock(&allpLock) for i := 0; i < len(allp); i++ { _p_ := allp[i] if _p_ == nil { continue } pd := &_p_.sysmontick s := _p_.status sysretake := false if s == _Prunning || s == _Psyscall { // 如果运行时间太长,则抢占g t := int64(_p_.schedtick) if int64(pd.schedtick) != t { pd.schedtick = uint32(t) pd.schedwhen = now } else if pd.schedwhen+forcePreemptNS <= now { preemptone(_p_) // 在系统调用的情况下,preemptone() 不会工作,因为P没有与之关联的M。 sysretake = true } } // 因为此时P的状态是 _Psyscall,所以是调用过了Syscall(或者Syscall6)开头的 entersyscall 函数,而此函数会解绑P和M,所以 p.m = 0;m.p=0。 if s == _Psyscall { ...... // p的local队列为空 && (存在自旋的m || 存在空闲的p) && 距离上次系统调用不超过10ms ==> 不需要继续执行 if runqempty(_p_) && atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > 0 && pd.syscallwhen+10*1000*1000 > now { continue } ...... // p的状态更改为空闲 if atomic.Cas(&_p_.status, s, _Pidle) { ...... n++ _p_.syscalltick++ handoffp(_p_) // 尝试为p寻找一个m(startm),如果没有寻找到则 pidleput } ...... } } unlock(&allpLock) return uint32(n) } preemptone

协作式抢占调度:设置抢占调度的标记,在下次进行函数调用前会检查此标记,然后调用 runtime.morestack_noctxt 最终抢占当前G

基于信号的异步抢占:给运行时间过长的G的M线程发送 _SIGURG。使其收到信号后执行 doSigPreempt 最终抢占当前G

func preemptone(_p_ *p) bool { mp := _p_.m.ptr() if mp == nil || mp == getg().m { return false } gp := mp.curg if gp == nil || gp == mp.g0 { return false } gp.preempt = true // 设置抢占标记 gp.stackguard0 = stackPreempt // 设置为一个大于任何真实sp的值。 // 基于信号的异步的抢占调度 if preemptMSupported && debug.asyncpreemptoff == 0 { _p_.preempt = true preemptM(mp) } return true } 协作式抢占调度

golang的编译器一般会在函数的汇编代码前后自动添加栈是否需要扩张的检查代码。

0x0000000000458360 <+0>: mov %fs:0xfffffffffffffff8,%rcx # 将当前g的指针存入rcx。tls还记得么? 0x0000000000458369 <+9>: cmp 0x10(%rcx),%rsp # 比较g.stackguard0和rsp。g结构体地址偏移16个字节就是g.stackguard0。 0x000000000045836d <+13>: jbe 0x4583b0 <main.caller+80> # 如果rsp较小,表示栈有溢出风险,调用runtime.morestack_noctxt // 此处省略具体函数汇编代码 0x00000000004583b0 <+80>: callq 0x451b30 <runtime.morestack_noctxt> 0x00000000004583b5 <+85>: jmp 0x458360 <main.caller>

假设上面的汇编代码是属于一个叫 caller 的函数的(实际上确实是的)。

当运行caller的G(暂且称其为gp)由于运行时间过长,被监控线程sysmon通过preemptone函数标记其 gp.preempt = true;gp.stackguard0 = stackPreempt。

当caller被调用时,会先进行栈的检查,因为 stackPreempt 是一个大于任何真实sp的值,所以jbe指令跳转调用 runtime.morestack_noctxt 。

goschedImpl

goschedImpl是抢占调度的关键逻辑,从 morestack_noctxt 到 goschedImpl 的调用链如下:

morestack_noctxt->morestack->newstack->gopreempt_m->goschedImpl。其中 morestack_noctxt 和 morestack 由汇编编写。

goschedImpl 的主要逻辑是:

更改gp的状态为_Grunable,dropg解绑G和M

globrunqput放入全局队列

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zyyfdz.html