go源碼解析之TCP連接(三)——Read

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ù)分步解釋:

  1. 例子通過系統(tǒng)調(diào)用open了一個(gè)文件纳令,open返回了文件的fd(file descriptor挽荠,文件描述符),這個(gè)fd就是系統(tǒng)分配給被打開的文件的一個(gè)id平绩,所以它是個(gè)整型圈匆。
  2. fd賦值給了File類型的p
  3. 設(shè)置了當(dāng)p被回收時(shí)關(guān)閉p.d所代表的打開的文件(runtime.SetFinalizer提供了變量被回收時(shí)必要的數(shù)據(jù)清理回調(diào),類似析構(gòu)函數(shù))捏雌。
  4. 進(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/netsrc/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):

  1. conn的Read方法調(diào)用了netFD的Read炭分,netFD的Read方法又調(diào)用了poll.FD的Read
  2. KeepAlive(此KeepAlive非上一章給用作網(wǎng)絡(luò)連接探活的KeepAlive)用來保證傳入的變量在KeepAlive調(diào)用前不被回收
  3. EINTR是信號對系統(tǒng)調(diào)用產(chǎn)生了中斷返回的錯(cuò)誤號桃焕,不是系統(tǒng)調(diào)用的錯(cuò)誤剑肯,遇到此錯(cuò)誤號可以重試
  4. src/internal/poll包是各種io讀寫共用的底層包

下一章我們將對TCPConn的Write方法進(jìn)行跟蹤捧毛,來了解數(shù)據(jù)寫入的過程。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市呀忧,隨后出現(xiàn)的幾起案子师痕,更是在濱河造成了極大的恐慌,老刑警劉巖而账,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件胰坟,死亡現(xiàn)場離奇詭異,居然都是意外死亡泞辐,警方通過查閱死者的電腦和手機(jī)笔横,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來咐吼,“玉大人吹缔,你說我怎么就攤上這事【馇眩” “怎么了厢塘?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長肌幽。 經(jīng)常有香客問我晚碾,道長,這世上最難降的妖魔是什么喂急? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任格嘁,我火速辦了婚禮,結(jié)果婚禮上煮岁,老公的妹妹穿的比我還像新娘讥蔽。我一直安慰自己,他們只是感情好画机,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布冶伞。 她就那樣靜靜地躺著,像睡著了一般步氏。 火紅的嫁衣襯著肌膚如雪响禽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天荚醒,我揣著相機(jī)與錄音芋类,去河邊找鬼。 笑死界阁,一個(gè)胖子當(dāng)著我的面吹牛侯繁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播泡躯,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼贮竟,長吁一口氣:“原來是場噩夢啊……” “哼丽焊!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起咕别,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤技健,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后惰拱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體雌贱,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年偿短,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了欣孤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,646評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡昔逗,死狀恐怖导街,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情纤子,我是刑警寧澤搬瑰,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站控硼,受9級特大地震影響泽论,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜卡乾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一翼悴、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧幔妨,春花似錦鹦赎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至锁施,卻和暖如春陪踩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背悉抵。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工肩狂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人姥饰。 一個(gè)月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓傻谁,卻偏偏與公主長得像,于是被迫代替她去往敵國和親列粪。 傳聞我的和親對象是個(gè)殘疾皇子审磁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評論 2 348

推薦閱讀更多精彩內(nèi)容