轉載請註明原創出處,謝謝!java
上篇NIO相關基礎篇二,主要介紹了文件鎖、以及比較關鍵的Selector,本篇繼續NIO相關話題內容,主要談談一些Linux 網絡 I/O模型、零拷貝等一些內容,目前能理解到的就這些了,後續還會繼續有一到二篇左右與NIO內容相關,估計在後續netty等一些學習完成以後,在回過頭來看看NIO系列,再補充補充。linux
咱們知道如今操做系統都是採用虛擬存儲器,那麼對32位操做系統而言,它的尋址空間(虛擬存儲空間)爲4G(2的32次方)。操心繫統的核心是內核,獨立於普通的應用程序,能夠訪問受保護的內存空間,也有訪問底層硬件設備的全部權限。爲了保證用戶進程不能直接操做內核,保證內核的安全,操心繫統將虛擬空間劃分爲兩部分,一部分爲內核空間,一部分爲用戶空間。針對linux操做系統而言,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱爲內核空間,而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱爲用戶空間。每一個進程能夠經過系統調用進入內核,所以,Linux內核由系統內的全部進程共享。因而,從具體進程的角度來看,每一個進程能夠擁有4G字節的虛擬空間。程序員
空間分配以下圖所示: web
有了用戶空間和內核空間,整個linux內部結構能夠分爲三部分,從最底層到最上層依次是:硬件-->內核空間-->用戶空間。 以下圖所示: 編程
須要注意的細節問題,從上圖能夠看出內核的組成:數組
- 內核空間中存放的是內核代碼和數據,而進程的用戶空間中存放的是用戶程序的代碼和數據。無論是內核空間仍是用戶空間,它們都處於虛擬空間中。
- Linux使用兩級保護機制:0級供內核使用,3級供用戶程序使用。
在整個請求過程當中,數據輸入至buffer須要時間,而從buffer複製數據至進程也須要時間。所以根據在這兩段時間內等待方式的不一樣,I/O動做能夠分爲如下五種模式:緩存
- 阻塞I/O (Blocking I/O)
- 非阻塞I/O (Non-Blocking I/O)
- I/O複用(I/O Multiplexing)
- 信號驅動的I/O (Signal Driven I/O)
- 異步I/O (Asynchrnous I/O) **說明:**若是像瞭解更多可能須要linux/unix方面的知識了,可自行去學習一些網絡編程原理應該有詳細說明,不過對大多數java程序員來講,不須要了解底層細節,知道個概念就行,知道對於系統而言,底層是支持的。
本文最重要的參考文獻是Richard Stevens的「UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking 」,6.2節「I/O Models 」,公衆號【匠心零度】回覆:linux ,獲取該資料,建議電腦下載(比較大以及chm格式),本文中的流程圖也是截取自中。安全
記住這兩點很重要 1 等待數據準備 (Waiting for the data to be ready) 2 將數據從內核拷貝到進程中 (Copying the data from the kernel to the process)bash
在linux中,默認狀況下全部的socket都是blocking,一個典型的讀操做流程大概是這樣:服務器
當用戶進程調用了recvfrom這個系統調用,內核就開始了IO的第一個階段:等待數據準備。對於network io來講,不少時候數據在一開始尚未到達(好比,尚未收到一個完整的UDP包),這個時候內核就要等待足夠的數據到來。而在用戶進程這邊,整個進程會被阻塞。當內核一直等到數據準備好了,它就會將數據從內核中拷貝到用戶內存,而後內核返回結果,用戶進程才解除block的狀態,從新運行起來。 因此,blocking IO的特色就是在IO執行的兩個階段都被block了。
linux下,能夠經過設置socket使其變爲non-blocking。當對一個non-blocking socket執行讀操做時,流程是這個樣子:
當用戶進程調用recvfrom時,系統不會阻塞用戶進程,而是馬上返回一個ewouldblock錯誤,從用戶進程角度講 ,並不須要等待,而是立刻就獲得了一個結果。用戶進程判斷標誌是ewouldblock時,就知道數據還沒準備好,因而它就能夠去作其餘的事了,因而它能夠再次發送recvfrom,一旦內核中的數據準備好了。而且又再次收到了用戶進程的system call,那麼它立刻就將數據拷貝到了用戶內存,而後返回。 當一個應用程序在一個循環裏對一個非阻塞調用recvfrom,咱們稱爲輪詢。應用程序不斷輪詢內核,看看是否已經準備好了某些操做。這一般是浪費CPU時間,但這種模式偶爾會遇到。
IO multiplexing這個詞可能有點陌生,可是若是我說select,epoll,大概就都能明白了。有些地方也稱這種IO方式爲event driven IO。咱們都知道,select/epoll的好處就在於單個process就能夠同時處理多個網絡鏈接的IO。它的基本原理就是select/epoll這個function會不斷的輪詢所負責的全部socket,當某個socket有數據到達了,就通知用戶進程。它的流程如圖:
當用戶進程調用了select,那麼整個進程會被block,而同時,內核會「監視」全部select負責的socket,當任何一個socket中的數據準備好了,select就會返回。這個時候用戶進程再調用read操做,將數據從內核拷貝到用戶進程。 這個圖和blocking IO的圖其實並無太大的不一樣,事實上,還更差一些。由於這裏須要使用兩個system call (select 和 recvfrom),而blocking IO只調用了一個system call (recvfrom)。可是,用select的優點在於它能夠同時處理多個connection。(多說一句。因此,若是處理的鏈接數不是很高的話,使用select/epoll的web server不必定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。select/epoll的優點並非對於單個鏈接能處理得更快,而是在於能處理更多的鏈接。) 在IO multiplexing Model中,實際中,對於每個socket,通常都設置成爲non-blocking,可是,如上圖所示,整個用戶的process實際上是一直被block的。只不過process是被select這個函數block,而不是被socket IO給block。
Linux的內核將全部外部設備均可以看作一個文件來操做。那麼咱們對與外部設備的操做均可以看作對文件進行操做。咱們對一個文件的讀寫,都經過調用內核提供的系統調用;內核給咱們返回一個filede scriptor(fd,文件描述符)。而對一個socket的讀寫也會有相應的描述符,稱爲socketfd(socket描述符)。描述符就是一個數字,指向內核中一個結構體(文件路徑,數據區,等一些屬性)。那麼咱們的應用程序對文件的讀寫就經過對描述符的讀寫完成。
**基本原理:**select 函數監視的文件描述符分3類,分別是writefds、readfds、和exceptfds。調用後select函數會阻塞,直到有描述符就緒(有數據 可讀、可寫、或者有except),或者超時(timeout指定等待時間,若是當即返回設爲null便可),函數返回。當select函數返回後,能夠經過遍歷fdset,來找到就緒的描述符。
缺點: 一、select最大的缺陷就是單個進程所打開的FD是有必定限制的,它由FD_SETSIZE設置,32位機默認是1024個,64位機默認是2048。 通常來講這個數目和系統內存關係很大,」具體數目能夠cat /proc/sys/fs/file-max察看」。32位機默認是1024個。64位機默認是2048. 二、對socket進行掃描時是線性掃描,即採用輪詢的方法,效率較低。 當套接字比較多的時候,每次select()都要經過遍歷FD_SETSIZE個Socket來完成調度,無論哪一個Socket是活躍的,都遍歷一遍。這會浪費不少CPU時間。」若是能給套接字註冊某個回調函數,當他們活躍時,自動完成相關操做,那就避免了輪詢」,這正是epoll與kqueue作的。 三、須要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時複製開銷大。
**基本原理:**poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,而後查詢每一個fd對應的設備狀態,若是設備就緒則在設備等待隊列中加入一項並繼續遍歷,若是遍歷完全部fd後沒有發現就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒後它又要再次遍歷fd。這個過程經歷了屢次無謂的遍歷。
它沒有最大鏈接數的限制,緣由是它是基於鏈表來存儲的,可是一樣有一個缺點: 一、大量的fd的數組被總體複製於用戶態和內核地址空間之間,而無論這樣的複製是否是有意義。 2 、poll還有一個特色是「水平觸發」,若是報告了fd後,沒有被處理,那麼下次poll時會再次報告該fd。
**注意:**從上面看,select和poll都須要在返回後,經過遍歷文件描述符來獲取已經就緒的socket。事實上,同時鏈接的大量客戶端在一時刻可能只有不多的處於就緒狀態,所以隨着監視的描述符數量的增加,其效率也會線性降低。
epoll是在2.6內核中提出的,是以前的select和poll的加強版本。相對於select和poll來講,epoll更加靈活,沒有描述符限制。epoll使用一個文件描述符管理多個描述符,將用戶關係的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次。
**基本原理:**epoll支持水平觸發和邊緣觸發,最大的特色在於邊緣觸發,它只告訴進程哪些fd剛剛變爲就緒態,而且只會通知一次。還有一個特色是,epoll使用「事件」的就緒通知方式,經過epoll_ctl註冊fd,一旦該fd就緒,內核就會採用相似callback的回調機制來激活該fd,epoll_wait即可以收到通知。
epoll的優勢: 一、沒有最大併發鏈接的限制,能打開的FD的上限遠大於1024(1G的內存上能監聽約10萬個端口)。 二、效率提高,不是輪詢的方式,不會隨着FD數目的增長效率降低。 只有活躍可用的FD纔會調用callback函數;即Epoll最大的優勢就在於它只管你「活躍」的鏈接,而跟鏈接總數無關,所以在實際的網絡環境中,Epoll的效率就會遠遠高於select和poll。 三、內存拷貝,利用mmap()文件映射內存加速與內核空間的消息傳遞;即epoll使用mmap減小複製開銷。
JDK1.5_update10版本使用epoll替代了傳統的select/poll,極大的提高了NIO通訊的性能。
**備註:**JDK NIO的BUG,例如臭名昭著的epoll bug,它會致使Selector空輪詢,最終致使CPU 100%。官方聲稱在JDK1.6版本的update18修復了該問題,可是直到JDK1.7版本該問題仍舊存在,只不過該BUG發生機率下降了一些而已,它並無被根本解決。這個能夠在後續netty系列裏面進行說明下。
因爲signal driven IO在實際中並不經常使用,因此簡單提下。
很明顯能夠看出用戶進程不是阻塞的。首先用戶進程創建SIGIO信號處理程序,並經過系統調用sigaction執行一個信號處理函數,這時用戶進程即可以作其餘的事了,一旦數據準備好,系統便爲該進程生成一個SIGIO信號,去通知它數據已經準備好了,因而用戶進程便調用recvfrom把數據從內核拷貝出來,並返回結果。
通常來講,這些函數經過告訴內核啓動操做並在整個操做(包括內核的數據到緩衝區的副本)完成時通知咱們。這個模型和前面的信號驅動I/O模型的主要區別是,在信號驅動的I/O中,內核告訴咱們什麼時候能夠啓動I/O操做,可是異步I/O時,內核告訴咱們什麼時候I/O操做完成。
當用戶進程向內核發起某個操做後,會馬上獲得返回,並把全部的任務都交給內核去完成(包括將數據從內核拷貝到用戶本身的緩衝區),內核完成以後,只需返回一個信號告訴用戶進程已經完成就能夠了。
**結果代表:**前四個模型之間的主要區別是第一階段,四個模型的第二階段是同樣的:過程受阻在調用recvfrom當數據從內核拷貝到用戶緩衝區。然而,異步I/O處理兩個階段,與前四個不一樣。
從同步、異步,以及阻塞、非阻塞兩個維度來劃分來看:
CPU不執行拷貝數據從一個存儲區域到另外一個存儲區域的任務,這一般用於在網絡上傳輸文件時節省CPU週期和內存帶寬。
緩存 IO 又被稱做標準 IO,大多數文件系統的默認 IO 操做都是緩存 IO。在 Linux 的緩存 IO 機制中,操做系統會將 IO 的數據緩存在文件系統的頁緩存( page cache )中,也就是說,數據會先被拷貝到操做系統內核的緩衝區中,而後纔會從操做系統內核的緩衝區拷貝到應用程序的地址空間。
緩存 IO 的缺點:數據在傳輸過程當中須要在應用程序地址空間和內核進行屢次數據拷貝操做,這些數據拷貝操做所帶來的 CPU 以及內存開銷是很是大的。
零拷貝技術的發展不少樣化,現有的零拷貝技術種類也很是多,而當前並無一個適合於全部場景的零拷貝技術的出現。對於 Linux 來講,現存的零拷貝技術也比較多,這些零拷貝技術大部分存在於不一樣的 Linux 內核版本,有些舊的技術在不一樣的 Linux 內核版本間獲得了很大的發展或者已經漸漸被新的技術所代替。本文針對這些零拷貝技術所適用的不一樣場景對它們進行了劃分。歸納起來,Linux 中的零拷貝技術主要有下面這幾種:
前兩類方法的目的主要是爲了不應用程序地址空間和操做系統內核地址空間這二者之間的緩衝區拷貝操做。這兩類零拷貝技術一般適用在某些特殊的狀況下,好比要傳送的數據不須要通過操做系統內核的處理或者不須要通過應用程序的處理。第三類方法則繼承了傳統的應用程序地址空間和操做系統內核地址空間之間數據傳輸的概念,進而針對數據傳輸自己進行優化。咱們知道,硬件和軟件之間的數據傳輸能夠經過使用 DMA 來進行,DMA 進行數據傳輸的過程當中幾乎不須要CPU參與,這樣就能夠把 CPU 解放出來去作更多其餘的事情,可是當數據須要在用戶地址空間的緩衝區和 Linux 操做系統內核的頁緩存之間進行傳輸的時候,並無相似DMA 這種工具可使用,CPU 須要全程參與到這種數據拷貝操做中,因此這第三類方法的目的是能夠有效地改善數據在用戶地址空間和操做系統內核地址空間之間傳遞的效率。
注意,對於各類零拷貝機制是否可以實現都是依賴於操做系統底層是否提供相應的支持。
從上圖中能夠看出,共產生了四次數據拷貝,即便使用了DMA來處理了與硬件的通信,CPU仍然須要處理兩次數據拷貝,與此同時,在用戶態與內核態也發生了屢次上下文切換,無疑也加劇了CPU負擔。 在此過程當中,咱們沒有對文件內容作任何修改,那麼在內核空間和用戶空間來回拷貝數據無疑就是一種浪費,而零拷貝主要就是爲了解決這種低效性。
咱們減小拷貝次數的一種方法是調用mmap()來代替read調用:
buf = mmap(diskfd, len);
write(sockfd, buf, len);
複製代碼
應用程序調用mmap()
,磁盤上的數據會經過DMA
被拷貝的內核緩衝區,接着操做系統會把這段內核緩衝區與應用程序共享,這樣就不須要把內核緩衝區的內容往用戶空間拷貝。應用程序再調用write()
,操做系統直接將內核緩衝區的內容拷貝到socket
緩衝區中,這一切都發生在內核態,最後,socket
緩衝區再把數據發到網卡去。
一樣的,看圖很簡單:
使用mmap替代read很明顯減小了一次拷貝,當拷貝數據量很大時,無疑提高了效率。可是使用mmap
是有代價的。當你使用mmap
時,你可能會遇到一些隱藏的陷阱。例如,當你的程序map
了一個文件,可是當這個文件被另外一個進程截斷(truncate)時, write系統調用會由於訪問非法地址而被SIGBUS
信號終止。SIGBUS
信號默認會殺死你的進程併產生一個coredump
,若是你的服務器這樣被停止了,那會產生一筆損失。
一般咱們使用如下解決方案避免這種問題:
爲SIGBUS信號創建信號處理程序 當遇到SIGBUS
信號時,信號處理程序簡單地返回,write
系統調用在被中斷以前會返回已經寫入的字節數,而且errno
會被設置成success,可是這是一種糟糕的處理辦法,由於你並無解決問題的實質核心。
使用文件租借鎖 一般咱們使用這種方法,在文件描述符上使用租借鎖,咱們爲文件向內核申請一個租借鎖,當其它進程想要截斷這個文件時,內核會向咱們發送一個實時的RT_SIGNAL_LEASE
信號,告訴咱們內核正在破壞你加持在文件上的讀寫鎖。這樣在程序訪問非法內存而且被SIGBUS
殺死以前,你的write
系統調用會被中斷。write
會返回已經寫入的字節數,而且置errno
爲success。 咱們應該在mmap
文件以前加鎖,而且在操做完文件後解鎖:
if(fcntl(diskfd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
perror("kernel lease set signal");
return -1;
}
/* l_type can be F_RDLCK F_WRLCK 加鎖*/
/* l_type can be F_UNLCK 解鎖*/
if(fcntl(diskfd, F_SETLEASE, l_type)){
perror("kernel lease set type");
return -1;
}
複製代碼
參考: https://www.ibm.com/developerworks/cn/linux/l-cn-zerocopy1/ https://www.jianshu.com/p/fad3339e3448
**說明:**零拷貝目前水平有限,大概先寫這麼多,零拷貝還在持續學習,到時候netty系列在看看是否來再來一篇。
本人水平有限,不免會有一些理解誤差的地方,若是發現,歡迎各位積極指出,感謝!!!
若是讀完以爲有收穫的話,歡迎點贊、關注、加公衆號【匠心零度】,查閱更多精彩歷史!!!