零拷貝技術主要包括mmap和sendfile公荧,在RocketMQ雹顺、Kafka這類高性能消息隊列中間件中有應用,在Netty這種高性能網絡通信框架中也有應用龟虎。在Java里mmap和sendfile分別對應MappedByteBuffer和FileChannel.transferTo()河爹,兩者都是Java的nio包提供的能力匠璧。
MappedByteBuffer與mmap
理解mmap內存文件映射需要理解虛擬內存或者說內存虛擬化,實際可以認為是零拷貝技術的一個基石咸这。
使用虛擬化技術夷恍,可以做到讓多個虛擬地址映射到同一片物理地址,這樣硬件設備驅動就可以做到通過DMA對一片同時對內核和應用都可見的內存區(qū)域進行讀寫了媳维。這樣的意義在于酿雪,由于這樣的內存區(qū)域對內核和應用都可見,應用程序才能做到直接操作內核內存去完成一些以往需要到自己應用程序的用戶態(tài)內存進行中轉的讀寫邏輯侄刽。
Java里的mmap內存文件映射能力是通過MappedByteBuffer = FileChannel.map()
這樣一個操作提供的指黎,下面來看一下示例代碼:
/**
* mmap內存映射在Java中的使用 FileChannel.map() -> MappedByteBuffer
*/
@Slf4j
public class MmapTest {
private static String filepath = "D:\\Media\\test.txt";
private static File f = new File(filepath);
// 使用java io包中的緩沖輸入流BufferedInputStream
public static void readFile() {
try {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(f));
byte[] buffer = new byte[1024];
int len = 0;
while ((len = bis.read(buffer)) != -1) { // 從bis讀到byte[] buffer里,讀了len個字節(jié)
log.info("從BufferedInputStream讀了{}", new String(buffer, 0, len, "UTF-8"));
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 使用內存映射
public static void mmapReadFile() {
f = new File(filepath);
int bufferSize = (int) f.length();
byte[] buffer = new byte[bufferSize];
FileChannel fileChannel;
try {
// fileChannel = new RandomAccessFile(f, "rw").getChannel();
fileChannel = FileChannel.open(Paths.get(filepath), StandardOpenOption.READ, StandardOpenOption.WRITE);
/*建立內存映射州丹,用戶態(tài)虛擬內存與文件讀取到的os文件系統(tǒng)內核態(tài)內存映射到相同的物理內存地址
這樣應用程序讀寫用戶態(tài)內存相當于就是讀寫內核態(tài)內存醋安,從而讀寫文件
內核態(tài)內存與磁盤文件之間由os的文件系統(tǒng)管理杂彭,讀的時候在內核內存就直接讀、不在就缺頁中斷吓揪,內核置換頁亲怠;
寫的話直接寫到內核態(tài)內存里,由os負責或手工flush到磁盤柠辞。*/
MappedByteBuffer mappedButeBuffer = fileChannel.map(MapMode.READ_WRITE, 0, f.length());
// 使用內存映射從內核態(tài)內存直接讀取到byte[] buffer里赁炎,因為是內存映射、所以不會發(fā)生從內核態(tài)復制到用戶態(tài)內存的過程钾腺。
ByteBuffer byteBuffer = mappedButeBuffer.get(buffer);
log.info("從MappedByteBuffer讀了{}", new String(buffer, 0, bufferSize, "UTF-8"));
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
readFile();
mmapReadFile();
}
}
上面的代碼很簡單,比較使用InputStream
讀文件內容以及使用MappedByteBuffer
按內存映射的方式讀取文件內容讥裤。
相比傳統(tǒng)的InputStream方式放棒,使用FileChannel.map()
建立內存文件映射,用戶態(tài)虛擬內存與文件通過DMA存入的os文件系統(tǒng)的內核態(tài)內存己英、映射到相同的物理內存地址间螟。 這樣應用程序讀寫用戶態(tài)內存相當于就是讀寫內核態(tài)內存。這也就是相當于讀寫文件:原因在于內核態(tài)內存與磁盤文件之間由os的文件系統(tǒng)管理损肛,讀的時候在內核內存就直接讀厢破、不在就缺頁中斷,內核置換頁治拿; 寫的話直接寫到內核態(tài)內存里摩泪,由os負責或手工flush到磁盤。
內存文件映射建立后得到MappedByteBuffer
劫谅,之后代碼里使用MappedByteBuffer.get(byte[])
將文件內容從內核態(tài)內存直接讀取到byte[]里见坑,因為是虛擬內存映射、所以不會發(fā)生從內核態(tài)復制到用戶態(tài)內存的過程捏检。
FileChannle.transferTo()與sendfile
transferTo好比將兩個流的channel直接進行連接荞驴,而不是像傳統(tǒng)的方式那樣從一個讀出來再寫到另一個去,直接走內核態(tài)的copy贯城,不用經過用戶態(tài)熊楼。底層實際是將文件通過DMA讀取到os文件系統(tǒng)的內核態(tài)內存之后、不復制到用戶態(tài)內存而是直接transfer到內核態(tài)的Socket緩沖區(qū)能犯、再通過DMA寫到網卡通過網絡發(fā)送出去鲫骗。
對應到操作系統(tǒng)層面底層使用的是sendfile內核調用,從Linux2.1開始提供悲雳。Linux2.4之后支持所謂“scatter-gather”特性挎峦,甚至內核態(tài)的copy都不用,target內核態(tài)緩沖已經記錄了src內核態(tài)緩沖的地址合瓢,相當于使用DMA直接從src往device(控制臺坦胶、文件、網路等等)進行輸出。也就是說上面提到的從文件系統(tǒng)內核態(tài)內存到Socket緩沖區(qū)內核態(tài)內存這步也省了顿苇,直接從文件系統(tǒng)內核態(tài)內存通過DMA就寫到了網卡峭咒。
底層系統(tǒng)的sendfile API:
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
說明:
第 1 個參數 out_fd,在 2.6 內核里纪岁,必須指向一個 socket 凑队。
第 2 個參數 in_fd,是一個要拷貝文件的文件fd幔翰。
第 3 個參數 offset, 是一個偏移量漩氨,它在不斷的 sendfile 中,這個偏移量會隨著偏移增加遗增,直到文件發(fā)送完為止叫惊,當然在程序中需要用如 while() 這樣的語句來控制。
第 4 個參數 count做修,表示要傳送的字節(jié)數(在以下示例中霍狰,是 1G 文件的大小,即 buf.st_size)
需要注意的是in_fd必須是一個文件饰及,而out_fd可以是文件和網絡socket等可寫的句柄蔗坯、但底層從2.6內核開始必須是socket了。從這里可以基本可以看出sendfile的使用場景跟它的名字一樣燎含,發(fā)送文件到網絡宾濒。
在Java里,sendfile技術對應的是FileChannel.transferTo()
方法屏箍。下面看一下例子程序:
@Slf4j
public class TestServer {
private ServerSocket ss;
public TestServer(int port) throws Exception {
ss = new ServerSocket(port);
}
public void doAccept() throws Exception {
log.info("TestServer start ...");
while (true) {
Socket client = ss.accept();
log.info("recv a connection " + client);
new Worker(client).start();
}
}
class Worker extends Thread {
Socket client;
byte[] buffer = new byte[1024];
Worker(Socket socket) {
client = socket;
}
@Override
public void run() {
try {
BufferedInputStream bis = new BufferedInputStream(client.getInputStream());
BufferedOutputStream bos = new BufferedOutputStream(client.getOutputStream());
int len = 0;
while ((len = bis.read(buffer)) != -1) {
log.info(new String(buffer, 0, len, "UTF-8"));
bos.write(buffer, 0, len);
}
client.shutdownInput();
bos.flush();
client.shutdownOutput();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != client)
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
try {
TestServer server = new TestServer(6687);
server.doAccept();
} catch (Exception e) {
e.printStackTrace();
}
}
}
我們要模擬一個客戶端使用FileChannel.transferTo發(fā)送文件到服務端鼎兽,上面是一個簡單的SockerServer服務端程序,做的事情也很簡單铣除,把收到文件后把文件內容再返回給客戶端谚咬,下面再看下客戶端:
/**
* sendfile在Java中的應用
*/
@Slf4j
public class SendFileTest {
private static File file = new File("D:\\Media\\test.txt");
public static void sendStream() {
Socket socket = null;
try {
socket = new Socket("127.0.0.1", 6687);
FileInputStream fis = new FileInputStream(file);
OutputStream os = socket.getOutputStream();
InputStream is = socket.getInputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = fis.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
os.flush();
socket.shutdownOutput();
log.info("發(fā)送完畢f(xié)lush and shutdownOutput");
byte[] readBuf = new byte[1024];
is.read(readBuf);
log.info("OutputStream發(fā)送文件收到回復{}", new String(readBuf, "UTF-8"));
socket.shutdownInput();
log.info("讀取回復完畢,shutdownInput");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (socket != null)
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void sendfile() {
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 6687));
FileInputStream fis = new FileInputStream(file);
FileChannel fileChannel = fis.getChannel();
// 從FileChannel直接transfer到SocketChannel尚粘,直接在內核態(tài)完成數據copy
fileChannel.transferTo(0, file.length(), socketChannel);
socketChannel.shutdownOutput();
ByteBuffer buffer = ByteBuffer.allocate(1024);
fileChannel.read(buffer);
log.info("sendfile發(fā)送文件收到回復{}", new String(buffer.array(), "UTF-8"));
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
sendStream();
sendfile();
}
}
客戶端先后用了傳統(tǒng)的socket OutputStream方法和FileChannel.transferTo方法發(fā)送文件择卦,并顯示服務端的返回。
參考
理論:
sendfile“零拷貝”郎嫁、mmap內存映射秉继、DMA - 簡書 (jianshu.com)
什么是零拷貝?mmap與sendFile的區(qū)別是什么泽铛?_The Mamba Mentality的博客-CSDN博客_mmap和sendfile
linux零拷貝原理尚辑,RocketMQ&Kafka使用對比 - 云+社區(qū) - 騰訊云 (tencent.com)
淺析Linux中的零拷貝技術 - 簡書 (jianshu.com)
代碼:
java 零拷貝-- MMAP,sendFile,Channel - 簡書 (jianshu.com)
?【Java深層系列】「并發(fā)編程系列」深入分析和研究MappedByteBuffer的實現(xiàn)原理和開發(fā)指南 - InfoQ 寫作平臺