別墅室內(nèi)設(shè)計網(wǎng)站寧波關(guān)鍵詞優(yōu)化企業(yè)網(wǎng)站建設(shè)
文章目錄
- 1. IO基本概念
- 2. 五種IO模型
- 2.1 五個釣魚的例子
- 2.2 五種IO模型
- 2.2.1 阻塞IO
- 2.2.2 非阻塞IO
- 2.2.3 信號驅(qū)動IO
- 2.2.4 IO多路轉(zhuǎn)接
- 2.2.5 異步IO
1. IO基本概念
認(rèn)識IO
IO就是輸入和輸出,在馮諾依曼體系結(jié)構(gòu)中,將數(shù)據(jù)從輸入設(shè)備拷貝到內(nèi)存就叫輸入,將輸出將內(nèi)存拷貝到輸出設(shè)備就叫輸出。
- 對文件進(jìn)行的讀寫操作本質(zhì)就是一種IO,文件IO對應(yīng)的外設(shè)就是磁盤。
- 對網(wǎng)絡(luò)進(jìn)行的讀寫操作本質(zhì)也是一種IO,網(wǎng)絡(luò)IO對應(yīng)的外設(shè)就是網(wǎng)卡。
OS如何得知外設(shè)中有數(shù)據(jù)可讀?
輸入就是操作系統(tǒng)將數(shù)據(jù)從外設(shè)拷貝到內(nèi)存的過程,操作系統(tǒng)一定要通過某種方法得知特定外設(shè)上是否有數(shù)據(jù)就緒。
- 并不是操作系統(tǒng)想要從外設(shè)中取數(shù)據(jù)時外設(shè)中就一定有數(shù)據(jù),比如可能用戶正在訪問服務(wù)器,當(dāng)用戶的請求報文發(fā)出之后就需要在網(wǎng)卡中讀取服務(wù)器發(fā)來的響應(yīng)報文,但此時服務(wù)器可能還沒有收到我們的報文,或是正在對收到的包文進(jìn)行數(shù)據(jù)分析。
- 但操作系統(tǒng)并不會去主動檢測外設(shè)上是否有數(shù)據(jù)就緒,這樣會降低操作系統(tǒng)的工作效率,因為大部分情況下外設(shè)當(dāng)中都是沒有數(shù)據(jù)的,如果操作系統(tǒng)去主動檢測外設(shè)上是否有數(shù)據(jù)那么大部分情況下這樣的操作都是徒勞的。
- 操作系統(tǒng)實際采用的是中斷的方式來得知外設(shè)上是否有數(shù)據(jù)就緒的,當(dāng)某個外設(shè)上面有數(shù)據(jù)就緒時,該外設(shè)就會向CPU的中斷控制器中發(fā)送中斷信號,中斷控制器再根據(jù)產(chǎn)生的中斷信號的優(yōu)先級順序發(fā)送給CPU。
- 每一個中斷信號都有一個對應(yīng)的中斷處理程序,存儲中斷信號和中斷處理程序映射關(guān)系的表叫做中斷向量表,當(dāng)CPU收到某個中斷信號時就會自動停止正在運行的程序,然后根據(jù)該中斷向量表執(zhí)行該中斷信號對應(yīng)的中斷處理程序,處理完畢后再返回被暫停的程序繼續(xù)運行。
需要注意的是,CPU不直接和外設(shè)打交道指的是在數(shù)據(jù)層面上,而外設(shè)其實可以直接將某些控制信號發(fā)給CPU當(dāng)中的某些控制器的。
OS如何處理從網(wǎng)卡中拿到的數(shù)據(jù)包?
操作系統(tǒng)任何時刻可能都會收到大量的數(shù)據(jù)包,因此操作系統(tǒng)必須將這些數(shù)據(jù)包管理起來。所謂的管理就是“先描述,再組織”,在內(nèi)核當(dāng)中有一個結(jié)構(gòu)叫做sk_buff,該結(jié)構(gòu)就是用來管理和控制接收或者發(fā)送數(shù)據(jù)包的信息的。
這是一個簡化版的sk_buff結(jié)構(gòu):
- 當(dāng)操作系統(tǒng)從網(wǎng)卡中讀取到了一個數(shù)據(jù)包之后,就會定義出一個sk_buff結(jié)構(gòu),然后用sk_buff結(jié)構(gòu)當(dāng)中的data指針指向這個讀取到的數(shù)據(jù)包,并將這個定義出來的sk_buff結(jié)構(gòu)與其他sk_buff結(jié)構(gòu)用雙鏈表的形式組織起來,這樣操作系統(tǒng)對各個數(shù)據(jù)包的管理就變成了對雙鏈表的增刪查改了。
- 接下來我們需要將讀取上來的數(shù)據(jù)包交給最底層的鏈路層處理,進(jìn)行鏈路層的解包和分用,此時就是讓sk_buff結(jié)構(gòu)當(dāng)中的mac_header指針指向最初的數(shù)據(jù)包,然后向后讀取鏈路層的報頭,剩下的就是需要交給網(wǎng)絡(luò)層的有效載荷了,此時便完成了鏈路層的解包。
- 這時鏈路層就需要將有效載荷向上交付給網(wǎng)絡(luò)層進(jìn)行解包和分用了,這里所說的向上交付只是形象的說法,實際向上交付并不是要將數(shù)據(jù)從鏈路層的緩沖區(qū)拷貝到網(wǎng)絡(luò)層的緩沖區(qū),而是只需要讓sk_buff結(jié)構(gòu)當(dāng)中的network_header指針指向數(shù)據(jù)包中鏈路層之后的數(shù)據(jù)即可,然后繼續(xù)向后讀取網(wǎng)絡(luò)層的報頭,便完成了網(wǎng)絡(luò)層的解包。
- 然后對傳輸層的處理同理,只需讓sk_buff結(jié)構(gòu)當(dāng)中的transport_header指針指向數(shù)據(jù)包中網(wǎng)絡(luò)層報頭之后的數(shù)據(jù),然后向后讀取傳輸層的報頭,便完成了傳輸層的解包。
- 傳輸層解包之后就可以根據(jù)具體的使用的傳輸層協(xié)議,對應(yīng)將剩下的數(shù)據(jù)寶貝到TCP或者UDP的接收緩沖區(qū)供用戶讀取即可。
發(fā)送數(shù)據(jù)時對數(shù)據(jù)進(jìn)行封裝也是同樣的道理,就是依此在數(shù)據(jù)前面拷貝上對應(yīng)的報頭,最后再將數(shù)據(jù)發(fā)送出去(UDP)或者拷貝到發(fā)送緩沖區(qū)(TCP)即可。也就說,數(shù)據(jù)包在封裝和解包的過程中,本質(zhì)數(shù)據(jù)的存儲位置是沒有發(fā)送變化的,我們實際只是在用不同的指針對數(shù)據(jù)進(jìn)行操作而已。
但內(nèi)核中的sk_buff其實并沒有那么簡單,設(shè)計遠(yuǎn)比上面說的復(fù)雜。
- 一方面,為了保證高效的網(wǎng)絡(luò)報文處理效率,這就要求sk_buff的結(jié)構(gòu)也必須高效。
- 另一方面,sk_buff結(jié)構(gòu)需要被內(nèi)核協(xié)議當(dāng)中的各個協(xié)議共同使用,因此sk_buff必須能夠兼容所有的網(wǎng)絡(luò)協(xié)議。
因此sk_buff結(jié)構(gòu)其實是非常復(fù)雜的。
高效的IO
IO主要分兩步:
- 第一步是等,等待IO就緒。
- 第二步是拷貝,將數(shù)據(jù)拷貝到內(nèi)存或者外設(shè)。
任何IO的過程,都包含等和拷貝這兩個步驟,在實際的應(yīng)用場景中“等”消耗的時間遠(yuǎn)比“拷貝”消耗的時間多,因此要讓IO變得高效,就要盡可能減少“等”的時間。
2. 五種IO模型
2.1 五個釣魚的例子
IO的過程其實和釣魚是非常類似的。
- 釣魚的過程同樣分為“等”和“拷貝”兩個步驟,只不過這里的“等”是等魚上鉤,“拷貝”指的是當(dāng)魚上鉤后將魚從河里“拷貝”到我們的魚桶當(dāng)中。
- IO時等消耗的時間往往要比拷貝消耗的多,釣魚也符合這個特點,釣魚時哦我們大部分時間都在等待魚上鉤,而當(dāng)魚上鉤后只需要一瞬間就能將魚“拷貝”上來。
在了解五種IO模型之前,我們先看看什么樣的釣魚方式才是最高效的。
- 張三:拿了1個魚竿,將魚鉤拋入水中就死死盯著浮漂,什么也不做,讓有魚上鉤后再將魚釣上來。
- 李四:拿了1個魚竿,將魚鉤拋入水中后就去做其他事情,然后定期觀察浮漂,如果有魚上鉤則揮動魚竿將魚釣上來,否則進(jìn)行去做其他事情。
- 王五:拿了1個魚竿,將魚鉤拋入水后在魚竿頂部綁一個鈴鐺,然后就去做其他事情,如果鈴鐺響了就揮動魚竿將魚釣上來。
- 趙六:拿了00個魚竿,將100個魚竿拋入水中就定期觀察這100個魚竿的浮漂,如果某個魚竿有魚上鉤則揮動對應(yīng)的魚竿將魚釣上來。
- 田七:田七是一個有錢的老板,他給了自己的司機(jī)一個桶,一個電話,一個魚竿,當(dāng)司機(jī)去釣魚,當(dāng)魚桶裝滿的時候再告訴田七來拿魚,而田七自己則開車去做其他事情了。
張三、李四、王五的釣魚效率是否一樣?為什么?
本質(zhì)都是一樣的。
- 首先它們的釣魚方式都是一樣的,都是等于上鉤,然后再將魚釣上來。
- 其次,因為它們每個人都是拿的一根魚竿。
因此他們?nèi)齻€的釣魚效率本質(zhì)是一樣的,只不過他們的等待方式不同而已,張三是死等,李四是定期檢測浮漂,王五是通過鈴鐺來判斷魚是否上鉤。
需要注意的是,這里問的是他們的釣魚效率是否一樣,而不是問他們整體誰做的事更多,如果說整體做事情的量的話,那一定是王五做的最多,李四次之,張三最少。
與張三、李四、王五相比,趙六的釣魚效率非常高。
高效的釣魚就是要減少等的時間,增加拷貝的時間,趙六可以將等的時間重合,一次等待多個魚竿有魚上鉤。
毫無疑問,趙六的釣魚效率是四個人中最高的。
如何看待田七的釣魚方式?
田七本人并沒有參數(shù)整個釣魚的過程,他只是發(fā)起了釣魚的任務(wù),真正釣魚的是司機(jī),田七在釣魚期間可能就去做其他事情了。
如果將釣魚看作是一種IO的話,前面四個人的釣魚方式就是同步IO,田七的釣魚方式就是異步IO。
2.2 五種IO模型
實際上上面所說的五種釣魚方式分別對應(yīng)五種IO模型
- 張三這種死等的方式叫做阻塞IO
- 李四這種定時檢測的方式叫做非阻塞IO
- 王五這種通過設(shè)置鈴鐺的方式叫做信號驅(qū)動IO
- 趙六這種一次等待多個魚竿的方式叫做IO多路轉(zhuǎn)接
- 田七這種讓別人幫自己釣魚的方式就是異步IO
通過上面的例子我們可以看到,阻塞IO、非阻塞IO、信號驅(qū)動IO不能提高IO的效率,但是非阻塞IO和信號驅(qū)動IO能提高整體做事的效率。
其實,這個釣魚場景中的各個事務(wù)都能與IO當(dāng)中的相關(guān)概念對應(yīng)起來,比如這里釣魚的河對應(yīng)就是內(nèi)核,這里的每一個人都說進(jìn)程或者線程,魚竿對應(yīng)的就是文件描述符或套接字,裝魚的桶對應(yīng)的就是用戶緩沖區(qū)。
2.2.1 阻塞IO
阻塞IO就是將內(nèi)核將數(shù)據(jù)準(zhǔn)備好之前,系統(tǒng)會一直等待。
阻塞IO是最常見的IO模型,所有的套接字,默認(rèn)都是阻塞方式。
- 比如調(diào)用recvfrom函數(shù)從某個套接字讀取數(shù)據(jù)時,可能是底層數(shù)據(jù)還沒有準(zhǔn)備好,此時就需要等待數(shù)據(jù)就緒,當(dāng)數(shù)據(jù)就緒之后再將數(shù)據(jù)從內(nèi)核拷貝到用戶空間,最后recvfrom函數(shù)才能返回。
- 在recvfrom函數(shù)等待數(shù)據(jù)就緒期間,在用戶看來該進(jìn)程或者線程就阻塞住了,本質(zhì)就是操作系統(tǒng)將該進(jìn)程或線程的狀態(tài)設(shè)置為了某種非R狀態(tài),然后將其放入等待隊列當(dāng)中,當(dāng)數(shù)據(jù)就緒后操作系統(tǒng)再將其從等待隊列中喚醒,然后該進(jìn)程或線程再將數(shù)據(jù)從內(nèi)核拷貝到用戶空間。
以阻塞方式進(jìn)行IO操作的進(jìn)程或者線程,在“等”和“拷貝”期間都不會返回,在用戶看來好像就是阻塞住了,因此我們稱之為阻塞IO。
2.2.2 非阻塞IO
非阻塞IO就是,如果內(nèi)核還未將數(shù)據(jù)準(zhǔn)備好,系統(tǒng)調(diào)用仍然會直接返回,并且返回EWOULDBLOCK錯誤碼。
非阻塞IO往往需要程序員以循環(huán)的方式反復(fù)嘗試讀寫文件描述符,這個過程稱為輪詢,這對CPU來說是一種很大的浪費,一般只有在特定場景下才使用。
- 比如當(dāng)調(diào)用recvfrom函數(shù)以非阻塞方式從某個套接字上讀取數(shù)據(jù)時,如果底層數(shù)據(jù)還沒有準(zhǔn)備好,那么recvfrom函數(shù)會立馬錯誤返回,而不會讓該進(jìn)程或線程進(jìn)行阻塞等待。
- 因為沒有讀取的數(shù)據(jù),因此該進(jìn)程或線程后續(xù)還需要繼續(xù)調(diào)用recvfrom函數(shù),檢測底層數(shù)據(jù)是否就緒,如果沒有就緒數(shù)據(jù),繼續(xù)返回,直到就緒之后,再將數(shù)據(jù)從內(nèi)核拷貝到用戶空間然后成功返回。
- 每次調(diào)用recvfrom函數(shù)讀取數(shù)據(jù)時,不管底層有沒有就緒,recvfrom函數(shù)都會直接返回,在用戶看來該進(jìn)程或線程就沒有被阻塞住,稱為非阻塞IO。
阻塞IO和非阻塞IO的區(qū)別在于,阻塞IO當(dāng)數(shù)據(jù)沒有就緒時,后續(xù)檢測數(shù)據(jù)是否就緒是由操作系統(tǒng)發(fā)起的,而非阻塞IO當(dāng)數(shù)據(jù)沒有就緒時,后續(xù)檢測數(shù)據(jù)是否就緒的工作是由用戶發(fā)起的。
2.2.3 信號驅(qū)動IO
信號驅(qū)動IO就是內(nèi)核將數(shù)據(jù)準(zhǔn)備好的時候,使用SIGIO信號通知程序進(jìn)行IO操作。
當(dāng)?shù)讓訑?shù)據(jù)就緒的時候會向當(dāng)前進(jìn)程或線程遞交SIGIO信號,因此可以通過signal或者sigaction函數(shù)將SIGIO的信號處理程序定義為需要進(jìn)行的IO操作,當(dāng)?shù)讓訑?shù)據(jù)就緒時就會執(zhí)行對應(yīng)的IO操作。
- 比如我們需要調(diào)用recvfrom函數(shù)從某個套接字上讀取數(shù)據(jù),那么就可以將操作定義為SIGIO的信號處理程序。
- 當(dāng)?shù)讓訑?shù)據(jù)就緒時,操作系統(tǒng)就會遞交SIGIO信號,那么此時就會自動執(zhí)行我們定義的信號處理程序,僅需將數(shù)據(jù)從內(nèi)核拷貝到用戶空間。
信號的產(chǎn)生是異步的,但信號驅(qū)動IO是同步IO的一種。
- 我們說信號的產(chǎn)生是異步的,因為信號在任何時刻都可能產(chǎn)生。
- 但信號驅(qū)動是同步IO的一種,因為當(dāng)?shù)讓訑?shù)據(jù)就緒時,當(dāng)前進(jìn)程或線程就需要停下正在做的事情,轉(zhuǎn)而進(jìn)行數(shù)據(jù)的拷貝操作,因此當(dāng)前進(jìn)程或線程仍然需要參與IO過程。
判斷一個IO過程是同步的還是異步的,本質(zhì)就是看當(dāng)前進(jìn)程或線程是否需要參與IO過程,如果要參與那么就是同步IO,否則就是異步IO。
2.2.4 IO多路轉(zhuǎn)接
IO多路轉(zhuǎn)接也叫IO多路復(fù)用,能夠同時等待多個文件描述符的等待裝填。
IO多路轉(zhuǎn)接的思想:
- 因為IO過程分為“等”和“拷貝”兩個步驟,因此我們使用的recvfrom等接口的底層實際上都做了兩件事,第一件事就是當(dāng)數(shù)據(jù)不就緒時需要等,第二件事就是當(dāng)數(shù)據(jù)就緒后需要進(jìn)行拷貝。
- 雖然recvfrom等接口也有等的能力,但這些接口一次只能等一個文件描述符上的數(shù)據(jù)或者空間就緒,這樣IO效率太低了。
- 因此系統(tǒng)為我們提供了三組接口,分別叫做select、poll、epoll,這些接口的核心工作就是等,我們可以將所有等的工作都交給這些多路轉(zhuǎn)接的接口。
- 因為這些多路轉(zhuǎn)接接口是一次等多個文件描述符的,因此能夠?qū)⒌鹊臅r間進(jìn)行重疊,當(dāng)數(shù)據(jù)就緒之后再調(diào)用對應(yīng)的recvfrom等函數(shù)進(jìn)行數(shù)據(jù)的拷貝,此時這些函數(shù)就能夠直接進(jìn)行拷貝,而不需要再進(jìn)行等操作了。
2.2.5 異步IO
異步IO就是數(shù)據(jù)拷貝完成之后,等待應(yīng)用程序。
- 進(jìn)行異步IO需要調(diào)用一些異步IO的接口,異步IO接口調(diào)用后會立馬返回,因為異步IO不需要你進(jìn)行“等”和“拷貝”的操作,這兩個操作都由操作系統(tǒng)完成,你要做的只是發(fā)起IO。
- 當(dāng)IO完成后操作系統(tǒng)會通知應(yīng)用程序,因此進(jìn)行異步IO的進(jìn)程或線程并不參與IO的所有細(xì)節(jié)。