1.传统 IO 拷贝
如图所示,发生 4 次上下文切换以及 4 次数据拷贝(2 次 CPU 拷贝以及 2 次 DMA 拷贝)
2.零拷贝的几种方式
- mmap+write
- sendfile
- 带有 DMA 收集拷贝功能的 sendfile
2.1mmap+write
虚拟内存
现代操作系统使用虚拟内存,即虚拟地址取代物理地址,使用虚拟内存可以有 2 个好处:
- 虚拟内存空间可以远远大于物理内存空间
- 多个虚拟内存可以指向同一个物理地址
正是多个虚拟内存可以指向同一个物理地址 ,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,这样的话,就可以减少 IO 的数据拷贝次数啦,示意图如下
mmap
- 用户进程通过
mmap方法
向操作系统内核发起 IO 调用,上下文从用户态切换为内核态 。 - CPU 利用 DMA 控制器,把数据从硬盘中拷贝到内核缓冲区。
- 上下文从内核态切换回用户态 ,mmap 方法返回。
- 用户进程通过
write
方法向操作系统内核发起 IO 调用,上下文从用户态切换为内核态 。 - CPU 将内核缓冲区的数据拷贝到的 socket 缓冲区。
- CPU 利用 DMA 控制器,把数据从 socket 缓冲区拷贝到网卡,上下文从内核态切换回用户态 ,write 调用返回。
可以发现,mmap+write
实现的零拷贝,I/O 发生了 4 次用户空间与内核空间的上下文切换,以及 3 次数据拷贝。其中 3 次数据拷贝中,包括了 2 次 DMA 拷贝和 1 次 CPU 拷贝 。
mmap
是将读缓冲区的地址和用户缓冲区的地址进行映射,内核缓冲区和应用缓冲区共享,所以节省了一次 CPU 拷贝‘’并且用户进程内存是虚拟的 ,只是映射 到内核的读缓冲区,可以节省一半的内存空间。
2.2 sendfile
- 用户进程发起 sendfile 系统调用,上下文(切换 1)从用户态转向内核态
- DMA 控制器,把数据从硬盘中拷贝到内核缓冲区。
- CPU 将读缓冲区中数据拷贝到 socket 缓冲区
- DMA 控制器,异步把数据从 socket 缓冲区拷贝到网卡,
- 上下文(切换 2)从内核态切换回用户态 ,sendfile 调用返回
sendfile
实现的零拷贝,I/O 发生了 2 次用户空间与内核空间的上下文切换,以及 3 次数据拷贝。其中 3 次数据拷贝中,包括了 2 次 DMA 拷贝和 1 次 CPU 拷贝 。那能不能把 CPU 拷贝的次数减少到 0 次呢?有的,即 带有DMA收集拷贝功能的sendfile
!
2.3 sendfile+DMA scatter/gather
- 用户进程发起 sendfile 系统调用,上下文(切换 1)从用户态转向内核态
- DMA 控制器,把数据从硬盘中拷贝到内核缓冲区。
- CPU 把内核缓冲区中的文件描述符信息 (包括内核缓冲区的内存地址和偏移量)发送到 socket 缓冲区
- DMA 控制器根据文件描述符信息,直接把数据从内核缓冲区拷贝到网卡
- 上下文(切换 2)从内核态切换回用户态 ,sendfile 调用返回。
可以发现,sendfile+DMA scatter/gather
实现的零拷贝,I/O 发生了 2 次用户空间与内核空间的上下文切换,以及 2 次数据拷贝。其中 2 次数据拷贝都是包 DMA 拷贝 。这就是真正的 零拷贝(Zero-copy) 技术,全程都没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。
3. Java 中的零拷贝
3.1 mmap
Java NIO 有一个 MappedByteBuffer
的类,可以用来实现内存映射
try {
//读取文件
FileChannel readChannel = FileChannel.open(Paths.get("./jay.txt"), StandardOpenOption.READ);
//mmap映射读取文件
MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, 1024 * 1024 * 40);
//获取写入通道
FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//数据传输,从mappedBytebuffer中写入到目标通道
writeChannel.write(data);
readChannel.close();
writeChannel.close();
}catch (Exception e){
System.out.println(e.getMessage());
}
3.2 sendfile
FileChannel 的 transferTo()/transferFrom()
,底层就是 sendfile() 系统调用函数。Kafka 这个开源项目就用到它
try {
FileChannel readChannel = FileChannel.open(Paths.get("./jay.txt"), StandardOpenOption.READ);
long len = readChannel.size();
long position = readChannel.position();
FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//数据传输
readChannel.transferTo(position, len, writeChannel);
readChannel.close();
writeChannel.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
4.直接内存
package com.sq.oom;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Date;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
@Slf4j
public class OomApplication {
public static void main(String[] args) {
SpringApplication.run(OomApplication.class, args);
}
@RequestMapping("/test")
public Object testOom() throws FileNotFoundException {
//启动参数 -XX:MaxDirectMemorySize=2m -verbose:gc -XX:+PrintGCDetails 限制了最大直接内存为2MB
//第一次调用没有任何问题,第二次调用直接OOM
service();
return "success";
}
public void service() throws FileNotFoundException {
String path = System.getProperty("java.io.tmpdir") + File.separator;
log.info("file-path={}", path);
String fileName = "test.DAT";
String fName = path + fileName;
File file = new File(fName);
if (file.exists()) {
file.delete();
}
RandomAccessFile rcf = new RandomAccessFile(fName, "rw");
FileChannel channel = rcf.getChannel();
int outLoopSize = 10;
int innerLoopSize = 2000;
long offset = 0;
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(2 * 1024 * 1024);
try {
for (int i = 0; i < outLoopSize; i++) {
StringBuilder builder = new StringBuilder();
for (int j = 0; j < innerLoopSize; j++) {
builder.append(i+"39xxxxxxxx");
}
byte[] bytes = builder.toString().getBytes();
System.out.println("bytes大小" + bytes.length);
byteBuffer.put(bytes, 0, bytes.length);
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
channel.write(byteBuffer);
}
channel.force(true);
byteBuffer.clear();
offset += bytes.length;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
channel.close();
rcf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于