原文出處: http://www.ibm.com/developerworks/library/j-zerocopy/
傳統的I/O
使用傳統的I/O程序讀取文件內容, 並寫入到另一個文件(或Socket), 如下程序:
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
會有較大的性能開銷, 主要表現在一下兩方面:
1. 上下文切換(context switch), 此處有4次用戶態和內核態的切換
2. Buffer內存開銷, 一個是應用程序buffer, 另一個是系統讀取buffer以及socket buffer
其運行示意圖如下
1) 先將文件內容從磁盤中拷貝到操作系統buffer
2) 再從操作系統buffer拷貝到程序應用buffer
3) 從程序buffer拷貝到socket buffer
4) 從socket buffer拷貝到協議引擎.
這是上下文切換圖
1) 調用read(), 程序切換到內核態
2) read()調用完畢, 返回數據, 程序切換回用戶態
3) 調用send(), 程序切換到內核態
4) send()完畢, 程序切換回用戶態
操作系統使用 read buffer 的好處是"預讀", 當你的程序需要對文件數據做處理, 並且每次讀取的數據小於read buffer 的時候, 可以先將多數數據預讀到 read buffer, 這樣程序在讀取的時候效率會更高. 但是當你需要讀取的數據大於操作系統的read buffer的時候, read buffer則會成為累贅.
另外, 在你的程序不需要處理數據, 而僅僅只是做數據轉移的時候, 程序buffer則會成為不必要的開銷.
上面會涉及到多次上下文切換以及多次數據拷貝, 很大一部分cpu及內存開銷是可以避免的, 於是有了zerocopy技術.
ZeroCopy
zerocopy技術省去了將操作系統的read buffer拷貝到程序的buffer, 以及從程序buffer拷貝到socket buffer的步驟, 直接將 read buffer 拷貝到 socket buffer. java 的 FileChannel.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);
如下圖
這樣, 省去了兩次buffer的copy, 並且上下文切換降到了2次(調用transferTo()進入內核態, 調用完畢返回用戶態)
Linux 2.4 及以后的內核, 又做了改進, 不再使用socket buffer, 而是直接將read buffer數據拷貝到協議引擎, 而socket buffer只會記錄數據位置的描述符和數據長度,如下