1. 程式人生 > >JAVA IO 以及 NIO 理解

JAVA IO 以及 NIO 理解

由於Netty,瞭解了一些非同步IO的知識,JAVA裡面NIO就是原來的IO的一個補充,本文主要記錄下在JAVA中IO的底層實現原理,以及對Zerocopy技術介紹。

IO,其實意味著:資料不停地搬入搬出緩衝區而已(使用了緩衝區)。比如,使用者程式發起讀操作,導致“ syscall read ”系統呼叫,就會把資料搬入到 一個buffer中;使用者發起寫操作,導致 “syscall write ”系統呼叫,將會把一個 buffer 中的資料 搬出去(傳送到網路中 or 寫入到磁碟檔案)

上面的過程看似簡單,但是底層作業系統具體如何實現以及實現的細節就非常複雜了。正是因為實現方式不同,有針對普通情況下的檔案傳輸(暫且稱普通IO吧),也有針對大檔案傳輸或者批量大資料傳輸的實現方式,比如zerocopy技術。

整個IO過程的流程如下:

1)程式設計師寫程式碼建立一個緩衝區(這個緩衝區是使用者緩衝區):哈哈。然後在一個while迴圈裡面呼叫read()方法讀資料(觸發”syscall read”系統呼叫)

byte[] b = new byte[4096];

while((read = inputStream.read(b))>=0) { total = total + read; // other code…. } 2)當執行到read()方法時,其實底層是發生了很多操作的:

①核心給磁碟控制器發命令說:我要讀磁碟上的某某塊磁碟塊上的資料。–kernel issuing a command to the disk controller hardware to fetch the data from disk.

②在DMA的控制下,把磁碟上的資料讀入到核心緩衝區。–The disk controller writes the data directly into a kernel memory buffer by DMA

③核心把資料從核心緩衝區複製到使用者緩衝區。–kernel copies the data from the temporary buffer in kernel space

這裡的使用者緩衝區應該就是我們寫的程式碼中 new 的 byte[] 陣列。

從上面的步驟中可以分析出什麼?

ⓐ對於作業系統而言,JVM只是一個使用者程序,處於使用者態空間中。而處於使用者態空間的程序是不能直接操作底層的硬體的。而IO操作就需要操作底層的硬體,比如磁碟。因此,IO操作必須得藉助核心的幫助才能完成(中斷,trap),即:會有使用者態到核心態的切換。

ⓑ我們寫程式碼 new byte[] 陣列時,一般是都是“隨意” 建立一個“任意大小”的陣列。比如,new byte[128]、new byte[1024]、new byte[4096]….

但是,對於磁碟塊的讀取而言,每次訪問磁碟讀資料時,並不是讀任意大小的資料的,而是:每次讀一個磁碟塊或者若干個磁碟塊(這是因為訪問磁碟操作代價是很大的,而且我們也相信區域性性原理) 因此,就需要有一個“中間緩衝區”–即核心緩衝區。先把資料從磁碟讀到核心緩衝區中,然後再把資料從核心緩衝區搬到使用者緩衝區。

這也是為什麼我們總感覺到第一次read操作很慢,而後續的read操作卻很快的原因吧。因為,對於後續的read操作而言,它所需要讀的資料很可能已經在核心緩衝區了,此時只需將核心緩衝區中的資料拷貝到使用者緩衝區即可,並未涉及到底層的讀取磁碟操作,當然就快了。

The kernel tries to cache and/or prefetch data, so the data being requested by the process may already be available in kernel space. If so, the data requested by the process is copied out. If the data isn’t available, the process is suspended while the kernel goes about bringing the data into memory. 如果資料不可用,process將會被掛起,並需要等待核心從磁碟上把資料取到核心緩衝區中。

那我們可能會說:DMA為什麼不直接將磁碟上的資料讀入到使用者緩衝區呢?一方面是 ⓑ中提到的核心緩衝區作為一箇中間緩衝區。用來“適配”使用者緩衝區的“任意大小”和每次讀磁碟塊的固定大小。另一方面則是,使用者緩衝區位於使用者態空間,而DMA讀取資料這種操作涉及到底層的硬體,硬體一般是不能直接訪問使用者態空間的(OS的原因吧)

綜上,由於DMA不能直接訪問使用者空間(使用者緩衝區),普通IO操作需要將資料來回地在 使用者緩衝區 和 核心緩衝區移動,這在一定程式上影響了IO的速度。那有沒有相應的解決方案呢?

那就是直接記憶體對映IO,也即JAVA NIO中提到的記憶體對映檔案,或者說 直接記憶體….總之,它們表達的意思都差不多。核心空間的 buffer 與 使用者空間的 buffer 都對映到同一塊 實體記憶體區域。

它的主要特點如下:

①對檔案的操作不需要再發read 或者 write 系統呼叫了—The user process sees the file data asmemory, so there is no need to issue read() or write() system calls.

②當用戶程序訪問“記憶體對映檔案”地址時,自動產生缺頁錯誤,然後由底層的OS負責將磁碟上的資料送到記憶體。關於頁式儲存管理,可參考:記憶體分配與記憶體管理的一些理解

As the user process touches the mapped memory space, page faults will be generated automatically to bring in the file data from disk. If the user modifies the mapped memory space, the affected page is automatically marked as dirty and will be subsequently flushed to disk to update the file.

這就是是JAVA NIO中提到的記憶體對映緩衝區(Memory-Mapped-Buffer)它類似於JAVA NIO中的直接緩衝區(Directed Buffer)。MemoryMappedBuffer可以通過java.nio.channels.FileChannel.java(通道)的 map方法建立。

使用記憶體對映緩衝區來操作檔案,它比普通的IO操作讀檔案要快得多。甚至比使用檔案通道(FileChannel)操作檔案 還要快。因為,使用記憶體對映緩衝區操作檔案時,沒有顯示的系統呼叫(read,write),而且OS還會自動快取一些檔案頁(memory page)

zerocopy技術介紹

看完了上面的IO操作的底層實現過程,再來了解zerocopy技術就很easy了。IBM有一篇名為《Efficient data transfer through zero copy》的論文對zerocopy做了完整的介紹。感覺非常好,下面就基於這篇文來記錄下自己的一些理解。

zerocopy技術的目標就是提高IO密集型JAVA應用程式的效能。在本文的前面部分介紹了:IO操作需要資料頻繁地在核心緩衝區和使用者緩衝區之間拷貝,而zerocopy技術可以減少這種拷貝的次數,同時也降低了上下文切換(使用者態與核心態之間的切換)的次數。

比如,大多數WEB應用程式執行的一項操作就是:接受使用者請求—>從本地磁碟讀資料—>資料進入核心緩衝區—>使用者緩衝區—>核心緩衝區—>使用者緩衝區—>socket傳送

資料每次在核心緩衝區與使用者緩衝區之間的拷貝會消耗CPU以及記憶體的頻寬。而zerocopy有效減少了這種拷貝次數。

Each time data traverses the user-kernel boundary, it must be copied, which consumes CPU cycles and memory bandwidth. Fortunately, you can eliminate these copies through a technique called—appropriately enough —zero copy

那它是怎麼做到的呢?

我們知道,JVM(JAVA虛擬機器)為JAVA語言提供了跨平臺的一致性,遮蔽了底層作業系統的具體實現細節,因此,JAVA語言也很難直接使用底層作業系統提供的一些“奇技淫巧”。

而要實現zerocopy,首先得有作業系統的支援。其次,JDK類庫也要提供相應的介面支援。幸運的是,自JDK1.4以來,JDK提供了對NIO的支援,通過java.nio.channels.FileChannel類的transferTo()方法可以直接將位元組傳送到可寫的通道中(Writable Channel),並不需要將位元組送入使用者程式空間(使用者緩衝區)

You can use the transferTo()method to transfer bytes directly from the channel on which it is invoked to another writable byte channel, without requiring data to flow through the application

下面就來詳細分析一下經典的web伺服器(比如檔案伺服器)乾的活:從磁碟中中讀檔案,並把檔案通過網路(socket)傳送給Client。

File.read(fileDesc, buf, len); Socket.send(socket, buf, len); 從程式碼上看,就是兩步操作。第一步:將檔案讀入buf;第二步:將 buf 中的資料通過socket傳送出去。但是,這兩步操作需要四次上下文切換(使用者態與核心態之間的切換) 和 四次拷貝操作才能完成。

①第一次上下文切換髮生在 read()方法執行,表示伺服器要去磁碟上讀檔案了,這會導致一個 sys_read()的系統呼叫。此時由使用者態切換到核心態,完成的動作是:DMA把磁碟上的資料讀入到核心緩衝區中(這也是第一次拷貝)。

②第二次上下文切換髮生在read()方法的返回(這也說明read()是一個阻塞呼叫),表示資料已經成功從磁碟上讀到核心緩衝區了。此時,由核心態返回到使用者態,完成的動作是:將核心緩衝區中的資料拷貝到使用者緩衝區(這是第二次拷貝)。

③第三次上下文切換髮生在 send()方法執行,表示伺服器準備把資料傳送出去了。此時,由使用者態切換到核心態,完成的動作是:將使用者緩衝區中的資料拷貝到核心緩衝區(這是第三次拷貝)

④第四次上下文切換髮生在 send()方法的返回【這裡的send()方法可以非同步返回,所謂非同步返回就是:執行緒執行了send()之後立即從send()返回,剩下的資料拷貝及傳送就交給底層作業系統實現了】。此時,由核心態返回到使用者態,完成的動作是:將核心緩衝區中的資料送到 protocol engine.(這是第四次拷貝)

這裡對 protocol engine不是太瞭解,但是從上面的示例圖來看:它是NIC(NetWork Interface Card) buffer。網絡卡的buffer???

下面這段話,非常值得一讀:這裡再一次提到了為什麼需要核心緩衝區。

複製程式碼 Use of the intermediate kernel buffer (rather than a direct transfer of the data into the user buffer)might seem inefficient. But intermediate kernel buffers were introduced into the process to improve performance. Using the intermediate buffer on the read side allows the kernel buffer to act as a “readahead cache” when the application hasn’t asked for as much data as the kernel buffer holds. This significantly improves performance when the requested data amount is less than the kernel buffer size. The intermediate buffer on the write side allows the write to complete asynchronously. 複製程式碼 一個核心觀點就是:核心緩衝區提高了效能。咦?是不是很奇怪?因為前面一直說正是因為引入了核心緩衝區(中間緩衝區),使得資料來回地拷貝,降低了效率。

那先來看看,它為什麼說核心緩衝區提高了效能。

對於讀操作而言,核心緩衝區就相當於一個“readahead cache”,當用戶程式一次只需要讀一小部分資料時,首先作業系統從磁碟上讀一大塊資料到核心緩衝區,使用者程式只取走了一小部分( 我可以只 new 了一個 128B的byte陣列啊! new byte[128])。當用戶程式下一次再讀資料,就可以直接從核心緩衝區中取了,作業系統就不需要再次訪問磁碟啦!因為使用者要讀的資料已經在核心緩衝區啦!這也是前面提到的:為什麼後續的讀操作(read()方法呼叫)要明顯地比第一次快的原因。從這個角度而言,核心緩衝區確實提高了讀操作的效能。

再來看寫操作:可以做到 “非同步寫”(write asynchronously)。也即:wirte(dest[]) 時,使用者程式告訴作業系統,把dest[]陣列中的內容寫到XX檔案中去,於是write方法就返回了。作業系統則在後臺默默地把使用者緩衝區中的內容(dest[])拷貝到核心緩衝區,再把核心緩衝區中的資料寫入磁碟。那麼,只要核心緩衝區未滿,使用者的write操作就可以很快地返回。這應該就是非同步刷盤策略吧。

(其實,到這裡。以前一個糾結的問題就是同步IO,非同步IO,阻塞IO,非阻塞IO之間的區別已經沒有太大的意義了。這些概念,只是針對的看問題的角度不一樣而已。阻塞、非阻塞是針對執行緒自身而言;同步、非同步是針對執行緒以及影響它的外部事件而言….)【更加完美、精闢的解釋可以參考這個系列的文章:系統間通訊(3)——IO通訊模型和JAVA實踐 上篇】

既然,你把核心緩衝區說得這麼強大和完美,那還要 zerocopy幹嘛啊???

Unfortunately, this approach itself can become a performance bottleneck if the size of the data requested is considerably larger than the kernel buffer size. The data gets copied multiple times among the disk, kernel buffer, and user buffer before it is finally delivered to the application. Zero copy improves performance by eliminating these redundant data copies. 終於輪到zerocopy粉墨登場了。當需要傳輸的資料遠遠大於核心緩衝區的大小時,核心緩衝區就會成為瓶頸。這也是為什麼zerocopy技術合適大檔案傳輸的原因。核心緩衝區為啥成為了瓶頸?—我想,很大的一個原因是它已經起不到“緩衝”的功能了,畢竟傳輸的資料量太大了。

下面來看看zerocopy技術是如何來處理檔案傳輸的。

當 transferTo()方法 被呼叫時,由使用者態切換到核心態。完成的動作是:DMA將資料從磁碟讀入 Read buffer中(第一次資料拷貝)。然後,還是在核心空間中,將資料從Read buffer 拷貝到 Socket buffer(第二次資料拷貝),最終再將資料從 Socket buffer 拷貝到 NIC buffer(第三次資料拷貝)。然後,再從核心態返回到使用者態。

上面整個過程就只涉及到了:三次資料拷貝和二次上下文切換。感覺也才減少了一次資料拷貝嘛。但這裡已經不涉及使用者空間的緩衝區了。

三次資料拷貝中,也只有一次拷貝需要到CPU的干預。(第2次拷貝),而前面的傳統資料拷貝需要四次且有三次拷貝需要CPU的干預。

This is an improvement: we’ve reduced the number of context switches from four to two and reduced the number of data copies from four to three (only one of which involves the CPU)

如果說zerocopy技術只能完成到這步,那也就 just so so 了。

We can further reduce the data duplication done by the kernel if the underlying network interface card supports gather operations. In Linux kernels 2.4 and later, the socket buffer descriptor was modified to accommodate this requirement. This approach not only reduces multiple context switches but also eliminates the duplicated data copies that require CPU involvement. 也就是說,如果底層的網路硬體以及作業系統支援,還可以進一步減少資料拷貝次數 以及 CPU干預次數。

這裡一共只有兩次拷貝 和 兩次上下文切換。而且這兩次拷貝都是DMA copy,並不需要CPU干預(嚴謹一點的話就是不完全需要吧.)。

整個過程如下:

使用者程式執行 transferTo()方法,導致一次系統呼叫,從使用者態切換到核心態。完成的動作是:DMA將資料從磁碟中拷貝到Read buffer

用一個描述符標記此次待傳輸資料的地址以及長度,DMA直接把資料從Read buffer 傳輸到 NIC buffer。資料拷貝過程都不用CPU干預了。

總結:

這篇文章從IO底層實現原理開始講解,分析了IO底層實現細節的一些優缺點,以及為什麼引入zerocopy技術和zerocopy技術的實現原理。個人的學習記錄,轉載請註明出處。

參考文獻:

1)《JAVA NIO》O’Reilly出版社

2)《Efficient data transfer through zero copy》IBM出版

3)Zero Copy I: User-Mode Perspective