Java NIO分析: 零拷貝技術以及NIO的支持

Java NIO分析(11): 零拷貝技術以及NIO的支持

 2018-07-24

 JAVA

 NIO LINUX

前面已經講了Selector,SocketChannelDirectBuffer, 這些是NIO網絡編程中最核心的組件
接下來我們會再講幾點非核心的優化(不代表不重要, 只是API不佔NIO設計的大頭):

  • 文件傳輸(File Transfer): 文件內容直接發送到網卡, 或者從網卡直接讀到文件裏
  • 內存映射文件(Memory-mapped Files): 將文件的一塊映射到內存

這兩項本質上都基於零拷貝(zero copy)技術。

1. 零拷貝?

1.1 簡介

零拷貝(Zero-Copy)是指計算機在執行操作時,CPU不需要先將數據從某處內存複製到一個特定區域,從而節省CPU時鐘週期和內存帶寬 —-維基百科

拿常用的網絡文件傳輸過程舉個栗子:

  1. DMAread讀取磁盤文件內容到內核緩衝區
  2. copy內核緩衝區數據到應用進程緩衝區
  3. 從應用進程緩衝區copy數據到socket緩衝區
  4. DMA copy給網卡發送

畫個圖:

可以清楚得看到,有2次copy是沒必要的, 就是上面的2和3,還會平白增加2次用戶態和內核態上下文切換, 在高併發場景下,這些會很致命。

1.2 Zero-Copy分類

解決上面這個問題有幾個思路

  1. 直接I/O: 應用進程直接操作硬件存儲
  2. 避免在用戶空間和內核空間地址之間拷貝數據
  3. 優化頁緩存應用進程緩衝區的傳輸

1和2都是避免應用程序地址空間和內核地址空間兩者之間的緩衝區拷貝, 3是從傳輸的角度優化,因爲DMA進行數據傳輸基本不需要CPU參與,但是用戶地址空間的緩衝區和內核的頁緩存傳輸沒有類似DMA的手段, 3就是從這個角度優化。

1.3 Linux的解決方案

直接I/O傳輸優化都涉及到硬件層面我們暫且不講,主要講避免上下文切換和數據來回拷貝這個思路, Linux內核提供了

  • mmap: 內存映射文件, 即將文件的一段直接映射到內存,內核和應用進程共用一塊內存地址,這樣就不需要拷貝了
  • sendfile: 從上圖的內核緩衝區直接複製到socket緩衝區, 不需要嚮應用進程緩衝區拷貝

如圖,mmap將buffer映射到了用戶空間,操作的是同一塊內存,也不需要切換了, 但是mmap有個缺點就是, 如果其他進程在向這個文件write, 那麼會被認爲是一個錯誤的存儲訪問

sendfile沒有映射, 保留了mmap不需要來回拷貝優點,適用於應用進程不需要對讀取的數據做任何處理的場景,如圖:

2.6以後還提供了splice, splice可以在內核態將數據整塊的從A複製到B地址。

2. NIO中的零拷貝

NIO中通過FileChannel來提供Zero-Copy的支持,分別是

  • FileChannel.map: 將文件的一部分映射到內存
  • FileChannel.transferTo: 將本Channel的文件字節轉移到指定的可寫Channel

FileChannel.map的基本用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * 測試FileChannel的用法
 * 
 * @author sound2gd
 *
 */
public class FileChannnelTest {

	public static void main(String[] args) {
		File file = new File("src/com/cris/chapter15/f6/FileChannnelTest.java");
		try (
				// FileInputStream打開的FileChannel只能讀取
				FileChannel fc = new FileInputStream(file).getChannel();
				// FileOutputStream打開的FileChannel只能寫入
				FileChannel fo = new FileOutputStream("src/com/cris/chapter15/f6/a.txt").getChannel();) {

			// 將FileChannel的數據全部映射成ByteBuffer
			MappedByteBuffer mbb = fc.map(MapMode.READ_ONLY, 0, file.length());
			// 使用UTF-8的字符集來創建解碼器
			Charset charset = Charset.forName("UTF-8");
			// 直接將buffer裏的數據全部輸出
			fo.write(mbb);
			mbb.clear();
			// 創建解碼器
			CharsetDecoder decoder = charset.newDecoder();
			// 使用解碼器將byteBuffer轉換爲CharBuffer
			CharBuffer decode = decoder.decode(mbb);
			System.out.println(decode);
		} catch (Exception e) {

		}
	}

}

這就是一個基本的例子,用於文件複製,可以看到fo.write(mbb)的時候,是將mbb Buffer的數據輸出到另一個文件的,看起來就像是
在內存中,而不是在文件裏, 這就是內存映射文件.

我們來看看map的實現

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public MappedByteBuffer map(MapMode mode, long position, long size)
  ...省略非關鍵代碼
      try {
          // 調用map0這個native方法
          addr = map0(imode, mapPosition, mapSize);
      } catch (OutOfMemoryError x) {
          // An OutOfMemoryError may indicate that we've exhausted memory
          // so force gc and re-attempt map
          // gc下防止內存不夠
          System.gc();
          try {
              // 等待gc結束
              Thread.sleep(100);
          } catch (InterruptedException y) {
              Thread.currentThread().interrupt();
          }
          try {
              // 再試一次
              addr = map0(imode, mapPosition, mapSize);
          } catch (OutOfMemoryError y) {
              // After a second OOME, fail
              throw new IOException("Map failed", y);
          }
      }
  ...
  }
  
  private native long map0(int prot, long position, long length)
  throws IOException;

打開FileChannelImpl.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this,
                                     jint prot, jlong off, jlong len)
{
    void *mapAddress = 0;
    jobject fdo = (*env)->GetObjectField(env, this, chan_fd);
    jint fd = fdval(env, fdo);
    int protections = 0;
    int flags = 0;

    if (prot == sun_nio_ch_FileChannelImpl_MAP_RO) {
        protections = PROT_READ;
        flags = MAP_SHARED;
    } else if (prot == sun_nio_ch_FileChannelImpl_MAP_RW) {
        protections = PROT_WRITE | PROT_READ;
        flags = MAP_SHARED;
    } else if (prot == sun_nio_ch_FileChannelImpl_MAP_PV) {
        protections =  PROT_WRITE | PROT_READ;
        flags = MAP_PRIVATE;
    }

    // 所以還是使用的mmap這個API
    mapAddress = mmap64(
        0,                    /* Let OS decide location */
        len,                  /* Number of bytes to map */
        protections,          /* File permissions */
        flags,                /* Changes are shared */
        fd,                   /* File descriptor of mapped file */
        off);                 /* Offset into file */

    if (mapAddress == MAP_FAILED) {
        if (errno == ENOMEM) {
            JNU_ThrowOutOfMemoryError(env, "Map failed");
            return IOS_THROWN;
        }
        return handle(env, -1, "Map failed");
    }

    return ((jlong) (unsigned long) mapAddress);
}

可以看到,還是使用的我們mmap的api, 瞭解一些底層知識還是有必要的, JVM很多東西都是對底層的一層封裝.

另一個API transferTo同理,最後調用的是transferTo0方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_transferTo0(JNIEnv *env, jobject this,
                                            jint srcFD,
                                            jlong position, jlong count,
                                            jint dstFD)
{
    off64_t offset = (off64_t)position;
    // 調用sendfile方法
    jlong n = sendfile64(dstFD, srcFD, &offset, (size_t)count);
    if (n < 0) {
        if (errno == EAGAIN)
            return IOS_UNAVAILABLE;
        if ((errno == EINVAL) && ((ssize_t)count >= 0))
            return IOS_UNSUPPORTED_CASE;
        if (errno == EINTR) {
            return IOS_INTERRUPTED;
        }
        JNU_ThrowIOExceptionWithLastError(env, "Transfer failed");
        return IOS_THROWN;
    }
    return n;
}

可以看到封裝的是sendfile這個方法,這裏看的是jvm在linux系統的的實現。

3. 總結

本文主要介紹了Linux中Zero-Copy零拷貝的概念,分類和解決方案。

同時介紹了NIO對Zero-Copy的支持, 分別是FileChannel.map以及FileChannel.transferTo.

在高併發場景下,這點提升是很關鍵的,著名框架NettyKafka都大量使用了零拷貝的API, 是其高性能的原因之一。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章