颠覆传统IO:零拷贝技术如何重塑Java高性能编程?

01 引言

零拷贝(Zero-copy)是高性能IO绕不开的话题,通过减少拷贝次数(IO次数)来提高IO的性能。

要了解零拷贝就要先了解一下几个基本概念:用户态(User Mode)、内核态(Kernal Mode)、虚拟内存以及DMA

  • 用户态:也叫用户空间,该区域主要用来运行程序代码,为了保证系统内核的安全,它不能直接访问内存等硬件设备,必须通过系统调用进入到内核态来访问那些受限的资源。
  • 内核态:也可以成为内核空间,该区域是运行内核代码的地方,就是计算底层的代码,如80中断等。可以执行任意的指令访问系统资源,既可以访问内核空间也可以访问用户空间。
  • 虚拟内存:操作系统为每个进程分配了独立的虚拟地址空间,也就是虚拟内存,虚拟地址空间又分为用户空间和内核空间。
  • DMA:内存直接读取(Direct Memory Access),又被人成为协处理器。是一种允许外围设备(硬件子系统)直接访问系统主内存的机制。

02 基本概念

我们先了解一下传统的IO,对于用户进程发出读写指令时,计算机是如何运行的。

  • 当用户进程调用read(),用户态无法调用内核态的设备,只能触发系统调用(IO)。这时计算机需要从用户态切换为内核态。
  • 到达内核态之后,计算机通过DMA控制器将数据从磁盘读取出来,放到内核的缓冲区。完成第一次拷贝。
  • CPU需要将缓冲区的数据拷贝到用户态的缓冲区,完成第二次拷贝,也是read()函数的返回。这时计算器需要从内核态切换为用户态。
  • 因为最终的数据需要通过网卡输出,所以用户进程就需要调用write()函数,CPU将用户缓冲区的数据拷贝到Socket缓冲区,完成第三次拷贝。同时需要再次触发系统调用。这时计算机又需要从用户态切换为内核态。
  • DMA控制器把数据从Socket缓冲区,拷贝到网卡设备输出,至此完成第四不拷贝。同时需要将内核态切换为用户态,write()函数返回。

从流程来看,传统的IO读写操作需要四次的拷贝,以及用户态和内核态的来回切换。而这些操作都是比较消耗资源的。所以传统的IO有它的瓶颈。

那这样的问题能不能解决呢?

当然可以,零拷贝的技术正是为此而来。

03 零拷贝

零拷贝(Zero-Copy)一般指的是从磁盘读取文件发送到网络或者从网络接收数据写入到磁盘文件的过程中,最大程度的减少数据的拷贝次数。零拷贝并不是真正上将拷贝的次数变成0,只是减少。

Java中也可以使用零拷贝技术, 主要是java.nio.channels.FileChannel中的方法:

  • transferTo(long position, long count, WritableByteChannel target)
  • transferFrom(ReadableByteChannel src,long position, long count)

用来实现字节数据从一个channel转换到另一个channel中,这里就是Page CacheSocket缓冲区的拷贝。

从示例图中,可以看到将两次拷贝变成了一次拷贝,总共三次拷贝。

  • 用户进程调用了FileChannel.transferTo()后,触发系统调用,系统从用户态切换到内核态。
  • DMA协处理器将文件数据拷贝到内核缓冲区Page Cahe。这是第一次拷贝。
  • CPU将内核缓冲区的数据拷贝到Socket缓冲区,完成第二次拷贝。
  • DMA又将Socket缓冲区的数据拷贝到网卡进行数据传输,完成第三次拷贝。

内核缓冲区到Socket缓冲区都属于内核态,能不能共享或者取消这次拷贝呢?这个属于系统层面的设计。而linux 2.4版本中已经做了优化。

“Then by using the DMA scatter/gather operation, the network interface card can gather all the data from different memory locations and store the assembled packet in the network card buffer.”

避免了从内核缓冲区拷贝到 Socket 缓冲区的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。

3.1 Java的场景

@Test
void test06() throws Exception {
    StopWatch watch = new StopWatch();
    watch.start();
    try (FileInputStream fileInputStream = new FileInputStream(Path.of("src/main/file/upload/10最大图片.jpg").toFile());
         FileOutputStream outputStream = new FileOutputStream(Path.of("src/main/file/temp/part.jpg").toFile())) {
        byte[] buffer = new byte[1024];
        int len = 0;
        while ((len = fileInputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, len);
        }
    }
    watch.stop();
    System.out.println(watch.getTotalTimeMillis());

    System.out.println("----------------------------------");
    StopWatch watch2 = new StopWatch();
    watch2.start();
    try (RandomAccessFile sraf = new RandomAccessFile("src/main/file/upload/10最大图片.jpg", "r");
         RandomAccessFile raf = new RandomAccessFile("src/main/file/temp/part1.jpg", "rw")) {
        long length = sraf.length();
        System.out.println("length:" + length);
        
        // 调用transferTo方法
        sraf.getChannel().transferTo(0, length, raf.getChannel());
    }
    watch2.stop();
    System.out.println(watch2.getTotalTimeMillis());
}

执行结果:

我们可以看到使用零拷贝,是传统的IO性能的10倍左右。意不意外,惊喜不惊喜!

3.2 Socket场景

Linux 中系统调用 sendfile() 可以实现将数据从一个文件描述符传输到另一个文件描述符,从而实现了零拷贝技术。


try (ServerSocketChannel server = ServerSocketChannel.open();
     FileChannel fileChannel = FileChannel.open(Paths.get("data.big"), READ)) {
     
    server.bind(new InetSocketAddress(9000));
    SocketChannel client = server.accept();
    
    fileChannel.transferTo(0, fileChannel.size(), client);
}

04 零拷贝技术栈扩展

框架零拷贝实现类应用场景
NettyFileRegion大文件网络传输
RocketMQMappedFile消息存储
TomcatSendfileFeature静态资源传输
gRPCByteBufferInputStream流式数据传输

05 小结

零拷贝和传统IO的性能产生了相当大的性能差异,赶快用起来吧。零拷贝的实现方式有很多如mmapsendfiledmadirectI/O等。

你了解几个呢?评论区留言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

智_永无止境

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值