NIO相關基礎篇三

匠心零度發表於2017-12-26

轉載請註明原創出處,謝謝!

說在前面

上篇NIO相關基礎篇二,主要介紹了檔案鎖、以及比較關鍵的Selector,本篇繼續NIO相關話題內容,主要談談一些Linux 網路 I/O模型、零拷貝等一些內容,目前能理解到的就這些了,後續還會繼續有一到二篇左右與NIO內容相關,估計在後續netty等一些學習完成之後,在回過頭來看看NIO系列,再補充補充。

使用者空間以及核心空間概念

我們知道現在作業系統都是採用虛擬儲存器,那麼對32位作業系統而言,它的定址空間(虛擬儲存空間)為4G(2的32次方)。操心繫統的核心是核心,獨立於普通的應用程式,可以訪問受保護的記憶體空間,也有訪問底層硬體裝置的所有許可權。為了保證使用者程式不能直接操作核心,保證核心的安全,操心繫統將虛擬空間劃分為兩部分,一部分為核心空間,一部分為使用者空間。針對linux作業系統而言,將最高的1G位元組(從虛擬地址0xC0000000到0xFFFFFFFF),供核心使用,稱為核心空間,而將較低的3G位元組(從虛擬地址0x00000000到0xBFFFFFFF),供各個程式使用,稱為使用者空間。每個程式可以通過系統呼叫進入核心,因此,Linux核心由系統內的所有程式共享。於是,從具體程式的角度來看,每個程式可以擁有4G位元組的虛擬空間。

空間分配如下圖所示:

NIO相關基礎篇三

有了使用者空間和核心空間,整個linux內部結構可以分為三部分,從最底層到最上層依次是:硬體-->核心空間-->使用者空間。 如下圖所示:

NIO相關基礎篇三

需要注意的細節問題,從上圖可以看出核心的組成:

  1. 核心空間中存放的是核心程式碼和資料,而程式的使用者空間中存放的是使用者程式的程式碼和資料。不管是核心空間還是使用者空間,它們都處於虛擬空間中。
  2. Linux使用兩級保護機制:0級供核心使用,3級供使用者程式使用。

Linux 網路 I/O模型

NIO相關基礎篇三
我們都知道,為了OS的安全性等的考慮,程式是無法直接操作I/O裝置的,其必須通過系統呼叫請求核心來協助完成I/O動作,而核心會為每個I/O裝置維護一個buffer。 如下圖所示:
NIO相關基礎篇三
整個請求過程為: 使用者程式發起請求,核心接受到請求後,從I/O裝置中獲取資料到buffer中,再將buffer中的資料copy到使用者程式的地址空間,該使用者程式獲取到資料後再響應客戶端。

在整個請求過程中,資料輸入至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格式),本文中的流程圖也是擷取自中

NIO相關基礎篇三

NIO相關基礎篇三

記住這兩點很重要 1 等待資料準備 (Waiting for the data to be ready) 2 將資料從核心拷貝到程式中 (Copying the data from the kernel to the process)

阻塞I/O (Blocking I/O)

在linux中,預設情況下所有的socket都是blocking,一個典型的讀操作流程大概是這樣:

NIO相關基礎篇三

當使用者程式呼叫了recvfrom這個系統呼叫,核心就開始了IO的第一個階段:等待資料準備。對於network io來說,很多時候資料在一開始還沒有到達(比如,還沒有收到一個完整的UDP包),這個時候核心就要等待足夠的資料到來。而在使用者程式這邊,整個程式會被阻塞。當核心一直等到資料準備好了,它就會將資料從核心中拷貝到使用者記憶體,然後核心返回結果,使用者程式才解除block的狀態,重新執行起來。 所以,blocking IO的特點就是在IO執行的兩個階段都被block了。

非阻塞I/O (Non-Blocking I/O)

linux下,可以通過設定socket使其變為non-blocking。當對一個non-blocking socket執行讀操作時,流程是這個樣子:

NIO相關基礎篇三

當使用者程式呼叫recvfrom時,系統不會阻塞使用者程式,而是立刻返回一個ewouldblock錯誤,從使用者程式角度講 ,並不需要等待,而是馬上就得到了一個結果。使用者程式判斷標誌是ewouldblock時,就知道資料還沒準備好,於是它就可以去做其他的事了,於是它可以再次傳送recvfrom,一旦核心中的資料準備好了。並且又再次收到了使用者程式的system call,那麼它馬上就將資料拷貝到了使用者記憶體,然後返回。 當一個應用程式在一個迴圈裡對一個非阻塞呼叫recvfrom,我們稱為輪詢。應用程式不斷輪詢核心,看看是否已經準備好了某些操作。這通常是浪費CPU時間,但這種模式偶爾會遇到。

I/O複用(I/O Multiplexing)

IO multiplexing這個詞可能有點陌生,但是如果我說select,epoll,大概就都能明白了。有些地方也稱這種IO方式為event driven IO。我們都知道,select/epoll的好處就在於單個process就可以同時處理多個網路連線的IO。它的基本原理就是select/epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有資料到達了,就通知使用者程式。它的流程如圖:

NIO相關基礎篇三

當使用者程式呼叫了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。

檔案描述符fd

Linux的核心將所有外部裝置都可以看做一個檔案來操作。那麼我們對與外部裝置的操作都可以看做對檔案進行操作。我們對一個檔案的讀寫,都通過呼叫核心提供的系統呼叫;核心給我們返回一個filede scriptor(fd,檔案描述符)。而對一個socket的讀寫也會有相應的描述符,稱為socketfd(socket描述符)。描述符就是一個數字,指向核心中一個結構體(檔案路徑,資料區,等一些屬性)。那麼我們的應用程式對檔案的讀寫就通過對描述符的讀寫完成。

select

**基本原理:**select 函式監視的檔案描述符分3類,分別是writefds、readfds、和exceptfds。呼叫後select函式會阻塞,直到有描述符就緒(有資料 可讀、可寫、或者有except),或者超時(timeout指定等待時間,如果立即返回設為null即可),函式返回。當select函式返回後,可以通過遍歷fdset,來找到就緒的描述符。

缺點: 1、select最大的缺陷就是單個程式所開啟的FD是有一定限制的,它由FD_SETSIZE設定,32位機預設是1024個,64位機預設是2048。 一般來說這個數目和系統記憶體關係很大,”具體數目可以cat /proc/sys/fs/file-max察看”。32位機預設是1024個。64位機預設是2048. 2、對socket進行掃描時是線性掃描,即採用輪詢的方法,效率較低。 當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成排程,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。”如果能給套接字註冊某個回撥函式,當他們活躍時,自動完成相關操作,那就避免了輪詢”,這正是epoll與kqueue做的。 3、需要維護一個用來存放大量fd的資料結構,這樣會使得使用者空間和核心空間在傳遞該結構時複製開銷大。

poll

**基本原理:**poll本質上和select沒有區別,它將使用者傳入的陣列拷貝到核心空間,然後查詢每個fd對應的裝置狀態,如果裝置就緒則在裝置等待佇列中加入一項並繼續遍歷,如果遍歷完所有fd後沒有發現就緒裝置,則掛起當前程式,直到裝置就緒或者主動超時,被喚醒後它又要再次遍歷fd。這個過程經歷了多次無謂的遍歷。

它沒有最大連線數的限制,原因是它是基於連結串列來儲存的,但是同樣有一個缺點: 1、大量的fd的陣列被整體複製於使用者態和核心地址空間之間,而不管這樣的複製是不是有意義。 2 、poll還有一個特點是“水平觸發”,如果報告了fd後,沒有被處理,那麼下次poll時會再次報告該fd。

**注意:**從上面看,select和poll都需要在返回後,通過遍歷檔案描述符來獲取已經就緒的socket。事實上,同時連線的大量客戶端在一時刻可能只有很少的處於就緒狀態,因此隨著監視的描述符數量的增長,其效率也會線性下降。

epoll

epoll是在2.6核心中提出的,是之前的select和poll的增強版本。相對於select和poll來說,epoll更加靈活,沒有描述符限制。epoll使用一個檔案描述符管理多個描述符,將使用者關係的檔案描述符的事件存放到核心的一個事件表中,這樣在使用者空間和核心空間的copy只需一次。

**基本原理:**epoll支援水平觸發和邊緣觸發,最大的特點在於邊緣觸發,它只告訴程式哪些fd剛剛變為就緒態,並且只會通知一次。還有一個特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl註冊fd,一旦該fd就緒,核心就會採用類似callback的回撥機制來啟用該fd,epoll_wait便可以收到通知。

epoll的優點: 1、沒有最大併發連線的限制,能開啟的FD的上限遠大於1024(1G的記憶體上能監聽約10萬個埠)。 2、效率提升,不是輪詢的方式,不會隨著FD數目的增加效率下降。 只有活躍可用的FD才會呼叫callback函式;即Epoll最大的優點就在於它只管你“活躍”的連線,而跟連線總數無關,因此在實際的網路環境中,Epoll的效率就會遠遠高於select和poll。 3、記憶體拷貝,利用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系列裡面進行說明下。

訊號驅動的I/O (Signal Driven I/O)

由於signal driven IO在實際中並不常用,所以簡單提下。

NIO相關基礎篇三

很明顯可以看出使用者程式不是阻塞的。首先使用者程式建立SIGIO訊號處理程式,並通過系統呼叫sigaction執行一個訊號處理函式,這時使用者程式便可以做其他的事了,一旦資料準備好,系統便為該程式生成一個SIGIO訊號,去通知它資料已經準備好了,於是使用者程式便呼叫recvfrom把資料從核心拷貝出來,並返回結果。

非同步I/O

一般來說,這些函式通過告訴核心啟動操作並在整個操作(包括核心的資料到緩衝區的副本)完成時通知我們。這個模型和前面的訊號驅動I/O模型的主要區別是,在訊號驅動的I/O中,核心告訴我們何時可以啟動I/O操作,但是非同步I/O時,核心告訴我們何時I/O操作完成。

NIO相關基礎篇三

當使用者程式向核心發起某個操作後,會立刻得到返回,並把所有的任務都交給核心去完成(包括將資料從核心拷貝到使用者自己的緩衝區),核心完成之後,只需返回一個訊號告訴使用者程式已經完成就可以了。

5中I/O模型的對比

NIO相關基礎篇三

**結果表明:**前四個模型之間的主要區別是第一階段,四個模型的第二階段是一樣的:過程受阻在呼叫recvfrom當資料從核心拷貝到使用者緩衝區。然而,非同步I/O處理兩個階段,與前四個不同。

NIO相關基礎篇三

從同步、非同步,以及阻塞、非阻塞兩個維度來劃分來看:

NIO相關基礎篇三

零拷貝

NIO相關基礎篇三

CPU不執行拷貝資料從一個儲存區域到另一個儲存區域的任務,這通常用於在網路上傳輸檔案時節省CPU週期和記憶體頻寬。

快取 IO

快取 IO 又被稱作標準 IO,大多數檔案系統的預設 IO 操作都是快取 IO。在 Linux 的快取 IO 機制中,作業系統會將 IO 的資料快取在檔案系統的頁快取( page cache )中,也就是說,資料會先被拷貝到作業系統核心的緩衝區中,然後才會從作業系統核心的緩衝區拷貝到應用程式的地址空間。

快取 IO 的缺點:資料在傳輸過程中需要在應用程式地址空間和核心進行多次資料拷貝操作,這些資料拷貝操作所帶來的 CPU 以及記憶體開銷是非常大的。

零拷貝技術分類

零拷貝技術的發展很多樣化,現有的零拷貝技術種類也非常多,而當前並沒有一個適合於所有場景的零拷貝技術的出現。對於 Linux 來說,現存的零拷貝技術也比較多,這些零拷貝技術大部分存在於不同的 Linux 核心版本,有些舊的技術在不同的 Linux 核心版本間得到了很大的發展或者已經漸漸被新的技術所代替。本文針對這些零拷貝技術所適用的不同場景對它們進行了劃分。概括起來,Linux 中的零拷貝技術主要有下面這幾種:

  • 直接 I/O:對於這種資料傳輸方式來說,應用程式可以直接訪問硬體儲存,作業系統核心只是輔助資料傳輸:這類零拷貝技術針對的是作業系統核心並不需要對資料進行直接處理的情況,資料可以在應用程式地址空間的緩衝區和磁碟之間直接進行傳輸,完全不需要 Linux 作業系統核心提供的頁快取的支援。
  • 在資料傳輸的過程中,避免資料在作業系統核心地址空間的緩衝區和使用者應用程式地址空間的緩衝區之間進行拷貝。有的時候,應用程式在資料進行傳輸的過程中不需要對資料進行訪問,那麼,將資料從 Linux 的頁快取拷貝到使用者程式的緩衝區中就可以完全避免,傳輸的資料在頁快取中就可以得到處理。在某些特殊的情況下,這種零拷貝技術可以獲得較好的效能。Linux 中提供類似的系統呼叫主要有 mmap(),sendfile() 以及 splice()。
  • 對資料在 Linux 的頁快取和使用者程式的緩衝區之間的傳輸過程進行優化。該零拷貝技術側重於靈活地處理資料在使用者程式的緩衝區和作業系統的頁快取之間的拷貝操作。這種方法延續了傳統的通訊方式,但是更加靈活。在Linux 中,該方法主要利用了寫時複製技術。

前兩類方法的目的主要是為了避免應用程式地址空間和作業系統核心地址空間這兩者之間的緩衝區拷貝操作。這兩類零拷貝技術通常適用在某些特殊的情況下,比如要傳送的資料不需要經過作業系統核心的處理或者不需要經過應用程式的處理。第三類方法則繼承了傳統的應用程式地址空間和作業系統核心地址空間之間資料傳輸的概念,進而針對資料傳輸本身進行優化。我們知道,硬體和軟體之間的資料傳輸可以通過使用 DMA 來進行,DMA 進行資料傳輸的過程中幾乎不需要CPU參與,這樣就可以把 CPU 解放出來去做更多其他的事情,但是當資料需要在使用者地址空間的緩衝區和 Linux 作業系統核心的頁快取之間進行傳輸的時候,並沒有類似DMA 這種工具可以使用,CPU 需要全程參與到這種資料拷貝操作中,所以這第三類方法的目的是可以有效地改善資料在使用者地址空間和作業系統核心地址空間之間傳遞的效率。

注意,對於各種零拷貝機制是否能夠實現都是依賴於作業系統底層是否提供相應的支援。

NIO相關基礎篇三
當應用程式訪問某塊資料時,作業系統首先會檢查,是不是最近訪問過此檔案,檔案內容是否快取在核心緩衝區,如果是,作業系統則直接根據read系統呼叫提供的buf地址,將核心緩衝區的內容拷貝到buf所指定的使用者空間緩衝區中去。如果不是,作業系統則首先將磁碟上的資料拷貝的核心緩衝區,這一步目前主要依靠DMA來傳輸,然後再把核心緩衝區上的內容拷貝到使用者緩衝區中。 接下來,write系統呼叫再把使用者緩衝區的內容拷貝到網路堆疊相關的核心緩衝區中,最後socket再把核心緩衝區的內容傳送到網路卡上。

從上圖中可以看出,共產生了四次資料拷貝,即使使用了DMA來處理了與硬體的通訊,CPU仍然需要處理兩次資料拷貝,與此同時,在使用者態與核心態也發生了多次上下文切換,無疑也加重了CPU負擔。 在此過程中,我們沒有對檔案內容做任何修改,那麼在核心空間和使用者空間來回拷貝資料無疑就是一種浪費,而零拷貝主要就是為了解決這種低效性。

讓資料傳輸不需要經過user space,使用mmap

我們減少拷貝次數的一種方法是呼叫mmap()來代替read呼叫:

buf = mmap(diskfd, len);
write(sockfd, buf, len);
複製程式碼

應用程式呼叫mmap(),磁碟上的資料會通過DMA被拷貝的核心緩衝區,接著作業系統會把這段核心緩衝區與應用程式共享,這樣就不需要把核心緩衝區的內容往使用者空間拷貝。應用程式再呼叫write(),作業系統直接將核心緩衝區的內容拷貝到socket緩衝區中,這一切都發生在核心態,最後,socket緩衝區再把資料發到網路卡去。

同樣的,看圖很簡單:

NIO相關基礎篇三

使用mmap替代read很明顯減少了一次拷貝,當拷貝資料量很大時,無疑提升了效率。但是使用mmap是有代價的。當你使用mmap時,你可能會遇到一些隱藏的陷阱。例如,當你的程式map了一個檔案,但是當這個檔案被另一個程式截斷(truncate)時, write系統呼叫會因為訪問非法地址而被SIGBUS訊號終止。SIGBUS訊號預設會殺死你的程式併產生一個coredump,如果你的伺服器這樣被中止了,那會產生一筆損失。

通常我們使用以下解決方案避免這種問題:

  1. 為SIGBUS訊號建立訊號處理程式 當遇到SIGBUS訊號時,訊號處理程式簡單地返回,write系統呼叫在被中斷之前會返回已經寫入的位元組數,並且errno會被設定成success,但是這是一種糟糕的處理辦法,因為你並沒有解決問題的實質核心。

  2. 使用檔案租借鎖 通常我們使用這種方法,在檔案描述符上使用租借鎖,我們為檔案向核心申請一個租借鎖,當其它程式想要截斷這個檔案時,核心會向我們傳送一個實時的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系列在看看是否來再來一篇。

結束語

本人水平有限,難免會有一些理解偏差的地方,如果發現,歡迎各位積極指出,感謝!!!


如果讀完覺得有收穫的話,歡迎點贊、關注、加公眾號【匠心零度】,查閱更多精彩歷史!!!

NIO相關基礎篇三

相關文章