零拷貝的應用程序要求內核(kernel)直接將數據從磁盤文件拷貝到套接字(Socket),而無須通過應用程序。零拷貝不僅提高了應用程序的性能,而且減少了內核和用戶模式見上下文切換。
數據傳輸:傳統方法
從文件中讀取數據,並將數據傳輸到網絡上的另一個程序的場景:從下圖可以看出,拷貝的操作需要4次用戶模式和內核模式之間的上下文切換,而且在操作完成前數據被復制了4次。
從磁盤中copy放到一個內存buf中,然后將buf通過socket傳輸給用戶,下面是偽代碼實現:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
從圖中可以看出文件經歷了4次copy過程:
1.首先,調用read方法,文件從user模式拷貝到了kernel模式;(用戶模式->內核模式的上下文切換,在內部發送sys_read() 從文件中讀取數據,存儲到一個內核地址空間緩存區中)
2.之后CPU控制將kernel模式數據拷貝到user模式下;(內核模式-> 用戶模式的上下文切換,read()調用返回,數據被存儲到用戶地址空間的緩存區中)
3.調用write時候,先將user模式下的內容copy到kernel模式下的socket的buffer中(用戶模式->內核模式,數據再次被放置在內核緩存區中,send()套接字調用)
4.最后將kernel模式下的socket buffer的數據copy到網卡設備中;(send套接字調用返回)
從圖中看2,3兩次copy是多余的,數據從kernel模式到user模式走了一圈,浪費了2次copy。
數據傳輸:零拷貝方法
從傳統的場景看,會注意到上圖,第2次和第3次拷貝根本就是多余的。應用程序只是起到緩存數據被將傳回到套接字的作用而已,別無他用。
應用程序使用zero-copy來請求kernel直接把disk的數據傳輸到socket中,而不是通過應用程序傳輸。zero-copy大大提高了應用程序的性能,並且減少了kernel和user模式的上下文切換。
數據可以直接從read buffer 讀緩存區傳輸到套接字緩沖區,也就是省去了將操作系統的read buffer 拷貝到程序的buffer,以及從程序buffer拷貝到socket buffer的步驟,直接將read buffer拷貝到socket buffer。JDK NIO中的的transferTo()
方法就能夠讓您實現這個操作,這個實現依賴於操作系統底層的sendFile()實現的:
public void transferTo(long position, long count, WritableByteChannel target);
底層調用sendFile方法:
#include <sys/socket.h> ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
使用了zero-copy技術后,整個過程如下:
1.transferTo()方法使得文件的內容直接copy到了一個read buffer(kernel buffer)中
2.然后數據(kernel buffer)copy到socket buffer中
3.最后將socket buffer中的數據copy到網卡設備(protocol engine)中傳輸;
這個顯然是一個偉大的進步:這里上下文切換從4次減少到2次,同時把數據copy的次數從4次降低到3次;
但是這是zero-copy么,答案是否定的;
linux 2.1 內核開始引入了sendfile函數,用於將文件通過socket傳輸。
sendfile(socket, file, len);
該函數通過一次調用完成了文件的傳輸。 該函數通過一次系統調用完成了文件的傳輸,減少了原來read/write方式的模式切換。此外更是減少了數據的copy,sendfile的詳細過程如圖:
通過sendfile傳送文件只需要一次系統調用,當調用sendfile時:
1.首先通過DMA將數據從磁盤讀取到kernel buffer中
2.然后將kernel buffer數據拷貝到socket buffer中
3.最后將socket buffer中的數據copy到網卡設備中(protocol buffer)發送;
sendfile與read/write模式相比,少了一次copy。但是從上述過程中發現從kernel buffer中將數據copy到socket buffer是沒有必要的;
Linux2.4 內核對sendfile做了改進,如圖:
改進后的處理過程如下:
-
將文件拷貝到kernel buffer中;(DMA引擎將文件內容copy到內核緩存區)
-
向socket buffer中追加當前要發生的數據在kernel buffer中的位置和偏移量;
-
根據socket buffer中的位置和偏移量直接將kernel buffer的數據copy到網卡設備(protocol engine)中;
從圖中看到,linux 2.1內核中的 “數據被copy到socket buffer”的動作,在Linux2.4 內核做了優化,取而代之的是只包含關於數據的位置和長度的信息的描述符被追加到了socket buffer 緩沖區中。DMA引擎直接把數據從內核緩沖區傳輸到協議引擎(protocol engine),從而消除了最后一次CPU copy。經過上述過程,數據只經過了2次copy就從磁盤傳送出去了。這個才是真正的Zero-Copy(這里的零拷貝是針對kernel來講的,數據在kernel模式下是Zero-Copy)。
正是Linux2.4的內核做了改進,Java中的TransferTo()實現了Zero-Copy,如下圖:
Zero-Copy技術的使用場景有很多,比如Kafka, 又或者是Netty等,可以大大提升程序的性能。
參考:
[netty內部實現的零copy機制] https://segmentfault.com/a/1190000007560884
[通過零拷貝實現有效數據傳輸]: https://www.ibm.com/developerworks/cn/java/j-zerocopy/
[理解Netty中的零拷貝(Zero-Copy)機制] : https://my.oschina.net/plucury/blog/192577
[Netty系列之Netty高性能之道]:http://www.infoq.com/cn/articles/netty-high-performance
[NIO的好處,Netty線程模型,什么是零拷貝] : http://ghost.mark.ah.cn/2018/05/08/niode-hao-chu-nettyxian-cheng-mo-xing-shi-yao-shi-ling-kao-bei/