go源碼解析之TCP連接系列基于go源碼1.16.5*
網(wǎng)絡(luò)數(shù)據(jù)讀取
上一章我們通過跟蹤TCPListener的Accept方法展鸡,了解了server側(cè)接收诵盼、新建連接的過程蓄拣,本章將通過TCPConn的Read方法的跟蹤來了解讀取網(wǎng)絡(luò)數(shù)據(jù)的過程拗小。
1. conn的Read方法
從上一章了解到TCPConn繼承自conn重罪,它的Read方法就是conn的Read,代碼如下:
src/net/net.go
func (c *conn) Read(b []byte) (int, error) {
...
n, err := c.fd.Read(b)
...
return n, err
}
conn的Read方法調(diào)用了fd的Read方法哀九,返回后進(jìn)行了相關(guān)的錯(cuò)誤判斷剿配。conn中的fd即netFD,netFD的Read方法如下:
src/net/fd_posix.go
func (fd *netFD) Read(p []byte) (n int, err error) {
n, err = fd.pfd.Read(p)
runtime.KeepAlive(fd)
return n, wrapSyscallError(readSyscallName, err)
}
netFD的Read方法又調(diào)用了pfd的Read阅束,即poll.FD的Read方法呼胚,關(guān)于Read我們先暫停,看一下第二行的KeepAlive方法:
2. KeepAlive
src/runtime/mfinal.go
// Mark KeepAlive as noinline so that it is easily detectable as an intrinsic.
//go:noinline
// KeepAlive marks its argument as currently reachable.
// This ensures that the object is not freed, and its finalizer is not run,
// before the point in the program where KeepAlive is called.
//
// A very simplified example showing where KeepAlive is required:
// type File struct { d int }
// d, err := syscall.Open("/file/path", syscall.O_RDONLY, 0)
// // ... do something if err != nil ...
// p := &Filenyncri8
// runtime.SetFinalizer(p, func(p *File) { syscall.Close(p.d) })
// var buf [10]byte
// n, err := syscall.Read(p.d, buf[:])
// // Ensure p is not finalized until Read returns.
// runtime.KeepAlive(p)
// // No more uses of p after this point.
//
// Without the KeepAlive call, the finalizer could run at the start of
// syscall.Read, closing the file descriptor before syscall.Read makes
// the actual system call.
func KeepAlive(x interface{}) {
// Introduce a use of x that the compiler can't eliminate.
// This makes sure x is alive on entry. We need x to be alive
// on entry for "defer runtime.KeepAlive(x)"; see issue 21402.
if cgoAlwaysFalse {
println(x)
}
}
注釋好長是不是息裸?但是代碼很短蝇更,這說明一點(diǎn):這個(gè)方法有點(diǎn)神奇,必須詳細(xì)說明:襞琛年扩!
它的作用就是保證傳入的參數(shù)在這個(gè)方法被調(diào)用之前不被垃圾回收器回收掉。
什么情況下需要這個(gè)方法呢宿亡?注釋里的例子給的就比較典型,下面按照代碼行數(shù)分步解釋:
- 例子通過系統(tǒng)調(diào)用open了一個(gè)文件纳令,open返回了文件的fd(file descriptor挽荠,文件描述符),這個(gè)fd就是系統(tǒng)分配給被打開的文件的一個(gè)id平绩,所以它是個(gè)整型圈匆。
- fd賦值給了File類型的p
- 設(shè)置了當(dāng)p被回收時(shí)關(guān)閉p.d所代表的打開的文件(runtime.SetFinalizer提供了變量被回收時(shí)必要的數(shù)據(jù)清理回調(diào),類似析構(gòu)函數(shù))捏雌。
- 進(jìn)行系統(tǒng)調(diào)用Read跃赚。
我們設(shè)想一下沒有KeepAlive的一種場景:在Read方法執(zhí)行前,垃圾回收器執(zhí)行,垃圾回收器發(fā)現(xiàn)p已經(jīng)沒有被其他任何地方引用纬傲,對p進(jìn)行了垃圾回收满败,且因?yàn)閷設(shè)置了Finalizer,回收的過程中關(guān)閉了p.d叹括。當(dāng)程序恢復(fù)執(zhí)行算墨,Read方法運(yùn)行,Read將在一個(gè)已經(jīng)被關(guān)閉的fd上工作汁雷,必然是會(huì)出錯(cuò)的净嘀。
那么KeepAlive又是怎么保證傳入它的變量不被回收?其實(shí)也不是什么魔法侠讯,就是因?yàn)樽兞勘划?dāng)作參數(shù)傳入挖藏,所以在KeepAlive調(diào)用之前,該變量不能被回收厢漩。我們自己寫一個(gè)類似方法也可以達(dá)到同樣的效果膜眠。當(dāng)然要注意編譯選項(xiàng)go:noinline
,它提示編譯器不要將該方法內(nèi)聯(lián)袁翁,如果沒有這個(gè)選項(xiàng)柴底,空方法可能直接被編譯器優(yōu)化掉,沒法起到keepalive作用粱胜。
回到Read方法中的runtime.KeepAlive(fd)
柄驻,再結(jié)合如下netFD的SetFinalizer方法,就容易理解了:
src/net/fd_posix.go
func (fd *netFD) setAddr(laddr, raddr Addr) {
fd.laddr = laddr
fd.raddr = raddr
runtime.SetFinalizer(fd, (*netFD).Close)
}
func (fd *netFD) Close() error {
runtime.SetFinalizer(fd, nil)
return fd.pfd.Close()
}
setAddr方法在netFD初始化后調(diào)用焙压。
3. poll.FD的Read方法
我們回到Read方法的跟蹤鸿脓,以下是poll.FD的Read方法:
src/internal/poll/fd_unix.go
func (fd *FD) Read(p []byte) (int, error) {
...
for {
n, err := ignoringEINTRIO(syscall.Read, fd.Sysfd, p)
if err != nil {
n = 0
if err == syscall.EAGAIN && fd.pd.pollable() {
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}
}
err = fd.eofError(n, err)
return n, err
}
}
// ignoringEINTRIO is like ignoringEINTR, but just for IO calls.
func ignoringEINTRIO(fn func(fd int, p []byte) (int, error), fd int, p []byte) (int, error) {
for {
n, err := fn(fd, p)
if err != syscall.EINTR {
return n, err
}
}
}
ignoringEINTRIO將syscall.Read作為方法參數(shù)傳入,并循環(huán)調(diào)用Read涯曲,當(dāng)錯(cuò)誤不是syscall.EINTR時(shí)返回野哭。查了一下EINTR錯(cuò)誤碼,它是當(dāng)進(jìn)程設(shè)置了signal handler幻件,并且沒有設(shè)置SA_RESTART拨黔,該進(jìn)程收到信號后,進(jìn)程內(nèi)正在進(jìn)行的可中斷系統(tǒng)調(diào)用將返回EINTR錯(cuò)誤绰沥。ENINTR錯(cuò)誤不是系統(tǒng)調(diào)用出現(xiàn)了錯(cuò)誤篱蝇,而是信號導(dǎo)致的中斷。關(guān)于原因可以參考這里的討論 徽曲。
ignoringEINTRIO返回后零截,下面的錯(cuò)誤處理和第二章Accept系統(tǒng)調(diào)用返回后類似:
如果錯(cuò)誤是EAGAIN(socket被設(shè)置為非阻塞模式,在這個(gè)socket上的系統(tǒng)調(diào)用都會(huì)立即返回而不會(huì)阻塞線程秃臣,例如此處的read調(diào)用涧衙,即使沒有讀取到數(shù)據(jù)也會(huì)立即返回,但是錯(cuò)誤信息會(huì)被設(shè)置為EAGAIN),并且fd.pd.pollable為true時(shí)弧哎,阻塞當(dāng)前goroutine進(jìn)行等待雁比,直到有新的可讀消息時(shí)continue,再次調(diào)用read進(jìn)行數(shù)據(jù)讀取傻铣。
這里提前簡單說一下pollDesc(即FD中的pd)章贞,它是IO多路復(fù)用(如epoll、kqueue非洲、CompletionPort等)在go語言中的集成鸭限,fd.pd.waitRead 即是等待io消息的到來。后續(xù)將有單獨(dú)章節(jié)介紹epoll在go語言網(wǎng)絡(luò)庫中的使用两踏。
最后看一下eofError方法:
src/internal/poll/fd_posix.go
// eofError returns io.EOF when fd is available for reading end of
// file.
func (fd *FD) eofError(n int, err error) error {
if n == 0 && err == nil && fd.ZeroReadIsEOF {
return io.EOF
}
return err
}
如果沒有讀取到數(shù)據(jù)且沒有返回錯(cuò)誤败京,再加上ZeroReadIsEOF這個(gè)為true,就返回EOF錯(cuò)誤梦染。我們看一下ZeroReadIsEOF的注釋:
// Whether a zero byte read indicates EOF. This is false for a
// message based socket connection.
ZeroReadIsEOF bool
從eofError方法的注釋和ZeroReadIsEOF的注釋基本可以斷定EOF錯(cuò)誤只適用于讀取文件赡麦,網(wǎng)絡(luò)連接數(shù)據(jù)的讀取不會(huì)產(chǎn)生這個(gè)錯(cuò)誤。
大家可能覺得奇怪帕识,“我們不是在跟蹤tcp數(shù)據(jù)讀取的代碼嗎泛粹?怎么這里還有跟文件相關(guān)的東西?”
其實(shí)大家注意代碼所在目錄的話肮疗,可以看到我們跟蹤的代碼跨了三個(gè)目錄晶姊,src/net
、src/internal/poll
伪货、src/runtime
们衙,完全屬于網(wǎng)絡(luò)層的代碼是在src/net
包中,而src/internal/poll
除了是網(wǎng)絡(luò)底層的實(shí)現(xiàn)還是文件讀寫的底層實(shí)現(xiàn)
“網(wǎng)絡(luò)數(shù)據(jù)的讀寫和文件數(shù)據(jù)的讀寫可以用同一個(gè)系統(tǒng)調(diào)用碱呼?”
沒錯(cuò)蒙挑,在linux世界里,任何io設(shè)備都可以用一個(gè)文件描述符(我們經(jīng)常見到的fd)代表愚臀,而對這些文件描述符的讀寫都可以使用write和read系統(tǒng)調(diào)用忆蚀。
可以看一下文件類的結(jié)構(gòu),同樣包含了poll.FD:
src/os/file_unix.go
// file is the real representation of *File.
type file struct {
pfd poll.FD
...
}
4. 小結(jié)
本章通過跟蹤conn的Read方法姑裂,了解了網(wǎng)絡(luò)數(shù)據(jù)讀取的過程馋袜。總結(jié)為以下幾點(diǎn):
- conn的Read方法調(diào)用了netFD的Read炭分,netFD的Read方法又調(diào)用了poll.FD的Read
- KeepAlive(此KeepAlive非上一章給用作網(wǎng)絡(luò)連接探活的KeepAlive)用來保證傳入的變量在KeepAlive調(diào)用前不被回收
- EINTR是信號對系統(tǒng)調(diào)用產(chǎn)生了中斷返回的錯(cuò)誤號桃焕,不是系統(tǒng)調(diào)用的錯(cuò)誤剑肯,遇到此錯(cuò)誤號可以重試
-
src/internal/poll
包是各種io讀寫共用的底層包
下一章我們將對TCPConn的Write方法進(jìn)行跟蹤捧毛,來了解數(shù)據(jù)寫入的過程。