深入Go語言網(wǎng)絡(luò)庫的基礎(chǔ)實(shí)現(xiàn)
Go語言的出現(xiàn),讓我見到了一門語言把網(wǎng)絡(luò)編程這件事情給做“正確”了,當(dāng)然,除了Go語言以外,還有很多語言也把這件事情做”正確”了。我一直堅(jiān)持著這樣的理念——要做"正確"的事情,而不是"高性能"的事情;很多時(shí)候,我們?cè)谧鱿到y(tǒng)設(shè)計(jì)、技術(shù)選型的時(shí)候,都被“高性能”這三個(gè)字給綁架了,當(dāng)然不是說性能不重要,你懂的。
目前很多高性能的基礎(chǔ)網(wǎng)絡(luò)服務(wù)器都是采用的C語言開發(fā)的,比如:Nginx、Redis、memcached等,它們都是基于”事件驅(qū)動(dòng) + 事件回掉函數(shù)”的方式實(shí)現(xiàn),也就是采用epoll等作為網(wǎng)絡(luò)收發(fā)數(shù)據(jù)包的核心驅(qū)動(dòng)。不少人(包括我自己)都認(rèn)為“事件驅(qū)動(dòng) + 事件回掉函數(shù)”的編程方法是“反人類”的;因?yàn)榇蠖鄶?shù)人都更習(xí)慣線性的處理一件事情,做完***件事情再做第二件事情,并不習(xí)慣在N件事情之間頻繁的切換干 活。為了解決程序員在開發(fā)服務(wù)器時(shí)需要自己的大腦不斷的“上下文切換”的問題,Go語言引入了一種用戶態(tài)線程goroutine來取代編寫異步的事件回掉 函數(shù),從而重新回歸到多線程并發(fā)模型的線性、同步的編程方式上。
用Go語言寫一個(gè)最簡(jiǎn)單的echo服務(wù)器:
- package main
- import (
- "log"
- "net"
- )
- func main() {
- ln, err := net.Listen("tcp", ":8080")
- if err != nil {
- log.Println(err)
- return
- }
- for {
- conn, err := ln.Accept()
- if err != nil {
- log.Println(err)
- continue
- }
- go echoFunc(conn)
- }
- }
- func echoFunc(c net.Conn) {
- buf := make([]byte, 1024)
- for {
- n, err := c.Read(buf)
- if err != nil {
- log.Println(err)
- return
- }
- c.Write(buf[:n])
- }
- }
main函數(shù)的過程就是首先創(chuàng)建一個(gè)監(jiān)聽套接字,然后用一個(gè)for循環(huán)不斷的從監(jiān)聽套接字上Accept新的連接,***調(diào)用echoFunc函數(shù)在建立的連接上干活。關(guān)鍵代碼是:
- go echoFunc(conn)
每收到一個(gè)新的連接,就創(chuàng)建一個(gè)“線程”去服務(wù)這個(gè)連接,因此所有的業(yè)務(wù)邏輯都可以同步、順序的編寫到echoFunc函數(shù)中,再也不用去關(guān)心網(wǎng)絡(luò) IO是否會(huì)阻塞的問題。不管業(yè)務(wù)多復(fù)雜,Go語言的并發(fā)服務(wù)器的編程模型都是長(zhǎng)這個(gè)樣子??梢钥隙ǖ氖牵趌inux上Go語言寫的網(wǎng)絡(luò)服務(wù)器也是采用的 epoll作為***層的數(shù)據(jù)收發(fā)驅(qū)動(dòng),Go語言網(wǎng)絡(luò)的底層實(shí)現(xiàn)中同樣存在“上下文切換”的工作,只是這個(gè)切換工作由runtime的調(diào)度器來做了,減少了 程序員的負(fù)擔(dān)。
弄明白網(wǎng)絡(luò)庫的底層實(shí)現(xiàn),貌似只要弄清楚echo服務(wù)器中的Listen、Accept、Read、Write四個(gè)函數(shù)的底層實(shí)現(xiàn)關(guān)系就可以了。本文將采用自底向上的方式來介紹,也就是從***層到上層的方式,這也是我閱讀源碼的方式。底層實(shí)現(xiàn)涉及到的核心源碼文件主要有:
net/fd_unix.go
net/fd_poll_runtime.go
runtime/netpoll.goc
runtime/netpoll_epoll.c
runtime/proc.c (調(diào)度器)
netpoll_epoll.c文件是Linux平臺(tái)使用epoll作為網(wǎng)絡(luò)IO多路復(fù)用的實(shí)現(xiàn)代碼,這份代碼可以了解到epoll相關(guān)的操作(比 如:添加fd到epoll、從epoll刪除fd等),只有4個(gè)函數(shù),分別是runtime·netpollinit、 runtime·netpollopen、runtime·netpollclose和runtime·netpoll。init函數(shù)就是創(chuàng)建epoll 對(duì)象,open函數(shù)就是添加一個(gè)fd到epoll中,close函數(shù)就是從epoll刪除一個(gè)fd,netpoll函數(shù)就是從epoll wait得到所有發(fā)生事件的fd,并將每個(gè)fd對(duì)應(yīng)的goroutine(用戶態(tài)線程)通過鏈表返回。用epoll寫過程序的人應(yīng)該都能理解這份代碼,沒 什么特別之處。
- void
- runtime·netpollinit(void)
- {
- epfd = runtime·epollcreate1(EPOLL_CLOEXEC);
- if(epfd >= 0)
- return;
- epfd = runtime·epollcreate(1024);
- if(epfd >= 0) {
- runtime·closeonexec(epfd);
- return;
- }
- runtime·printf("netpollinit: failed to create descriptor (%d)\n", -epfd);
- runtime·throw("netpollinit: failed to create descriptor");
- }
runtime·netpollinit函數(shù)首先使用runtime·epollcreate1創(chuàng)建epoll實(shí)例,如果沒有創(chuàng)建成功,就換用 runtime·epollcreate再創(chuàng)建一次。這兩個(gè)create函數(shù)分別等價(jià)于glibc的epoll_create1和 epoll_create函數(shù)。只是因?yàn)镚o語言并沒有直接使用glibc,而是自己封裝的系統(tǒng)調(diào)用,但功能是等價(jià)于glibc的??梢酝ㄟ^man手冊(cè)查 看這兩個(gè)create的詳細(xì)信息。
- int32
- runtime·netpollopen(uintptr fd, PollDesc *pd)
- {
- EpollEvent ev;
- int32 res;
- ev.events = EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET;
- ev.data = (uint64)pd;
- res = runtime·epollctl(epfd, EPOLL_CTL_ADD, (int32)fd, &ev);
- return -res;
- }
添加fd到epoll中的runtime·netpollopen函數(shù)可以看到每個(gè)fd一開始都關(guān)注了讀寫事件,并且采用的是邊緣觸發(fā),除此之外還關(guān)注了一個(gè)不常見的新事件EPOLLRDHUP,這個(gè)事件是在較新的內(nèi)核版本添加的,目的是解決對(duì)端socket關(guān)閉,epoll本身并不能直接感知到這個(gè)關(guān)閉動(dòng)作的問題。注意任何一個(gè)fd在添加到epoll中的時(shí)候就關(guān)注了EPOLLOUT事件的話,就立馬產(chǎn)生一次寫事件,這次事件可能是多余浪費(fèi)的。
#p#
epoll操作的相關(guān)函數(shù)都會(huì)在事件驅(qū)動(dòng)的抽象層中去調(diào)用,為什么需要這個(gè)抽象層呢?原因很簡(jiǎn)單,因?yàn)镚o語言需要跑在不同的平臺(tái)上,有Linux、Unix、Mac OS X和Windows等,所以需要靠事件驅(qū)動(dòng)的抽象層來為網(wǎng)絡(luò)庫提供一致的接口,從而屏蔽事件驅(qū)動(dòng)的具體平臺(tái)依賴實(shí)現(xiàn)。runtime/netpoll.goc源文件就是整個(gè)事件驅(qū)動(dòng)抽象層的實(shí)現(xiàn),抽象層的核心數(shù)據(jù)結(jié)構(gòu)是:
- struct PollDesc
- {
- PollDesc* link; // in pollcache, protected by pollcache.Lock
- Lock; // protectes the following fields
- uintptr fd;
- bool closing;
- uintptr seq; // protects from stale timers and ready notifications
- G* rg; // G waiting for read or READY (binary semaphore)
- Timer rt; // read deadline timer (set if rt.fv != nil)
- int64 rd; // read deadline
- G* wg; // the same for writes
- Timer wt;
- int64 wd;
- };
每個(gè)添加到epoll中的fd都對(duì)應(yīng)了一個(gè)PollDesc結(jié)構(gòu)實(shí)例,PollDesc維護(hù)了讀寫此fd的goroutine這一非常重要的信息。可以大膽的推測(cè)一下,網(wǎng)絡(luò)IO讀寫操作的實(shí)現(xiàn)應(yīng)該是:當(dāng)在一個(gè)fd上讀寫遇到EAGAIN錯(cuò)誤的時(shí)候,就將當(dāng)前goroutine存儲(chǔ)到這個(gè)fd對(duì)應(yīng)的PollDesc中,同時(shí)將goroutine給park住,直到這個(gè)fd上再此發(fā)生了讀寫事件后,再將此goroutine給ready激活重新運(yùn)行。事實(shí)上的實(shí)現(xiàn)大概也是這個(gè)樣子的。
事件驅(qū)動(dòng)抽象層主要干的事情就是將具體的事件驅(qū)動(dòng)實(shí)現(xiàn)(比如: epoll)通過統(tǒng)一的接口封裝成Go接口供net庫使用,主要的接口也是:創(chuàng)建事件驅(qū)動(dòng)實(shí)例、添加fd、刪除fd、等待事件以及設(shè)置DeadLine。runtime_pollServerInit負(fù)責(zé)創(chuàng)建事件驅(qū)動(dòng)實(shí)例,runtime_pollOpen將分配一個(gè)PollDesc實(shí)例和fd綁定起來,然后將fd添加到epoll中,runtime_pollClose就是將fd從epoll中刪除,同時(shí)將刪除的fd綁定的PollDesc實(shí)例刪除,runtime_pollWait接口是至關(guān)重要的,這個(gè)接口一般是在非阻塞讀寫發(fā)生EAGAIN錯(cuò)誤的時(shí)候調(diào)用,作用就是park當(dāng)前讀寫的goroutine。
runtime中的epoll事件驅(qū)動(dòng)抽象層其實(shí)在進(jìn)入net庫后,又被封裝了一次,這一次封裝從代碼上看主要是為了方便在純Go語言環(huán)境進(jìn)行操作,net庫中的這次封裝實(shí)現(xiàn)在net/fd_poll_runtime.go文件中,主要是通過pollDesc對(duì)象來實(shí)現(xiàn)的:
- type pollDesc struct {
- runtimeCtx uintptr
- }
注意:此處的pollDesc對(duì)象不是上文提到的runtime中的PollDesc,相反此處pollDesc對(duì)象的runtimeCtx成員才是指向的runtime的PollDesc實(shí)例。pollDesc對(duì)象主要就是將runtime的事件驅(qū)動(dòng)抽象層給再封裝了一次,供網(wǎng)絡(luò)fd對(duì)象使用。
- var serverInit sync.Once
- func (pd *pollDesc) Init(fd *netFD) error {
- serverInit.Do(runtime_pollServerInit)
- ctx, errno := runtime_pollOpen(uintptr(fd.sysfd))
- if errno != 0 {
- return syscall.Errno(errno)
- }
- pd.runtimeCtx = ctx
- return nil
- }
pollDesc對(duì)象最需要關(guān)注的就是其Init方法,這個(gè)方法通過一個(gè)sync.Once變量來調(diào)用了runtime_pollServerInit函數(shù),也就是創(chuàng)建epoll實(shí)例的函數(shù)。意思就是runtime_pollServerInit函數(shù)在整個(gè)進(jìn)程生命周期內(nèi)只會(huì)被調(diào)用一次,也就是只會(huì)創(chuàng)建一次epoll實(shí)例。epoll實(shí)例被創(chuàng)建后,會(huì)調(diào)用runtime_pollOpen函數(shù)將fd添加到epoll中。
網(wǎng)絡(luò)編程中的所有socket fd都是通過netFD對(duì)象實(shí)現(xiàn)的,netFD是對(duì)網(wǎng)絡(luò)IO操作的抽象,linux的實(shí)現(xiàn)在文件net/fd_unix.go中。netFD對(duì)象實(shí)現(xiàn)有自己的init方法,還有完成基本IO操作的Read和Write方法,當(dāng)然除了這三個(gè)方法以外,還有很多非常有用的方法供用戶使用。
- // 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
- }
通過netFD對(duì)象的定義可以看到每個(gè)fd都關(guān)聯(lián)了一個(gè)pollDesc實(shí)例,通過上文我們知道pollDesc對(duì)象最終是對(duì)epoll的封裝。
- func (fd *netFD) init() error {
- if err := fd.pd.Init(fd); err != nil {
- return err
- }
- return nil
- }
netFD對(duì)象的init函數(shù)僅僅是調(diào)用了pollDesc實(shí)例的Init函數(shù),作用就是將fd添加到epoll中,如果這個(gè)fd是***個(gè)網(wǎng)絡(luò)socket fd的話,這一次init還會(huì)擔(dān)任創(chuàng)建epoll實(shí)例的任務(wù)。要知道在Go進(jìn)程里,只會(huì)有一個(gè)epoll實(shí)例來管理所有的網(wǎng)絡(luò)socket fd,這個(gè)epoll實(shí)例也就是在***個(gè)網(wǎng)絡(luò)socket fd被創(chuàng)建的時(shí)候所創(chuàng)建。
- for {
- n, err = syscall.Read(int(fd.sysfd), p)
- if err != nil {
- n = 0
- if err == syscall.EAGAIN {
- if err = fd.pd.WaitRead(); err == nil {
- continue
- }
- }
- }
- err = chkReadErr(n, err, fd)
- break
- }
上面代碼段是從netFD的Read方法中摘取,重點(diǎn)關(guān)注這個(gè)for循環(huán)中的syscall.Read調(diào)用的錯(cuò)誤處理。當(dāng)有錯(cuò)誤發(fā)生的時(shí)候,會(huì)檢查這個(gè)錯(cuò)誤是否是syscall.EAGAIN,如果是,則調(diào)用WaitRead將當(dāng)前讀這個(gè)fd的goroutine給park住,直到這個(gè)fd上的讀事件再次發(fā)生為止。當(dāng)這個(gè)socket上有新數(shù)據(jù)到來的時(shí)候,WaitRead調(diào)用返回,繼續(xù)for循環(huán)的執(zhí)行。這樣的實(shí)現(xiàn),就讓調(diào)用netFD的Read的地方變成了同步“阻塞”方式編程,不再是異步非阻塞的編程方式了。netFD的Write方法和Read的實(shí)現(xiàn)原理是一樣的,都是在碰到EAGAIN錯(cuò)誤的時(shí)候?qū)?dāng)前goroutine給park住直到socket再次可寫為止。
本文只是將網(wǎng)絡(luò)庫的底層實(shí)現(xiàn)給大體上引導(dǎo)了一遍,知道底層代碼大概實(shí)現(xiàn)在什么地方,方便結(jié)合源碼深入理解。Go語言中的高并發(fā)、同步阻塞方式編程的關(guān)鍵其實(shí)是”goroutine和調(diào)度器”,針對(duì)網(wǎng)絡(luò)IO的時(shí)候,我們需要知道EAGAIN這個(gè)非常關(guān)鍵的調(diào)度點(diǎn),掌握了這個(gè)調(diào)度點(diǎn),即使沒有調(diào)度器,自己也可以在epoll的基礎(chǔ)上配合協(xié)程等用戶態(tài)線程實(shí)現(xiàn)網(wǎng)絡(luò)IO操作的調(diào)度,達(dá)到同步阻塞編程的目的。
***,為什么需要同步阻塞的方式編程?只有看多、寫多了異步非阻塞代碼的時(shí)候才能夠深切體會(huì)到這個(gè)問題。真正的高大上絕對(duì)不是——“別人不會(huì),我會(huì);別人寫不出來,我寫得出來。”
原文鏈接:http://skoo.me/go/2014/04/21/go-net-core/