還不懂零拷貝(Zero-Copy)?怎麼稱得上高級程序員

概述

考慮這樣一種常用的情形:你需要將靜態內容(類似圖片、文件)展示給用戶。那麼這個情形就意味著你需要先將靜態內容從磁盤中拷貝出來放到一個內存buf中,然後將這個buf通過socket傳輸給用戶,進而用戶或者靜態內容的展示。這看起來再正常不過了,但是實際上這是很低效的流程,我們把上面的這種情形抽象成下面的過程:

read(file, tmp_buf, len);
write(socket, tmp_buf, len);

首先調用read將靜態內容,這裡假設為文件A,讀取到tmp_buf, 然後調用write將tmp_buf寫入到socket中,如圖:

還不懂零拷貝(Zero-Copy)?怎麼稱得上高級程序員

在這個過程中文件A的經歷了4次copy的過程:

  1. 首先,調用read時,文件A拷貝到了kernel模式;
  2. 之後,CPU控制將kernel模式數據copy到user模式下;
  3. 調用write時,先將user模式下的內容copy到kernel模式下的socket的buffer中;
  4. 最後將kernel模式下的socket buffer的數據copy到網卡設備中傳送;

從上面的過程可以看出,數據白白從kernel模式到user模式走了一圈,浪費了2次copy(第一次,從kernel模式拷貝到user模式;第二次從user模式再拷貝回kernel模式,即上面4次過程的第2和3步驟。)。而且上面的過程中kernel和user模式的上下文的切換也是4次。

幸運的是,你可以用一種叫做Zero-Copy的技術來去掉這些無謂的copy。應用程序用Zero-Copy來請求kernel直接把disk的data傳輸給socket,而不是通過應用程序傳輸。Zero-Copy大大提高了應用程序的性能,並且減少了kernel和user模式上下文的切換。

詳述

Zero-Copy技術省去了將操作系統的read buffer拷貝到程序的buffer,以及從程序buffer拷貝到socket buffer的步驟,直接將read buffer拷貝到socket buffer. Java NIO中的FileChannal.transferTo()方法就是這樣的實現,這個實現是依賴於操作系統底層的sendFile()實現的。

public void transferTo(long position, long count, WritableByteChannel target);

他底層的調用時系統調用**sendFile()**方法:

#include 
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

下圖展示了在transferTo()之後的數據流向:

還不懂零拷貝(Zero-Copy)?怎麼稱得上高級程序員

下圖展示了在使用transferTo()之後的上下文切換:

還不懂零拷貝(Zero-Copy)?怎麼稱得上高級程序員

使用了Zero-Copy技術之後,整個過程如下:

  1. transferTo()方法使得文件A的內容直接拷貝到一個read buffer(kernel buffer)中;
  2. 然後數據(kernel buffer)拷貝到socket buffer中。
  3. 最後將socket buffer中的數據拷貝到網卡設備(protocol engine)中傳輸;

這顯然是一個偉大的進步:這裡把上下文的切換次數從4次減少到2次,同時也把數據copy的次數從4次降低到了3次。

但是這是Zero-Copy麼,答案是否定的。

進階

Linux 2.1內核開始引入了sendfile函數(上一節有提到),用於將文件通過socket傳送。

sendfile(socket, file, len);

該函數通過一次系統調用完成了文件的傳送,減少了原來read/write方式的模式切換。此外更是減少了數據的copy, sendfile的詳細過程如圖:

還不懂零拷貝(Zero-Copy)?怎麼稱得上高級程序員

通過sendfile傳送文件只需要一次系統調用,當調用sendfile時:

  1. 首先(通過DMA)將數據從磁盤讀取到kernel buffer中;
  2. 然後將kernel buffer拷貝到socket buffer中;
  3. 最後將socket buffer中的數據copy到網卡設備(protocol engine)中發送;

這個過程就是第二節(詳述)中的那個步驟。

sendfiel與read/write模式相比,少了一次copy。但是從上述過程中也可以發現從kernel buffer中將數據copy到socket buffer是沒有必要的。

Linux2.4 內核對sendfile做了改進,如圖:

還不懂零拷貝(Zero-Copy)?怎麼稱得上高級程序員

改進後的處理過程如下:

  1. 將文件拷貝到kernel buffer中;
  2. 向socket buffer中追加當前要發生的數據在kernel buffer中的位置和偏移量;
  3. 根據socket buffer中的位置和偏移量直接將kernel buffer的數據copy到網卡設備(protocol engine)中;

經過上述過程,數據只經過了2次copy就從磁盤傳送出去了。

這個才是真正的Zero-Copy(這裡的零拷貝是針對kernel來講的,數據在kernel模式下是Zero-Copy)。

正是Linux2.4的內核做了改進,Java中的TransferTo()實現了Zero-Copy,如下圖:

還不懂零拷貝(Zero-Copy)?怎麼稱得上高級程序員

Zero-Copy技術的使用場景有很多,比如Kafka, 又或者是Netty等,可以大大提升程序的性能。

原文鏈接:https://blog.csdn.net/u013256816/article/details/52589524


分享到:


相關文章: