目前很多高性能的基礎網絡服務器都是采用的C語言開發的,比如:Nginx、Redis、memcached等,它們都是基于”事件驅動 + 事件回掉函數”的方式實現,也就是采用epoll等作為網絡收發數據包的核心驅動。不少人(包括我自己)都認為“事件驅動 + 事件回掉函數”的編程方法是“反人類”的;因為大多數人都更習慣線性的處理一件事情,做完第一件事情再做第二件事情,并不習慣在N件事情之間頻繁的切換干活。為了解決程序員在開發服務器時需要自己的大腦不斷的“上下文切換”的問題,Go語言引入了一種用戶態線程goroutine來取代編寫異步的事件回掉函數,從而重新回歸到多線程并發模型的線性、同步的編程方式上。
main函數的過程就是首先創建一個監聽套接字,然后用一個for循環不斷的從監聽套接字上Accept新的連接,最后調用echoFunc函數在建立的連接上干活。關鍵代碼是:
每收到一個新的連接,就創建一個“線程”去服務這個連接,因此所有的業務邏輯都可以同步、順序的編寫到echoFunc函數中,再也不用去關心網絡IO是否會阻塞的問題。不管業務多復雜,Go語言的并發服務器的編程模型都是長這個樣子。可以肯定的是,在linux上Go語言寫的網絡服務器也是采用的epoll作為最底層的數據收發驅動,Go語言網絡的底層實現中同樣存在“上下文切換”的工作,只是這個切換工作由runtime的調度器來做了,減少了程序員的負擔。
弄明白網絡庫的底層實現,貌似只要弄清楚echo服務器中的Listen、Accept、Read、Write四個函數的底層實現關系就可以了。本文將采用自底向上的方式來介紹,也就是從最底層到上層的方式,這也是我閱讀源碼的方式。底層實現涉及到的核心源碼文件主要有:
netpoll_epoll.c文件是Linux平臺使用epoll作為網絡IO多路復用的實現代碼,這份代碼可以了解到epoll相關的操作(比如:添加fd到epoll、從epoll刪除fd等),只有4個函數,分別是runtime·netpollinit、runtime·netpollopen、runtime·netpollclose和runtime·netpoll。init函數就是創建epoll對象,open函數就是添加一個fd到epoll中,close函數就是從epoll刪除一個fd,netpoll函數就是從epoll wait得到所有發生事件的fd,并將每個fd對應的goroutine(用戶態線程)通過鏈表返回。用epoll寫過程序的人應該都能理解這份代碼,沒什么特別之處。
runtime·netpollinit函數首先使用runtime·epollcreate1創建epoll實例,如果沒有創建成功,就換用runtime·epollcreate再創建一次。這兩個create函數分別等價于glibc的epoll_create1和epoll_create函數。只是因為Go語言并沒有直接使用glibc,而是自己封裝的系統調用,但功能是等價于glibc的。可以通過man手冊查看這兩個create的詳細信息。
添加fd到epoll中的runtime·netpollopen函數可以看到每個fd一開始都關注了讀寫事件,并且采用的是邊緣觸發,除此之外還關注了一個不常見的新事件EPOLLRDHUP,這個事件是在較新的內核版本添加的,目的是解決對端socket關閉,epoll本身并不能直接感知到這個關閉動作的問題。注意任何一個fd在添加到epoll中的時候就關注了EPOLLOUT事件的話,就立馬產生一次寫事件,這次事件可能是多余浪費的。
epoll操作的相關函數都會在事件驅動的抽象層中去調用,為什么需要這個抽象層呢?原因很簡單,因為Go語言需要跑在不同的平臺上,有Linux、Unix、Mac OS X和Windows等,所以需要靠事件驅動的抽象層來為網絡庫提供一致的接口,從而屏蔽事件驅動的具體平臺依賴實現。runtime/netpoll.goc源文件就是整個事件驅動抽象層的實現,抽象層的核心數據結構是:
每個添加到epoll中的fd都對應了一個PollDesc結構實例,PollDesc維護了讀寫此fd的goroutine這一非常重要的信息。可以大膽的推測一下,網絡IO讀寫操作的實現應該是:當在一個fd上讀寫遇到EAGAIN錯誤的時候,就將當前goroutine存儲到這個fd對應的PollDesc中,同時將goroutine給park住,直到這個fd上再此發生了讀寫事件后,再將此goroutine給ready激活重新運行。事實上的實現大概也是這個樣子的。
事件驅動抽象層主要干的事情就是將具體的事件驅動實現(比如: epoll)通過統一的接口封裝成Go接口供net庫使用,主要的接口也是:創建事件驅動實例、添加fd、刪除fd、等待事件以及設置DeadLine。runtime_pollServerInit負責創建事件驅動實例,runtime_pollOpen將分配一個PollDesc實例和fd綁定起來,然后將fd添加到epoll中,runtime_pollClose就是將fd從epoll中刪除,同時將刪除的fd綁定的PollDesc實例刪除,runtime_pollWait接口是至關重要的,這個接口一般是在非阻塞讀寫發生EAGAIN錯誤的時候調用,作用就是park當前讀寫的goroutine。
runtime中的epoll事件驅動抽象層其實在進入net庫后,又被封裝了一次,這一次封裝從代碼上看主要是為了方便在純Go語言環境進行操作,net庫中的這次封裝實現在net/fd_poll_runtime.go文件中,主要是通過pollDesc對象來實現的:
注意:此處的pollDesc對象不是上文提到的runtime中的PollDesc,相反此處pollDesc對象的runtimeCtx成員才是指向的runtime的PollDesc實例。pollDesc對象主要就是將runtime的事件驅動抽象層給再封裝了一次,供網絡fd對象使用。
pollDesc對象最需要關注的就是其Init方法,這個方法通過一個sync.Once變量來調用了runtime_pollServerInit函數,也就是創建epoll實例的函數。意思就是runtime_pollServerInit函數在整個進程生命周期內只會被調用一次,也就是只會創建一次epoll實例。epoll實例被創建后,會調用runtime_pollOpen函數將fd添加到epoll中。
網絡編程中的所有socket fd都是通過netFD對象實現的,netFD是對網絡IO操作的抽象,linux的實現在文件net/fd_unix.go中。netFD對象實現有自己的init方法,還有完成基本IO操作的Read和Write方法,當然除了這三個方法以外,還有很多非常有用的方法供用戶使用。
// Network file descriptor.
netFD對象的init函數僅僅是調用了pollDesc實例的Init函數,作用就是將fd添加到epoll中,如果這個fd是第一個網絡socket fd的話,這一次init還會擔任創建epoll實例的任務。要知道在Go進程里,只會有一個epoll實例來管理所有的網絡socket fd,這個epoll實例也就是在第一個網絡socket fd被創建的時候所創建。
上面代碼段是從netFD的Read方法中摘取,重點關注這個for循環中的syscall.Read調用的錯誤處理。當有錯誤發生的時候,會檢查這個錯誤是否是syscall.EAGAIN,如果是,則調用WaitRead將當前讀這個fd的goroutine給park住,直到這個fd上的讀事件再次發生為止。當這個socket上有新數據到來的時候,WaitRead調用返回,繼續for循環的執行。這樣的實現,就讓調用netFD的Read的地方變成了同步“阻塞”方式編程,不再是異步非阻塞的編程方式了。netFD的Write方法和Read的實現原理是一樣的,都是在碰到EAGAIN錯誤的時候將當前goroutine給park住直到socket再次可寫為止。
本文只是將網絡庫的底層實現給大體上引導了一遍,知道底層代碼大概實現在什么地方,方便結合源碼深入理解。Go語言中的高并發、同步阻塞方式編程的關鍵其實是”goroutine和調度器”,針對網絡IO的時候,我們需要知道EAGAIN這個非常關鍵的調度點,掌握了這個調度點,即使沒有調度器,自己也可以在epoll的基礎上配合協程等用戶態線程實現網絡IO操作的調度,達到同步阻塞編程的目的。
最后,為什么需要同步阻塞的方式編程?只有看多、寫多了異步非阻塞代碼的時候才能夠深切體會到這個問題。真正的高大上絕對不是——“別人不會,我會;別人寫不出來,我寫得出來。”
http://ju.outofmemory.cn/entry/168649
本文分析了Golang的socket文件描述符和goroutine阻塞調度的原理。代碼中大部分是Go代碼,小部分是匯編代碼。完整理解本文需要Go語言知識,并且用Golang寫過網絡程序。更重要的是,需要提前理解goroutine的調度原理。
1. TCP的連接對象:
連接對象:
在net.go中有一個名為Conn的接口,提供了對于連接的讀寫和其他操作:
type Conn interface {
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
Close() error
LocalAddr() Addr
RemoteAddr() Addr
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
}
這個接口就是對下面的結構體conn的抽象。conn結構體包含了對連接的讀寫和其他操作:
type conn struct {
fd *netFD
}
從連接讀取數據:
// Read implements the Conn Read method.
func (c *conn) Read(b []byte) (int, error) {
if !c.ok() {
return 0, syscall.EINVAL
}
return c.fd.Read(b)
}
向連接寫入數據:
// Write implements the Conn Write method.
func (c *conn) Write(b []byte) (int, error) {
if !c.ok() {
return 0, syscall.EINVAL
}
return c.fd.Write(b)
}
關閉連接:
// Close closes the connection.
func (c *conn) Close() error {
if !c.ok() {
return syscall.EINVAL
}
return c.fd.Close()
}
設置讀寫超時:
// SetDeadline implements the Conn SetDeadline method.
func (c *conn) SetDeadline(t time.Time) error {
if !c.ok() {
return syscall.EINVAL
}
return c.fd.setDeadline(t)
}
// SetReadDeadline implements the Conn SetReadDeadline method.
func (c *conn) SetReadDeadline(t time.Time) error {
if !c.ok() {
return syscall.EINVAL
}
return c.fd.setReadDeadline(t)
}
// SetWriteDeadline implements the Conn SetWriteDeadline method.
func (c *conn) SetWriteDeadline(t time.Time) error {
if !c.ok() {
return syscall.EINVAL
}
return c.fd.setWriteDeadline(t)
}
可以看到,對連接的所有操作,都體現在對*netFD的操作上。我們繼續跟蹤c.fd.Read()函數.
2.文件描述符
net/fd_unix.go:
網絡連接的文件描述符:
// Network file descriptor.
type netFD struct {
// locking/lifetime of sysfd + serialize access to Read and Write methods
fdmu fdMutex
// immutable until Close
sysfd int
family int
sotype int
isConnected bool
net string
laddr Addr
raddr Addr
// wait server
pd pollDesc
}
文件描述符讀取數據:
func (fd *netFD) Read(p []byte) (n int, err error) {
if err := fd.readLock(); err != nil {
return 0, err
}
defer fd.readUnlock()
if err := fd.pd.PrepareRead(); err != nil {
return 0, &OpError{"read", fd.net, fd.raddr, err}
}
// 調用system call,循環從fd.sysfd讀取數據
for {
// 系統調用Read讀取數據
n, err = syscall.Read(int(fd.sysfd), p)
// 如果發生錯誤,則需要處理
// 并且只處理EAGAIN類型的錯誤,其他錯誤一律返回給調用者
if err != nil {
n = 0
// 對于非阻塞的網絡連接的文件描述符,如果錯誤是EAGAIN
// 說明Socket的緩沖區為空,未讀取到任何數據
// 則調用fd.pd.WaitRead,
if err == syscall.EAGAIN {
if err = fd.pd.WaitRead(); err == nil {
continue
}
}
}
err = chkReadErr(n, err, fd)
break
}
if err != nil && err != io.EOF {
err = &OpError{"read", fd.net, fd.raddr, err}
}
return
}
網絡輪詢器
網絡輪詢器是Golang中針對每個socket文件描述符建立的輪詢機制。 此處的輪詢并不是一般意義上的輪詢,而是Golang的runtime在調度goroutine或者GC完成之后或者指定時間之內,調用epoll_wait獲取所有產生IO事件的socket文件描述符。當然在runtime輪詢之前,需要將socket文件描述符和當前goroutine的相關信息加入epoll維護的數據結構中,并掛起當前goroutine,當IO就緒后,通過epoll返回的文件描述符和其中附帶的goroutine的信息,重新恢復當前goroutine的執行。
// Integrated network poller (platform-independent part).
// 網絡輪詢器(平臺獨立部分)
// A particular implementation (epoll/kqueue) must define the following functions:
// 實際的實現(epoll/kqueue)必須定義以下函數:
// func netpollinit() // to initialize the poller,初始化輪詢器
// func netpollopen(fd uintptr, pd *pollDesc) int32 // to arm edge-triggered notifications, 為fd和pd啟動邊緣觸發通知
// and associate fd with pd.
// 一個實現必須調用下面的函數,用來指示pd已經準備好
// An implementation must call the following function to denote that the pd is ready.
// func netpollready(gpp **g, pd *pollDesc, mode int32)
// pollDesc contains 2 binary semaphores, rg and wg, to park reader and writer
// goroutines respectively. The semaphore can be in the following states:
// pollDesc包含了2個二進制的信號,分別負責讀寫goroutine的暫停.
// 信號可能處于下面的狀態:
// pdReady - IO就緒通知被掛起;
// 一個goroutine將次狀態置為nil來消費一個通知。
// pdReady - io readiness notification is pending;
// a goroutine consumes the notification by changing the state to nil.
// pdWait - 一個goroutine準備暫停在信號上,但是還沒有完成暫停。
// 這個goroutine通過把這個狀態改變為G指針去提交這個暫停動作。
// 或者,替代性的,并行的其他通知將狀態改變為READY.
// 或者,替代性的,并行的超時/關閉會將次狀態變為nil
// pdWait - a goroutine prepares to park on the semaphore, but not yet parked;
// the goroutine commits to park by changing the state to G pointer,
// or, alternatively, concurrent io notification changes the state to READY,
// or, alternatively, concurrent timeout/close changes the state to nil.
// G指針 - 阻塞在信號上的goroutine
// IO通知或者超時/關閉會分別將此狀態置為READY或者nil.
// G pointer - the goroutine is blocked on the semaphore;
// io notification or timeout/close changes the state to READY or nil respectively
// and unparks the goroutine.
// nil - nothing of the above.
const (
pdReady uintptr = 1
pdWait uintptr = 2
)
網絡輪詢器的數據結構如下:
// Network poller descriptor.
// 網絡輪詢器描述符
type pollDesc struct {
link *pollDesc // in pollcache, protected by pollcache.lock
// The lock protects pollOpen, pollSetDeadline, pollUnblock and deadlineimpl operations.
// This fully covers seq, rt and wt variables. fd is constant throughout the PollDesc lifetime.
// pollReset, pollWait, pollWaitCanceled and runtime·netpollready (IO readiness notification)
// proceed w/o taking the lock. So closing, rg, rd, wg and wd are manipulated
// in a lock-free way by all operations.
// NOTE(dvyukov): the following code uses uintptr to store *g (rg/wg),
// that will blow up when GC starts moving objects.
//
// lock鎖對象保護了pollOpen, pollSetDeadline, pollUnblock和deadlineimpl操作。
// 而這些操作又完全包含了對seq, rt, tw變量。
// fd在PollDesc整個生命過程中都是一個常量。
// 處理pollReset, pollWait, pollWaitCanceled和runtime.netpollready(IO就緒通知)不需要用到鎖。
// 所以closing, rg, rd, wg和wd的所有操作都是一個無鎖的操作。
lock mutex // protectes the following fields
fd uintptr
closing bool
seq uintptr // protects from stale timers and ready notifications
rg uintptr // pdReady, pdWait, G waiting for read or nil
rt timer // read deadline timer (set if rt.f != nil)
rd int64 // read deadline
wg uintptr // pdReady, pdWait, G waiting for write or nil
wt timer // write deadline timer
wd int64 // write deadline
user unsafe.Pointer // user settable cookie
}
將當前goroutine設置為阻塞在fd上:
pd.WaitRead():
func (pd *pollDesc) WaitRead() error {
return pd.Wait('r')
}
func (pd *pollDesc) Wait(mode int) error {
res := runtime_pollWait(pd.runtimeCtx, mode)
return convertErr(res)
}
res是runtime_pollWait函數返回的結果,由conevertErr函數包裝后返回:
func convertErr(res int) error {
switch res {
case 0:
return nil
case 1:
return errClosing
case 2:
return errTimeout
}
println("unreachable: ", res)
panic("unreachable")
}
函數返回0,表示IO已經準備好,返回nil。
返回1,說明連接已關閉,應該放回errClosing。
返回2,說明對IO進行的操作發生超時,應該返回errTimeout。
runtime_pollWait會調用runtime/thunk.s中的函數:
TEXT net·runtime_pollWait(SB),NOSPLIT,$0-0
JMP runtime·netpollWait(SB)
這是一個包裝函數,沒有參數,直接跳轉到runtime/netpoll.go中的函數netpollWait:
func netpollWait(pd *pollDesc, mode int) int {
// 檢查pd的狀態是否異常
err := netpollcheckerr(pd, int32(mode))
if err != 0 {
return err
}
// As for now only Solaris uses level-triggered IO.
if GOOS == "solaris" {
onM(func() {
netpollarm(pd, mode)
})
}
// 循環中檢查pd的狀態是不是已經被設置為pdReady
// 即檢查IO是不是已經就緒
for !netpollblock(pd, int32(mode), false) {
err = netpollcheckerr(pd, int32(mode))
if err != 0 {
return err
}
// Can happen if timeout has fired and unblocked us,
// but before we had a chance to run, timeout has been reset.
// Pretend it has not happened and retry.
}
return 0
}
netpollcheckerr函數檢查pd是否出現異常:
// 檢查pd的異常
func netpollcheckerr(pd *pollDesc, mode int32) int {
// 是否已經關閉
if pd.closing {
return 1 // errClosing
}
// 當讀寫狀態下,deadline小于0,表示pd已經過了超時時間
if (mode == 'r' && pd.rd < 0) || (mode == 'w' && pd.wd < 0) {
return 2 // errTimeout
}
// 正常情況返回0
return 0
}
netpollblock():
// returns true if IO is ready, or false if timedout or closed
// waitio - wait only for completed IO, ignore errors
// 這個函數被netpollWait循環調用
// 返回true說明IO已經準備好,返回false說明IO操作已經超時或者已經關閉
func netpollblock(pd *pollDesc, mode int32, waitio bool) bool {
// 獲取pd的rg
gpp := &pd.rg
// 如果模式是w,則獲取pd的wg
if mode == 'w' {
gpp = &pd.wg
}
// set the gpp semaphore to WAIT
// 在循環中設置pd的gpp為pdWait
// 因為casuintptr是自旋鎖,所以需要在循環中調用
for {
// 如果在循環中發現IO已經準備好(pg的rg或者wg為pdReady狀態)
// 則設置rg/wg為0,返回true
old := *gpp
if old == pdReady {
*gpp = 0
return true
}
// 每次netpollblock執行完畢之后,gpp重置為0
// 非0表示重復wait
if old != 0 {
gothrow("netpollblock: double wait")
}
// CAS操作改變gpp為pdWait
if casuintptr(gpp, 0, pdWait) {
break
}
}
// need to recheck error states after setting gpp to WAIT
// this is necessary because runtime_pollUnblock/runtime_pollSetDeadline/deadlineimpl
// do the opposite: store to closing/rd/wd, membarrier, load of rg/wg
//
// 當設置gpp為pdWait狀態后,重新檢查gpp的狀態
// 這是必要的,因為runtime_pollUnblock/runtime_pollSetDeadline/deadlineimpl會做相反的操作
// 如果狀態正常則掛起當前的goroutine
//
// 當netpollcheckerr檢查io出現超時或者錯誤,waitio為true可用于等待ioReady
// 否則當waitio為false, 且io不出現錯誤或者超時才會掛起當前goroutine
if waitio || netpollcheckerr(pd, mode) == 0 {
// 解鎖函數,設置gpp為pdWait,如果設置不成功
// 說明已經是發生其他事件,可以讓g繼續運行,而不是掛起當前g
f := netpollblockcommit
// 嘗試掛起當前g
gopark(**(**unsafe.Pointer)(unsafe.Pointer(&f)), unsafe.Pointer(gpp), "IO wait")
}
// be careful to not lose concurrent READY notification
old := xchguintptr(gpp, 0)
if old > pdWait {
gothrow("netpollblock: corrupted state")
}
return old == pdReady
}
runtime/proc.go: gopark():
// Puts the current goroutine into a waiting state and calls unlockf.
// If unlockf returns false, the goroutine is resumed.
// 將當前goroutine置為waiting狀態,然后調用unlockf
func gopark(unlockf unsafe.Pointer, lock unsafe.Pointer, reason string) {
// 獲取當前M
mp := acquirem()
// 獲取當前G
gp := mp.curg
// 獲取G的狀態
status := readgstatus(gp)
// 如果不是_Grunning或者_Gscanrunning,則報錯
if status != _Grunning && status != _Gscanrunning {
gothrow("gopark: bad g status")
}
// 設置lock和unlockf
mp.waitlock = lock
mp.waitunlockf = unlockf
gp.waitreason = reason
releasem(mp)
// can't do anything that might move the G between Ms here.
// 在m->g0這個棧上調用park_m,而不是當前g的棧
mcall(park_m)
}
mcall函數是一段匯編,在m->g0的棧上調用park_m,而不是在當前goroutine的棧上。mcall的功能分兩部分,第一部分保存當前G的PC/SP到G的gobuf的pc/sp字段,第二部分調用park_m函數:
// func mcall(fn func(*g))
// Switch to m->g0's stack, call fn(g).
// Fn must never return. It should gogo(&g->sched)
// to keep running g.
TEXT runtime·mcall(SB), NOSPLIT, $0-8
// 將需要執行的函數保存在DI
MOVQ fn+0(FP), DI
// 將M的TLS存放在CX
get_tls(CX)
// 將G對象存放在AX
MOVQ g(CX), AX // save state in g->sched
// 將調用者的PC存放在BX
MOVQ 0(SP), BX // caller's PC
// 將調用者的PC保存到g->sched.pc
MOVQ BX, (g_sched+gobuf_pc)(AX)
// 第一個參數的地址,即棧頂的地址,保存到BX
LEAQ fn+0(FP), BX // caller's SP
// 保存SP的地址到g->sched.sp
MOVQ BX, (g_sched+gobuf_sp)(AX)
// 將g對象保存到g->sched->g
MOVQ AX, (g_sched+gobuf_g)(AX)
// switch to m->g0 & its stack, call fn
// 將g對象指針保存到BX
MOVQ g(CX), BX
// 將g->m保存到BX
MOVQ g_m(BX), BX
// 將m->g0保存到SI
MOVQ m_g0(BX), SI
CMPQ SI, AX // if g == m->g0 call badmcall
JNE 3(PC)
MOVQ $runtime·badmcall(SB), AX
JMP AX
// 將m->g0保存到g
MOVQ SI, g(CX) // g = m->g0
// 將g->sched.sp恢復到SP寄存器
// 即使用g0的棧
MOVQ (g_sched+gobuf_sp)(SI), SP // sp = m->g0->sched.sp
// AX進棧
PUSHQ AX
MOVQ DI, DX
// 將fn的地址復制到DI
MOVQ 0(DI), DI
// 調用函數
CALL DI
// AX出棧
POPQ AX
MOVQ $runtime·badmcall2(SB), AX
JMP AX
RET
park_m函數的功能分為三部分,第一部分讓當前G和當前M脫離關系,第二部分是調用解鎖函數,這里是調用netpoll.go源文件中的netpollblockcommit函數:
// runtime·park continuation on g0.
void
runtime·park_m(G *gp)
{
bool ok;
// 設置當前g為Gwaiting狀態
runtime·casgstatus(gp, Grunning, Gwaiting);
// 讓當前g和m脫離關系
dropg();
if(g->m->waitunlockf) {
ok = g->m->waitunlockf(gp, g->m->waitlock);
g->m->waitunlockf = nil;
g->m->waitlock = nil;
// 返回0為false,非0為true
// 0說明g->m->waitlock發生了變化,即不是在gopark是設置的(pdWait)
// 說明了脫離了WAIT狀態,應該設置為Grunnable,并執行g
if(!ok) {
runtime·casgstatus(gp, Gwaiting, Grunnable);
execute(gp); // Schedule it back, never returns.
}
}
// 這里是調度當前m繼續執行其他g
// 而不是上面執行execute
schedule();
}
netpollblockcommit函數,設置gpp為pdWait,設置成功返回1,否則返回0。1為true,0為false:
func netpollblockcommit(gp *g, gpp unsafe.Pointer) bool {
return casuintptr((*uintptr)(gpp), pdWait, uintptr(unsafe.Pointer(gp)))
}
到這里當前goroutine對socket文件描述符的等待IO繼續的行為已經完成。過程中首先盡早嘗試判斷IO是否已經就緒,如果未就緒則掛起當前goroutine,掛起之后再次判斷IO是否就緒,如果還未就緒則調度當前M運行其他G。如果是在調度goroutine之前IO已經就緒,則不會使當前goroutine進入調度隊列,會直接運行剛才掛起的G。否則當前goroutine會進入調度隊列。
接下來是等待runtime將其喚醒。runtime在執行findrunnablequeue、starttheworld,sysmon函數時,都會調用netpoll_epoll.go中的netpoll函數,尋找到IO就緒的socket文件描述符,并找到這些socket文件描述符對應的輪詢器中附帶的信息,根據這些信息將之前等待這些socket文件描述符就緒的goroutine狀態修改為Grunnable。在以上函數中,執行完netpoll之后,會找到一個就緒的goroutine列表,接下來將就緒的goroutine加入到調度隊列中,等待調度運行。
在netpoll_epoll.go中的netpoll函數中,epoll_wait函數返回N個發生事件的文件描述符對應的epollevent,接著對于每個event使用其data屬性,將event.data轉換為*pollDesc類型,再調用netpoll.go中的netpollready函數,將*pollDesc類型中的G數據類型去除,并附加到netpoll函數的調用者傳遞的G鏈表中:
// 將ev.data轉換為*pollDesc類型
pd := *(**pollDesc)(unsafe.Pointer(&ev.data))
// 調用netpollready將取出pd中保存的G,并添加到鏈表中
netpollready((**g)(noescape(unsafe.Pointer(&gp))), pd, mode)
所以runtime在執行findrunnablequeue、starttheworld,sysmon函數中會執行netpoll函數,并返回N個goroutine。這些goroutine期待的網絡事件已經發生,runtime會將這些goroutine放入到當前P的可運行隊列中,接下來調度它們并運行。