1、系列文章引言
1.1 文章目的
作為即時(shí)通訊技術(shù)的開(kāi)發(fā)者來(lái)說(shuō),高性能、高并發(fā)相關(guān)的技術(shù)概念早就了然與胸,什么線(xiàn)程池、零拷貝、多路復(fù)用、事件驅(qū)動(dòng)、epoll等等名詞信手拈來(lái),又或許你對(duì)具有這些技術(shù)特征的技術(shù)框架比如:Java的Netty、Php的workman、Go的nget等熟練掌握。但真正到了面視或者技術(shù)實(shí)踐過(guò)程中遇到無(wú)法釋?xiě)训囊苫髸r(shí),方知自已所掌握的不過(guò)是皮毛。
返璞歸真、回歸本質(zhì),這些技術(shù)特征背后的底層原理到底是什么?如何能通俗易懂、毫不費(fèi)力真正透徹理解這些技術(shù)背后的原理,正是《從根上理解高性能、高并發(fā)》系列文章所要分享的。
1.2 文章源起
我整理了相當(dāng)多有關(guān)IM、消息推送等即時(shí)通訊技術(shù)相關(guān)的資源和文章,從最開(kāi)始的開(kāi)源IM框架MobileIMSDK,到網(wǎng)絡(luò)編程經(jīng)典巨著《TCP/IP詳解》的在線(xiàn)版本,再到IM開(kāi)發(fā)綱領(lǐng)性文章《新手入門(mén)一篇就夠:從零開(kāi)發(fā)移動(dòng)端IM》,以及網(wǎng)絡(luò)編程由淺到深的《網(wǎng)絡(luò)編程懶人入門(mén)》、《腦殘式網(wǎng)絡(luò)編程入門(mén)》、《高性能網(wǎng)絡(luò)編程》、《不為人知的網(wǎng)絡(luò)編程》系列文章。
越往知識(shí)的深處走,越覺(jué)得對(duì)即時(shí)通訊技術(shù)了解的太少。于是后來(lái),為了讓開(kāi)發(fā)者門(mén)更好地從基礎(chǔ)電信技術(shù)的角度理解網(wǎng)絡(luò)(尤其移動(dòng)網(wǎng)絡(luò))特性,我跨專(zhuān)業(yè)收集整理了《IM開(kāi)發(fā)者的零基礎(chǔ)通信技術(shù)入門(mén)》系列高階文章。這系列文章已然是普通即時(shí)通訊開(kāi)發(fā)者的網(wǎng)絡(luò)通信技術(shù)知識(shí)邊界,加上之前這些網(wǎng)絡(luò)編程資料,解決網(wǎng)絡(luò)通信方面的知識(shí)盲點(diǎn)基本夠用了。
對(duì)于即時(shí)通訊IM這種系統(tǒng)的開(kāi)發(fā)來(lái)說(shuō),網(wǎng)絡(luò)通信知識(shí)確實(shí)非常重要,但回歸到技術(shù)本質(zhì),實(shí)現(xiàn)網(wǎng)絡(luò)通信本身的這些技術(shù)特征:包括上面提到的線(xiàn)程池、零拷貝、多路復(fù)用、事件驅(qū)動(dòng)等等,它們的本質(zhì)是什么?底層原理又是怎樣?這就是整理本系列文章的目的,希望對(duì)你有用。
1.3 文章目錄
1.4 本篇概述
接上篇《深入計(jì)算機(jī)底層,理解線(xiàn)程與線(xiàn)程池》,本篇是高性能、高并發(fā)系列的第2篇文章,在這里我們來(lái)到了I/O這一話(huà)題。你有沒(méi)有想過(guò),當(dāng)我們執(zhí)行文件I/O、網(wǎng)絡(luò)I/O操作時(shí)計(jì)算機(jī)底層到底發(fā)生了些什么?對(duì)于計(jì)算機(jī)來(lái)說(shuō)I/O是極其重要的,本篇將帶給你這個(gè)問(wèn)的答案。
2、本文作者
應(yīng)作者要求,不提供真名,也不提供個(gè)人照片。
本文作者主要技術(shù)方向?yàn)榛ヂ?lián)網(wǎng)后端、高并發(fā)高性能服務(wù)器、檢索引擎技術(shù),網(wǎng)名是“碼農(nóng)的荒島求生”,公眾號(hào)“碼農(nóng)的荒島求生”。感謝作者的無(wú)私分享。
3、不能執(zhí)行I/O的計(jì)算機(jī)是什么?
相信對(duì)于程序員來(lái)說(shuō)I/O操作是最為熟悉不過(guò)的了,比如:
- 1)當(dāng)我們使用C語(yǔ)言中的printf、C++中的"<<",Python中的print,Java中的System.out.println等時(shí);
- 2)當(dāng)我們使用各種語(yǔ)言讀寫(xiě)文件時(shí);
- 3)當(dāng)我們通過(guò)TCP/IP進(jìn)行網(wǎng)絡(luò)通信時(shí);
- 4)當(dāng)我們使用鼠標(biāo)龍飛鳳舞時(shí);
- 5)當(dāng)我們拿起鍵盤(pán)在評(píng)論區(qū)指點(diǎn)江山亦或是埋頭苦干努力制造bug時(shí);
- 6)當(dāng)我們能看到屏幕上的漂亮的圖形界面時(shí)等等。
以上這一切,都是I/O!
想一想:如果沒(méi)有I/O計(jì)算機(jī)該是一種多么枯燥的設(shè)備,不能看電影、不能玩游戲,也不能上網(wǎng),這樣的計(jì)算機(jī)最多就是一個(gè)大號(hào)的計(jì)算器。
既然I/O這么重要,那么到底什么才是I/O呢?
4、什么是I/O?
I/O就是簡(jiǎn)單的數(shù)據(jù)Copy,僅此而已!
這一點(diǎn)很重要!
既然是copy數(shù)據(jù),那么又是從哪里copy到哪里呢?
如果數(shù)據(jù)是從外部設(shè)備copy到內(nèi)存中,這就是Input。
如果數(shù)據(jù)是從內(nèi)存copy到外部設(shè)備,這就是Output。
內(nèi)存與外部設(shè)備之間不嫌麻煩的來(lái)回copy數(shù)據(jù)就是Input and Output,簡(jiǎn)稱(chēng)I/O(Input/Output),僅此而已。
5、I/O與CPU
現(xiàn)在我們知道了什么是I/O,接下來(lái)就是重點(diǎn)部分了,大家注意,坐穩(wěn)了。
我們知道現(xiàn)在的CPU其主頻都是數(shù)GHz起步,這是什么意思呢?
簡(jiǎn)單說(shuō)就是:CPU執(zhí)行機(jī)器指令的速度是納秒級(jí)別的,而通常的I/O比如磁盤(pán)操作,一次磁盤(pán)seek大概在毫秒級(jí)別,因此如果我們把CPU的速度比作戰(zhàn)斗機(jī)的話(huà),那么I/O操作的速度就是肯德雞。
也就是說(shuō)當(dāng)我們的程序跑起來(lái)時(shí)(CPU執(zhí)行機(jī)器指令),其速度是要遠(yuǎn)遠(yuǎn)快于I/O速度的。那么接下來(lái)的問(wèn)題就是二者速度相差這么大,那么我們?cè)撊绾卧O(shè)計(jì)、該如何更加合理的高效利用系統(tǒng)資源呢?
既然有速度差異,而且進(jìn)程在執(zhí)行完I/O操作前不能繼續(xù)向前推進(jìn),那么顯然只有一個(gè)辦法,那就是等待(wait)。
同樣是等待,有聰明的等待,也有傻傻的等待,簡(jiǎn)稱(chēng)傻等,那么是選擇聰明的等待呢還是選擇傻等呢?
假設(shè)你是一個(gè)急性子(CPU),需要等待一個(gè)重要的文件,不巧的是這個(gè)文件只能快遞過(guò)來(lái)(I/O),那么這時(shí)你是選擇什么事情都不干了,深情的注視著門(mén)口就像盼望著你的哈尼一樣專(zhuān)心等待這個(gè)快遞呢?還是暫時(shí)先不要管快遞了,玩?zhèn)€游戲看個(gè)電影刷會(huì)兒短視頻等快遞來(lái)了再說(shuō)呢?
很顯然,更好的方法就是先去干其它事情,快遞來(lái)了再說(shuō)。
因此:這里的關(guān)鍵點(diǎn)就是快遞沒(méi)到前手頭上的事情可以先暫停,切換到其它任務(wù),等快遞過(guò)來(lái)了再切換回來(lái)。
理解了這一點(diǎn)你就能明白執(zhí)行I/O操作時(shí)底層都發(fā)生了什么。
接下來(lái)讓我們以讀取磁盤(pán)文件為例來(lái)講解這一過(guò)程。
6、執(zhí)行I/O時(shí)底層都發(fā)生了什么
在上一篇《深入計(jì)算機(jī)底層,理解線(xiàn)程與線(xiàn)程池》中,我們引入了進(jìn)程和線(xiàn)程的概念。
在支持線(xiàn)程的操作系統(tǒng)中,實(shí)際上被調(diào)度的是線(xiàn)程而不是進(jìn)程,為了更加清晰的理解I/O過(guò)程,我們暫時(shí)假設(shè)操作系統(tǒng)只有進(jìn)程這樣的概念,先不去考慮線(xiàn)程,這并不會(huì)影響我們的討論。
現(xiàn)在內(nèi)存中有兩個(gè)進(jìn)程,進(jìn)程A和進(jìn)程B,當(dāng)前進(jìn)程A正在運(yùn)行。
如下圖所示:
進(jìn)程A中有一段讀取文件的代碼,不管在什么語(yǔ)言中通常我們定義一個(gè)用來(lái)裝數(shù)據(jù)的buff,然后調(diào)用read之類(lèi)的函數(shù)。
就像這樣:
read(buff);
這就是一種典型的I/O操作,當(dāng)CPU執(zhí)行到這段代碼的時(shí)候會(huì)向磁盤(pán)發(fā)送讀取請(qǐng)求。
注意:與CPU執(zhí)行指令的速度相比,I/O操作操作是非常慢的,因此操作系統(tǒng)是不可能把寶貴的CPU計(jì)算資源浪費(fèi)在無(wú)謂的等待上的,這時(shí)重點(diǎn)來(lái)了,注意接下來(lái)是重點(diǎn)哦。
由于外部設(shè)備執(zhí)行I/O操作是相當(dāng)慢的,因此在I/O操作完成之前進(jìn)程是無(wú)法繼續(xù)向前推進(jìn)的,這就是所謂的阻塞,即通常所說(shuō)的block。
操作系統(tǒng)檢測(cè)到進(jìn)程向I/O設(shè)備發(fā)起請(qǐng)求后就暫停進(jìn)程的運(yùn)行,怎么暫停運(yùn)行呢?很簡(jiǎn)單:只需要記錄下當(dāng)前進(jìn)程的運(yùn)行狀態(tài)并把CPU的PC寄存器指向其它進(jìn)程的指令就可以了。
進(jìn)程有暫停就會(huì)有繼續(xù)執(zhí)行,因此操作系統(tǒng)必須保存被暫停的進(jìn)程以備后續(xù)繼續(xù)執(zhí)行,顯然我們可以用隊(duì)列來(lái)保存被暫停執(zhí)行的進(jìn)程。
如下圖所示,進(jìn)程A被暫停執(zhí)行并被放到阻塞隊(duì)列中(注意:不同的操作系統(tǒng)會(huì)有不同的實(shí)現(xiàn),可能每個(gè)I/O設(shè)備都有一個(gè)對(duì)應(yīng)的阻塞隊(duì)列,但這種實(shí)現(xiàn)細(xì)節(jié)上的差異不影響我們的討論)。
這時(shí)操作系統(tǒng)已經(jīng)向磁盤(pán)發(fā)送了I/O請(qǐng)求,因此磁盤(pán)driver開(kāi)始將磁盤(pán)中的數(shù)據(jù)copy到進(jìn)程A的buff中。雖然這時(shí)進(jìn)程A已經(jīng)被暫停執(zhí)行了,但這并不妨礙磁盤(pán)向內(nèi)存中copy數(shù)據(jù)。
注意:現(xiàn)代磁盤(pán)向內(nèi)存copy數(shù)據(jù)時(shí)無(wú)需借助CPU的幫助,這就是所謂的DMA(Direct Memory Access)。
這個(gè)過(guò)程如下圖所示:
讓磁盤(pán)先copy著數(shù)據(jù),我們接著聊。
實(shí)際上:操作系統(tǒng)中除了有阻塞隊(duì)列之外也有就緒隊(duì)列,所謂就緒隊(duì)列是指隊(duì)列里的進(jìn)程準(zhǔn)備就緒可以被CPU執(zhí)行了。
你可能會(huì)問(wèn)為什么不直接執(zhí)行非要有個(gè)就緒隊(duì)列呢?答案很簡(jiǎn)單:那就是僧多粥少,在即使只有1個(gè)核的機(jī)器上也可以創(chuàng)建出成千上萬(wàn)個(gè)進(jìn)程,CPU不可能同時(shí)執(zhí)行這么多的進(jìn)程,因此必然存在這樣的進(jìn)程,即使其一切準(zhǔn)備就緒也不能被分配到計(jì)算資源,這樣的進(jìn)程就被放到了就緒隊(duì)列。
現(xiàn)在進(jìn)程B就位于就緒隊(duì)列,萬(wàn)事俱備只欠CPU。
如下圖所示:
當(dāng)進(jìn)程A被暫停執(zhí)行后CPU是不可以閑下來(lái)的,因?yàn)榫途w隊(duì)列中還有嗷嗷待哺的進(jìn)程B,這時(shí)操作系統(tǒng)開(kāi)始在就緒隊(duì)列中找下一個(gè)可以執(zhí)行的進(jìn)程,也就是這里的進(jìn)程B。
此時(shí)操作系統(tǒng)將進(jìn)程B從就緒隊(duì)列中取出,找出進(jìn)程B被暫停時(shí)執(zhí)行到的機(jī)器指令的位置,然后將CPU的PC寄存器指向該位置,這樣進(jìn)程B就開(kāi)始運(yùn)行啦。
如下圖所示:
注意:接下來(lái)的這段是重點(diǎn)中的重點(diǎn)!
注意觀察上圖:此時(shí)進(jìn)程B在被CPU執(zhí)行,磁盤(pán)在向進(jìn)程A的內(nèi)存空間中copy數(shù)據(jù),看出來(lái)了嗎——大家都在忙,誰(shuí)都沒(méi)有閑著,數(shù)據(jù)copy和指令執(zhí)行在同時(shí)進(jìn)行,在操作系統(tǒng)的調(diào)度下,CPU、磁盤(pán)都得到了充分的利用,這就是程序員的智慧所在。
現(xiàn)在你應(yīng)該理解為什么操作系統(tǒng)這么重要了吧。
此后磁盤(pán)終于將全部數(shù)據(jù)都copy到了進(jìn)程A的內(nèi)存中,這時(shí)磁盤(pán)通知操作系統(tǒng)任務(wù)完成啦,你可能會(huì)問(wèn)怎么通知呢?這就是中斷。
操作系統(tǒng)接收到磁盤(pán)中斷后發(fā)現(xiàn)數(shù)據(jù)copy完畢,進(jìn)程A重新獲得繼續(xù)運(yùn)行的資格,這時(shí)操作系統(tǒng)小心翼翼的把進(jìn)程A從阻塞隊(duì)列放到了就緒隊(duì)列當(dāng)中。
如下圖所示:
注意:從前面關(guān)于就緒狀態(tài)的討論中我們知道,操作系統(tǒng)是不會(huì)直接運(yùn)行進(jìn)程A的,進(jìn)程A必須被放到就緒隊(duì)列中等待,這樣對(duì)大家都公平。
此后進(jìn)程B繼續(xù)執(zhí)行,進(jìn)程A繼續(xù)等待,進(jìn)程B執(zhí)行了一會(huì)兒后操作系統(tǒng)認(rèn)為進(jìn)程B執(zhí)行的時(shí)間夠長(zhǎng)了,因此把進(jìn)程B放到就緒隊(duì)列,把進(jìn)程A取出并繼續(xù)執(zhí)行。
注意:操作系統(tǒng)把進(jìn)程B放到的是就緒隊(duì)列,因此進(jìn)程B被暫停運(yùn)行僅僅是因?yàn)闀r(shí)間片到了而不是因?yàn)榘l(fā)起I/O請(qǐng)求被阻塞。
如下圖所示:
進(jìn)程A繼續(xù)執(zhí)行,此時(shí)buff中已經(jīng)裝滿(mǎn)了想要的數(shù)據(jù),進(jìn)程A就這樣愉快的運(yùn)行下去了,就好像從來(lái)沒(méi)有被暫停過(guò)一樣,進(jìn)程對(duì)于自己被暫停一事一無(wú)所知,這就是操作系統(tǒng)的魔法。
現(xiàn)在你應(yīng)該明白了I/O是一個(gè)怎樣的過(guò)程了吧。
這種進(jìn)程執(zhí)行I/O操作被阻塞暫停執(zhí)行的方式被稱(chēng)為阻塞式I/O,blocking I/O,這也是最常見(jiàn)最容易理解的I/O方式,有阻塞式I/O就有非阻塞式I/O,在這里我們暫時(shí)先不考慮這種方式。
在本節(jié)開(kāi)頭我們說(shuō)過(guò)暫時(shí)只考慮進(jìn)程而不考慮線(xiàn)程,現(xiàn)在我們放寬這個(gè)條件,實(shí)際上也非常簡(jiǎn)單,只需要把前圖中調(diào)度的進(jìn)程改為線(xiàn)程就可以了,這里的討論對(duì)于線(xiàn)程一樣成立。
7、零拷貝(Zero-copy)
最后需要注意的一點(diǎn)就是:上面的講解中我們直接把磁盤(pán)數(shù)據(jù)copy到了進(jìn)程空間中,但實(shí)際上一般情況下I/O數(shù)據(jù)是要首先copy到操作系統(tǒng)內(nèi)部,然后操作系統(tǒng)再copy到進(jìn)程空間中。
因此我們可以看到這里其實(shí)還有一層經(jīng)過(guò)操作系統(tǒng)的copy,對(duì)于性能要求很高的場(chǎng)景其實(shí)也是可以繞過(guò)操作系統(tǒng)直接進(jìn)行數(shù)據(jù)copy的,這也是本文描述的場(chǎng)景,這種繞過(guò)操作系統(tǒng)直接進(jìn)行數(shù)據(jù)copy的技術(shù)被稱(chēng)為Zero-copy,也就零拷貝,高并發(fā)、高性能場(chǎng)景下常用的一種技術(shù),原理上很簡(jiǎn)單吧。
PS:對(duì)于搞即時(shí)通訊開(kāi)發(fā)的Java程序員來(lái)說(shuō),著名的高性能網(wǎng)絡(luò)框架Netty就使用了零拷貝技術(shù),具體可以讀《NIO框架詳解:Netty的高性能之道》一文的第12節(jié)。如果對(duì)于Netty框架很好奇但不了解的話(huà),可以因著這兩篇文章入門(mén):《新手入門(mén):目前為止最透徹的的Netty高性能原理和框架架構(gòu)解析》、《史上最通俗Netty入門(mén)長(zhǎng)文:基本介紹、環(huán)境搭建、動(dòng)手實(shí)戰(zhàn)》。
8、本文小結(jié)
本文講解的是程序員常用的I/O(包括所謂的網(wǎng)絡(luò)I/O),一般來(lái)說(shuō)作為程序員我們無(wú)需關(guān)心,但是理解I/O背后的底層原理對(duì)于設(shè)計(jì)比如IM這種高性能、高并發(fā)系統(tǒng)是極為有益的,希望這篇能對(duì)大家加深對(duì)I/O的認(rèn)識(shí)有所幫助。
接下來(lái)的一篇《從根上理解高性能、高并發(fā)(三):深入操作系統(tǒng),徹底理解I/O多路復(fù)用》將要分享的是I/O技術(shù)的一大突破,正是因?yàn)樗艔氐捉鉀Q了高并發(fā)網(wǎng)絡(luò)通信中的C10K問(wèn)題(見(jiàn)《高性能網(wǎng)絡(luò)編程(二):上一個(gè)10年,著名的C10K并發(fā)連接問(wèn)題》),敬請(qǐng)期待!
附錄:更多高性能、高并發(fā)文章精選
《高性能網(wǎng)絡(luò)編程(一):?jiǎn)闻_(tái)服務(wù)器并發(fā)TCP連接數(shù)到底可以有多少》
《高性能網(wǎng)絡(luò)編程(二):上一個(gè)10年,著名的C10K并發(fā)連接問(wèn)題》
《高性能網(wǎng)絡(luò)編程(三):下一個(gè)10年,是時(shí)候考慮C10M并發(fā)問(wèn)題了》
《高性能網(wǎng)絡(luò)編程(四):從C10K到C10M高性能網(wǎng)絡(luò)應(yīng)用的理論探索》
《高性能網(wǎng)絡(luò)編程(五):一文讀懂高性能網(wǎng)絡(luò)編程中的I/O模型》
《高性能網(wǎng)絡(luò)編程(六):一文讀懂高性能網(wǎng)絡(luò)編程中的線(xiàn)程模型》
《高性能網(wǎng)絡(luò)編程(七):到底什么是高并發(fā)?一文即懂!》
《以網(wǎng)游服務(wù)端的網(wǎng)絡(luò)接入層設(shè)計(jì)為例,理解實(shí)時(shí)通信的技術(shù)挑戰(zhàn)》
《知乎技術(shù)分享:知乎千萬(wàn)級(jí)并發(fā)的高性能長(zhǎng)連接網(wǎng)關(guān)技術(shù)實(shí)踐》
《淘寶技術(shù)分享:手淘?xún)|級(jí)移動(dòng)端接入層網(wǎng)關(guān)的技術(shù)演進(jìn)之路》
《一套海量在線(xiàn)用戶(hù)的移動(dòng)端IM架構(gòu)設(shè)計(jì)實(shí)踐分享(含詳細(xì)圖文)》
《一套原創(chuàng)分布式即時(shí)通訊(IM)系統(tǒng)理論架構(gòu)方案》
《微信后臺(tái)基于時(shí)間序的海量數(shù)據(jù)冷熱分級(jí)架構(gòu)設(shè)計(jì)實(shí)踐》
《微信技術(shù)總監(jiān)談架構(gòu):微信之道——大道至簡(jiǎn)(演講全文)》
《如何解讀《微信技術(shù)總監(jiān)談架構(gòu):微信之道——大道至簡(jiǎn)》》
《快速裂變:見(jiàn)證微信強(qiáng)大后臺(tái)架構(gòu)從0到1的演進(jìn)歷程(一)》
《17年的實(shí)踐:騰訊海量產(chǎn)品的技術(shù)方法論》
《騰訊資深架構(gòu)師干貨總結(jié):一文讀懂大型分布式系統(tǒng)設(shè)計(jì)的方方面面》
《以微博類(lèi)應(yīng)用場(chǎng)景為例,總結(jié)海量社交系統(tǒng)的架構(gòu)設(shè)計(jì)步驟》
《新手入門(mén):零基礎(chǔ)理解大型分布式架構(gòu)的演進(jìn)歷史、技術(shù)原理、最佳實(shí)踐》
《從新手到架構(gòu)師,一篇就夠:從100到1000萬(wàn)高并發(fā)的架構(gòu)演進(jìn)之路》
本文已同步發(fā)布于“即時(shí)通訊技術(shù)圈”公眾號(hào)。

▲ 本文在公眾號(hào)上的鏈接是:點(diǎn)此進(jìn)入。同步發(fā)布鏈接是:http://www.52im.net/thread-3280-1-1.html