本文原題「讀取文件時,程序經歷了什麼?」,本次發佈時有少量改動。php
一、系列文章引言
1.1 文章目的
做爲即時通信技術的開發者來講,高性能、高併發相關的技術概念早就瞭然與胸,什麼線程池、零拷貝、多路複用、事件驅動、epoll等等名詞信手拈來,又或許你對具備這些技術特徵的技術框架好比:Java的Netty、Php的workman、Go的nget等熟練掌握。但真正到了面視或者技術實踐過程當中遇到沒法釋懷的疑惑時,方知自已所掌握的不過是皮毛。html
返璞歸真、迴歸本質,這些技術特徵背後的底層原理究竟是什麼?如何能通俗易懂、絕不費力真正透徹理解這些技術背後的原理,正是《從根上理解高性能、高併發》系列文章所要分享的。git
1.2 文章源起
我整理了至關多有關IM、消息推送等即時通信技術相關的資源和文章,從最開始的開源IM框架MobileIMSDK,到網絡編程經典鉅著《TCP/IP詳解》的在線版本,再到IM開發綱領性文章《新手入門一篇就夠:從零開發移動端IM》,以及網絡編程由淺到深的《網絡編程懶人入門》、《腦殘式網絡編程入門》、《高性能網絡編程》、《鮮爲人知的網絡編程》系列文章。程序員
越往知識的深處走,越以爲對即時通信技術瞭解的太少。因而後來,爲了讓開發者門更好地從基礎電信技術的角度理解網絡(尤爲移動網絡)特性,我跨專業收集整理了《IM開發者的零基礎通訊技術入門》系列高階文章。這系列文章已然是普通即時通信開發者的網絡通訊技術知識邊界,加上以前這些網絡編程資料,解決網絡通訊方面的知識盲點基本夠用了。github
對於即時通信IM這種系統的開發來講,網絡通訊知識確實很是重要,但迴歸到技術本質,實現網絡通訊自己的這些技術特徵:包括上面提到的線程池、零拷貝、多路複用、事件驅動等等,它們的本質是什麼?底層原理又是怎樣?這就是整理本系列文章的目的,但願對你有用。編程
1.3 文章目錄
- 《從根上理解高性能、高併發(一):深刻計算機底層,理解線程與線程池》
- 《從根上理解高性能、高併發(二):深刻操做系統,理解I/O與零拷貝技術》(* 本文)
- 《從根上理解高性能、高併發(三):深刻操做系統,完全理解I/O多路複用》
- 《從根上理解高性能、高併發(四):深刻操做系統,完全理解同步與異步》
- 《從根上理解高性能、高併發(五):深刻操做系統,理解高併發中的協程》
- 《從根上理解高性能、高併發(六):通俗易懂,高性能服務器究竟是如何實現的》
1.4 本篇概述
接上篇《深刻計算機底層,理解線程與線程池》,本篇是高性能、高併發系列的第2篇文章,在這裏咱們來到了I/O這一話題。你有沒有想過,當咱們執行文件I/O、網絡I/O操做時計算機底層到底發生了些什麼?對於計算機來講I/O是極其重要的,本篇將帶給你這個問的答案。後端
二、本文做者
應做者要求,不提供真名,也不提供我的照片。服務器
本文做者主要技術方向爲互聯網後端、高併發高性能服務器、檢索引擎技術,網名是「碼農的荒島求生」,公衆號「碼農的荒島求生」。感謝做者的無私分享。網絡
三、不能執行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呢?
四、什麼是I/O?
I/O就是簡單的數據Copy,僅此而已!
這一點很重要!
既然是copy數據,那麼又是從哪裏copy到哪裏呢?
若是數據是從外部設備copy到內存中,這就是Input。
若是數據是從內存copy到外部設備,這就是Output。
內存與外部設備之間不嫌麻煩的來回copy數據就是Input and Output,簡稱I/O(Input/Output),僅此而已。
五、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操做時底層都發生了什麼。
接下來讓咱們以讀取磁盤文件爲例來說解這一過程。
六、執行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,在這裏咱們暫時先不考慮這種方式。
在本節開頭咱們說過暫時只考慮進程而不考慮線程,如今咱們放寬這個條件,實際上也很是簡單,只須要把前圖中調度的進程改成線程就能夠了,這裏的討論對於線程同樣成立。
七、零拷貝(Zero-copy)
最後須要注意的一點就是:上面的講解中咱們直接把磁盤數據copy到了進程空間中,但實際上通常狀況下I/O數據是要首先copy到操做系統內部,而後操做系統再copy到進程空間中。
所以咱們能夠看到這裏其實還有一層通過操做系統的copy,對於性能要求很高的場景其實也是能夠繞過操做系統直接進行數據copy的,這也是本文描述的場景,這種繞過操做系統直接進行數據copy的技術被稱爲Zero-copy,也就零拷貝,高併發、高性能場景下經常使用的一種技術,原理上很簡單吧。
PS:對於搞即時通信開發的Java程序員來講,著名的高性能網絡框架Netty就使用了零拷貝技術,具體能夠讀《NIO框架詳解:Netty的高性能之道》一文的第12節。若是對於Netty框架很好奇但不瞭解的話,能夠因着這兩篇文章入門:《新手入門:目前爲止最透徹的的Netty高性能原理和框架架構解析》、《史上最通俗Netty入門長文:基本介紹、環境搭建、動手實戰》。
八、本文小結
本文講解的是程序員經常使用的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模型》
本文已同步發佈於「即時通信技術圈」公衆號。
▲ 本文在公衆號上的連接是:點此進入。同步發佈連接是:http://www.52im.net/thread-3280-1-1.html