聊聊 go 語言基於 epoll 的網絡併發實現


在之前的文章中我們已經介紹了 epoll 模型,而本文就從 go 語言源碼的角度來了解一下,go 語言是如何基於 epoll 模型完成高性能的網絡協程併發程序的。

Hi,我是 sharkChili ,是個不斷在硬核技術上作死的 java coder ,是 CSDN 的博客專家 ,也是開源項目 Java Guide 的維護者之一,熟悉 Java 也會一點 Go ,偶爾也會在 C 源碼 邊緣徘徊。寫過很多有意思的技術博客,也還在研究並輸出技術的路上,希望我的文章對你有幫助,非常歡迎你關注我的公衆號: 寫代碼的 SharkChili

詳解 go 語言對於 epoll 的抽象

從設計角度瞭解 go 語言的網絡協程工作機制

當客戶端和服務端建立連接之後,每一個協程go-routine都會得到對應的establish socket,而 go 語言則會將對應socket的文件描述符 fd 註冊到網絡輪詢器上,如果epoll輪詢到對應的socket的事件,則喚醒對應的協程並處理這些讀寫事件,反之則將協程掛起:

從頂層設計入手

我們可以從netpoll.go這個文件中看到go語言對於多路複用器的抽象,從註釋可以看出任何操作系統的網絡輪詢器都需要實現如下幾個方法:

  1. netpollinit:新建網絡輪詢器。

  2. netpollopen:通過邊緣觸發的方式將這些建立連接的socketfd註冊到網絡輪詢器中。

  3. netpoll:獲取就緒的 socket 的讀寫事件。

這裏我們也貼出這段代碼的註釋:

// func netpollinit()
//     Initialize the poller. Only called once.
//
// func netpollopen(fd uintptr, pd *pollDesc) int32
//     Arm edge-triggered notifications for fd. The pd argument is to pass
//     back to netpollready when fd is ready. Return an errno value.
//
// func netpollclose(fd uintptr) int32
//     Disable notifications for fd. Return an errno value.
//
// func netpoll(delta int64) gList
//     Poll the network. If delta < 0, block indefinitely. If delta == 0,
//     poll without blocking. If delta > 0, block for up to delta nanoseconds.
//     Return a list of goroutines built by calling netpollready.
//

新建網絡輪詢器

我們以Linux爲例,查看對應的實現類netpoll_epoll.go如何完成網絡輪詢器的抽象,可以看到Linux系統下的netpollinit方法,本質就是調用C語言epoll_create方法並創建網絡輪詢器,並將我們服務端 socket 的連接輸入事件註冊到epoll模型上:

func netpollinit() {
 var errno uintptr
 //調用底層C語言實現的epoll_create創建網絡輪詢器
 epfd, errno = syscall.EpollCreate1(syscall.EPOLL_CLOEXEC)
 //......
 //封裝輸入事件ev 
 ev := syscall.EpollEvent{
  Events: syscall.EPOLLIN,
 }
 //......
 //將輸入事件註冊到epoll
 errno = syscall.EpollCtl(epfd, syscall.EPOLL_CTL_ADD, r, &ev)
 //......
}

插入事件

每當客戶端和服務端socket建立連接之後,服務端都會爲當前客戶端創建一個establish socket對應的socket的文件描述符就會被封裝成pollDesc,並將其對應的輸入、輸出等事件通過註冊到epoll上,並將 epoll 設置爲邊緣觸發模式,等待epoll輪詢通知當前socket對應的協程處理。

ps: 這裏簡單介紹一下邊緣觸發,和水平觸發不同,水平觸發只要一有網絡 IO 數據就通知socket處理,而邊緣觸發爲避免這種頻繁在用戶態到內核態的開銷,如果當前 IO 數據沒處理完,則等待下一次 IO 數據就緒後再處理,所以這種模式就要求應用程序必須一次性將數據讀取完成,在應用層面進行處理。

對應的我們也給出這段描述的代碼實現:

func netpollopen(fd uintptr, pd *pollDesc) uintptr {
 //封裝輸入、輸出、掛起註冊到epoll,並將epoll設置爲邊緣觸發模式運行
 var ev syscall.EpollEvent
 ev.Events = syscall.EPOLLIN | syscall.EPOLLOUT | syscall.EPOLLRDHUP | syscall.EPOLLET
 *(**pollDesc)(unsafe.Pointer(&ev.Data)) = pd
 //調用C語言的epoll_create爲當前establish socket註冊事件到epoll中
 return syscall.EpollCtl(epfd, syscall.EPOLL_CTL_ADD, int32(fd)&ev)
}

查詢事件

Linux系統對於事件輪詢的方法netpoll的實現則比較簡單,調用 C 語言的epoll_wait獲取就緒的事件,基於這些事件定位到對應的socket,並將socket對應的協程存入待運行列表toRun等待被輪詢處理:

對應的我們也給出Linux的實現netpoll_epoll.go關於netpoll的源碼實現:

func netpoll(delay int64) gList {
 //......
 var events [128]syscall.EpollEvent
retry:
 //調用epoll_wait查看註冊的socket事件中是否有就緒的事件
 n, errno := syscall.EpollWait(epfd, events[:], int32(len(events)), waitms)
 //......
 var toRun gList
 //遍歷事件列表
 for i := int32(0); i < n; i++ {
  ev := events[i]
  if ev.Events == 0 {
   continue
  }

  //......
  
  //判斷消息的讀寫類型
  var mode int32
  if ev.Events&(syscall.EPOLLIN|syscall.EPOLLRDHUP|syscall.EPOLLHUP|syscall.EPOLLERR) != 0 {
   mode += 'r'
  }
  if ev.Events&(syscall.EPOLLOUT|syscall.EPOLLHUP|syscall.EPOLLERR) != 0 {
   mode += 'w'
  }
  //將這些事件對應socket的協程存入toRun這個協程列表中,等待被喚醒並處理
  if mode != 0 {
   pd := *(**pollDesc)(unsafe.Pointer(&ev.Data))
   pd.setEventErr(ev.Events == syscall.EPOLLERR)
   netpollready(&toRun, pd, mode)
  }
 }
 return toRun
}

小結

以上便是筆者對於 go 語言中在 Linux 下對於網絡輪詢器抽象的剖析,希望對你有幫助,感謝您的閱讀!

我是 sharkchiliCSDN Java 領域博客專家開源項目—JavaGuide contributor,我想寫一些有意思的東西,希望對你有幫助,如果你想實時收到我寫的硬核的文章也歡迎你關注我的公衆號: 寫代碼的 SharkChili 。 因爲近期收到很多讀者的私信,所以也專門創建了一個交流羣,感興趣的讀者可以通過上方的公衆號獲取筆者的聯繫方式完成好友添加,點擊備註  “加羣”  即可和筆者和筆者的朋友們進行深入交流。

參考

epoll: 水平觸發與邊緣觸發 :https://zhuanlan.zhihu.com/p/363353777

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/pK0FKios4-SArea_bbbVtg