内部实现(三)
上面的章节中我们大概提到了在哪些情景下会检查哪些套接字上有可读写事件发生。接下来我们就仔细描述下这些场景并看看有事件发生后到底golang内部如何处理这些事件。
sysmon
golang的后台监控协程,会定期运行,检测系统状态。其中一个重要的任务就是检测是否有就绪的socket事件。如下:
func sysmon() {
......
// poll network if not polled for more than 10ms
lastpoll := int64(atomicload64(&sched.lastpoll))
now := nanotime()
unixnow := unixnanotime()
if lastpoll != 0 && lastpoll+10*1000*1000 < now {
cas64(&sched.lastpoll, uint64(lastpoll), uint64(now))
// non-blocking - returns list of goroutines
gp := netpoll(false)
if gp != nil {
// Need to decrement number of idle locked M's
// (pretending that one more is running) before injectglist.
// Otherwise it can lead to the following situation:
// injectglist grabs all P's but before it starts M's to run the P's,
// another M returns from syscall, finishes running its G,
// observes that there is no work to do and no other running M's
// and reports deadlock.
incidlelocked(-1)
injectglist(gp)
incidlelocked(1)
}
}
......
}
在sysmon中调用netpoll()函数,获得当前就绪的socket事件,返回等待在这些事件上的协程g链表。 注意,这里调用netpoll()时传入的参数为false,表示告诉netpoll()内部无需wait。如果没有就绪的socket事件就立即返回。
findrunnable
该函数在调用schedule()时触发,用于找到当前可运行的协程。这里也有可能会触发等待网络事件。
// Finds a runnable goroutine to execute.
// Tries to steal from other P's, get g from global queue, poll network.
func findrunnable() (gp *g, inheritTime bool) {
_g_ := getg()
......
// Poll network.
// This netpoll is only an optimization before we resort to stealing.
// We can safely skip it if there a thread blocked in netpoll already.
// If there is any kind of logical race with that blocked thread
// (e.g. it has already returned from netpoll, but does not set lastpoll yet),
// this thread will do blocking netpoll below anyway.
if netpollinited() && sched.lastpoll != 0 {
if gp := netpoll(false); gp != nil { // non-blocking
// netpoll returns list of goroutines linked by schedlink.
injectglist(gp.schedlink.ptr())
casgstatus(gp, _Gwaiting, _Grunnable)
if trace.enabled {
traceGoUnpark(gp, 0)
}
return gp, false
}
...
}
如果线程m.p没有可运行g且全局g队列也没有可运行g,此时就需要看看有没有网络事件就绪,如果有,那最好,可以用当前等待在这些事件上的g来运行。
这里注意:调用netpoll()时传入的参数也是false,调用者并不想阻塞。因为它在后面还可以通过其他手段来拿到可运行的g。
startTheWorld
在做完gc后,golang也会查看下是否有网络事件在等待。但这里想不出有什么特别的需求要这么做。
对唤醒网络事件协程的处理
上面我们仔细描述了golang中何时会去检测网络事件。接下来我们要描述的是检测到就绪网络事件后,并获得了等待在这些网络事件上的协程后,该如何处理这些协程。
前面章节我们仔细描述了一个协程如何阻塞在网络IO上。等到这些网络IO就绪后,在上面描述的三种场景中可以获得那些阻塞的协程。接下来调用injectglist()处理这些协程。
// Injects the list of runnable G's into the scheduler.
// Can run concurrently with GC.
func injectglist(glist *g) {
if glist == nil {
return
}
if trace.enabled {
for gp := glist; gp != nil; gp = gp.schedlink.ptr() {
traceGoUnpark(gp, 0)
}
}
lock(&sched.lock)
var n int
for n = 0; glist != nil; n++ {
gp := glist
glist = gp.schedlink.ptr()
casgstatus(gp, _Gwaiting, _Grunnable)
globrunqput(gp)
}
unlock(&sched.lock)
for ; n != 0 && sched.npidle != 0; n-- {
startm(nil, false)
}
}
顾名思义,这个函数是要将获得的协程列表插入到某个地方,看代码知道时插入到了全局的可运行g队列。为什么是插入到这里呢? 因为这些被阻塞的协程在阻塞的时候都调用了dropg()来放弃自己的m。这样他们也就没有了所属的p。
因此,我们只能将这些g添加到全局的可运行g队列中。这样,在下次调度时,这些g就有可能得到执行的机会。