這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
說明
前面的章節我們基本聊完了golang網路編程的關鍵API流程,但遺留了一個關鍵內容:當系統調用返回EAGAIN時,會調用WaitRead/WaitWrite來阻塞當前協程,現在我們接著聊。
WaitRead/WaitWrite
func (pd *pollDesc) Wait(mode int) error { res := runtime_pollWait(pd.runtimeCtx, mode) return convertErr(res)}func (pd *pollDesc) WaitRead() error { return pd.Wait('r')}func (pd *pollDesc) WaitWrite() error { return pd.Wait('w')}
最終runtime_pollWait走到下面去了:
TEXT net·runtime_pollWait(SB),NOSPLIT,$0-0 JMP runtime·netpollWait(SB)
我們仔細考慮應該明白:netpollWait的主要作用是:等待關心的socket是否有事件(其實後面我們知道只是等待一個標記位是否發生改變),如果沒有事件,那麼就將當前的協程掛起,直到有通知事件發生,我們接下來看看到底如何?:
func netpollWait(pd *pollDesc, mode int) int { // 先檢查該socket是否有error發生(如關閉、逾時等) 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) }) } // 迴圈等待netpollblock傳回值為true // 如果傳回值為false且該socket未出現任何錯誤 // 那該協程可能被意外喚醒,需要重新被掛起 // 還有一種可能:該socket由於逾時而被喚醒 // 此時netpollcheckerr就是用來檢測逾時錯誤的 for !netpollblock(pd, int32(mode), false) { err = netpollcheckerr(pd, int32(mode)) if err != 0 { return err } } return 0 }func netpollblock(pd *pollDesc, mode int32, waitio bool) bool { gpp := &pd.rg if mode == 'w' { gpp = &pd.wg } // set the gpp semaphore to WAIT // 首先將輪詢狀態設定為pdWait // 為什麼要使用for呢?因為casuintptr使用了自旋鎖 // 為什麼使用自旋鎖就要加for迴圈呢? for { old := *gpp if old == pdReady { *gpp = 0 return true } if old != 0 { gothrow("netpollblock: double wait") } // 將socket輪詢相關的狀態設定為pdWait if casuintptr(gpp, 0, pdWait) { break } } // 如果未出錯將該協程掛起,解鎖函數是netpollblockcommit if waitio || netpollcheckerr(pd, mode) == 0 { f := netpollblockcommit gopark(**(**unsafe.Pointer)(unsafe.Pointer(&f)), unsafe.Pointer(gpp), "IO wait") } // 可能是被掛起的協程被喚醒 // 或者由於某些原因該協程壓根未被掛起 // 擷取其目前狀態記錄在old中 old := xchguintptr(gpp, 0) if old > pdWait { gothrow("netpollblock: corrupted state") } return old == pdReady}
從上面的分析我們看到,如果無法讀寫,golang會將當前協程掛起,在協程被喚醒的時候,該標記位應該會被置位。 我們接下來看看這些掛起的協程何時會被喚醒。
事件通知
golang運行庫在系統運行過程中存在socket事件檢查點,目前,該檢查點主要位於以下幾個地方:
runtime·startTheWorldWithSema(void):在完成gc後;
findrunnable():這個暫時不知道何時會觸發?
sysmon:golang中的監控協程,會周期性檢查就緒socket
TODO: 為什麼是在這些地方檢查socket就緒事件呢?
接下來我們看看如何檢查socket就緒事件,在socket就緒後又是如何喚醒被掛起的協程?主要調用函數runtime-netpoll()
我們只關注epoll的實現,對於epoll,上面的方法具體實現是netpoll_epoll.go中的netpoll
func netpoll(block bool) (gp *g) { if epfd == -1 { return } waitms := int32(-1) if !block { // 如果調用者不希望block // 設定waitsm為0 waitms = 0 } var events [128]epolleventretry: // 調用epoll_wait擷取就緒事件 n := epollwait(epfd, &events[0], int32(len(events)), waitms) if n < 0 { ... } goto retry } for i := int32(0); i < n; i++ { ev := &events[i] if ev.events == 0 { continue } var mode int32 if ev.events&(_EPOLLIN|_EPOLLRDHUP|_EPOLLHUP|_EPOLLERR) != 0 { mode += 'r' } if ev.events&(_EPOLLOUT|_EPOLLHUP|_EPOLLERR) != 0 { mode += 'w' } // 對每個事件,調用了netpollready // pd主要記錄了與該socket關聯的等待協程 if mode != 0 { pd := *(**pollDesc)(unsafe.Pointer(&ev.data)) netpollready((**g)(noescape(unsafe.Pointer(&gp))), pd, mode) } } // 如果調用者同步等待且本次未擷取到就緒socket // 繼續重試 if block && gp == nil { goto retry } return gp}
這個函數主要調用epoll_wait(當然,golang封裝了系統調用)來擷取就緒socket fd,對每個就緒的fd,調用netpollready()作進一步處理。這個函數的最終傳回值就是一個已經就緒的協程(g)鏈表。
netpollready主要是將該socket fd標記為IOReady,並喚醒等待在該fd上的協程g,將其添加到傳入的g鏈表中。
// make pd ready, newly runnable goroutines (if any) are returned in rg/wg func netpollready(gpp **g, pd *pollDesc, mode int32) { var rg, wg *g if mode == 'r' || mode == 'r'+'w' { rg = netpollunblock(pd, 'r', true) } if mode == 'w' || mode == 'r'+'w' { wg = netpollunblock(pd, 'w', true) } // 將就緒協程添加至鏈表中 if rg != nil { rg.schedlink = *gpp *gpp = rg } if wg != nil { wg.schedlink = *gpp *gpp = wg }}// 將pollDesc的狀態置為pdReady並返回就緒協程 func netpollunblock(pd *pollDesc, mode int32, ioready bool) *g { gpp := &pd.rg if mode == 'w' { gpp = &pd.wg } for { old := *gpp if old == pdReady { return nil } if old == 0 && !ioready { return nil } var new uintptr if ioready { new = pdReady } if casuintptr(gpp, old, new) { if old == pdReady || old == pdWait { old = 0 } return (*g)(unsafe.Pointer(old)) } }}
疑問:一個fd會被多個協程同時進行IO嗎?比如一個協程讀,另外一個協程寫?或者多個協程同時讀?此時返回的是哪個協程就緒呢?
一個socket fd可支援並發讀寫,因為對於tcp協議來說,是全雙工系統。讀寫操作的是不同緩衝區,但是不支援並發讀和並發寫,因為這樣會錯亂的。所以上面的netFD.RWLock()就是幹這個作用的。