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

▲ 本文在公眾號上的鏈接是:點此進入。同步發布鏈接是:http://www.52im.net/thread-3280-1-1.html