斟酌這樣1種經常使用的情形:你需要將靜態內容(類似圖片、文件)展現給用戶。那末這個情形就意味著你需要先將靜態內容從磁盤中拷貝出來放到1個內存buf中,然后將這個buf通過socket傳輸給用戶,進而用戶或靜態內容的展現。這看起來再正常不過了,但是實際上這是很低效的流程,我們把上面的這類情形抽象成下面的進程:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
首先調用read將靜態內容,這里假定為文件A,讀取到tmp_buf, 然后調用write將tmp_buf寫入到socket中,如圖:
在這個進程中文件A的經歷了4次copy的進程:
從上面的進程可以看出,數據白白從kernel模式到user模式走了1圈,浪費了2次copy(第1次,從kernel模式拷貝到user模式;第2次從user模式再拷貝回kernel模式,即上面4次進程的第2和3步驟。)。而且上面的進程中kernel和user模式的上下文的切換也是4次。
榮幸的是,你可以用1種叫做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 <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
下圖展現了在transferTo()以后的數據流向:
下圖展現了在使用transferTo()以后的上下文切換:
使用了Zero-Copy技術以后,全部進程以下:
但是這是Zero-Copy么,答案是不是定的。
Linux 2.1內核開始引入了sendfile函數(上1節有提到),用于將文件通過socket傳送。
sendfile(socket, file, len);
該函數通過1次系統調用完成了文件的傳送,減少了原來read/write方式的模式切換。另外更是減少了數據的copy, sendfile的詳細進程如圖:
通過sendfile傳送文件只需要1次系統調用,當調用sendfile時:
這個進程就是第2節(詳述)中的那個步驟。
sendfiel與read/write模式相比,少了1次copy。但是從上述進程中也能夠發現從kernel buffer中將數據copy到socket buffer是沒有必要的。
Linux2.4 內核對sendfile做了改進,如圖:
改進后的處理進程以下:
經過上述進程,數據只經過了2次copy就從磁盤傳送出去了。
這個才是真實的Zero-Copy(這里的零拷貝是針對kernel來說的,數據在kernel模式下是Zero-Copy)。
正是Linux2.4的內核做了改進,Java中的TransferTo()實現了Zero-Copy,以下圖:
Zero-Copy技術的使用處景有很多,比如Kafka, 又或是Netty等,可以大大提升程序的性能。