Golang網路:核心API實現剖析二)

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

說明

前面的章節我們基本聊完了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()就是幹這個作用的。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.