首頁  >  文章  >  类库下载  >  JAVA IO 以及 NIO 理解

JAVA IO 以及 NIO 理解

坏嘻嘻
坏嘻嘻原創
2018-09-14 09:23:241786瀏覽

由於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() 或 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  
introdd into the process tomom . 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 application hasn't asked for as much data as the kernel buffer holds.ific. 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 multi; #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 was 到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幹預了。

相關推薦:


小結Node.js中非阻塞IO與事件循環_node.js

##Node. js 的非同步 IO 效能探討_node.js

#

以上是JAVA IO 以及 NIO 理解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn