颠覆传统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的场景

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() 可以实现将数据从一个文件描述符传输到另一个文件描述符,从而实现了零拷贝技术。

java 复制代码
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 零拷贝技术栈扩展

框架 零拷贝实现类 应用场景
Netty FileRegion 大文件网络传输
RocketMQ MappedFile 消息存储
Tomcat SendfileFeature 静态资源传输
gRPC ByteBufferInputStream 流式数据传输

05 小结

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

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

相关推荐
胚芽鞘6813 分钟前
关于java项目中maven的理解
java·数据库·maven
岁忧1 小时前
(LeetCode 面试经典 150 题 ) 11. 盛最多水的容器 (贪心+双指针)
java·c++·算法·leetcode·面试·go
CJi0NG1 小时前
【自用】JavaSE--算法、正则表达式、异常
java
Nejosi_念旧1 小时前
解读 Go 中的 constraints包
后端·golang·go
风无雨1 小时前
GO 启动 简单服务
开发语言·后端·golang
Hellyc1 小时前
用户查询优惠券之缓存击穿
java·redis·缓存
小明的小名叫小明1 小时前
Go从入门到精通(19)-协程(goroutine)与通道(channel)
后端·golang
斯普信专业组2 小时前
Go语言包管理完全指南:从基础到最佳实践
开发语言·后端·golang
今天又在摸鱼2 小时前
Maven
java·maven
老马啸西风2 小时前
maven 发布到中央仓库常用脚本-02
java·maven