0 前言

近期遇到难题:1个大数据集的查询导出API,因从数据库查询后占用内存极大,每次调用将消耗近100MB的JVM内存资源。故现需考虑研究和应用零拷贝技术。

如下全文摘自:

看一遍就理解:零拷贝原理详解 - Zhihu/捡田螺的小男孩

零拷贝是老生常谈的问题啦,大厂非常喜欢问。比如 Kafka 为什么快,RocketMQ 为什么快等,都涉及到零拷贝知识点。最近技术讨论群几个伙伴分享了阿里、虾皮的面试真题,也都涉及到零拷贝。因此本文将跟大家一起来学习零拷贝原理。

Kafka
rocketmq

Netty
nginx
apache
  • 什么是零拷贝
  • 传统的IO执行流程
  • 零拷贝相关的知识点回顾
  • 零拷贝实现的几种方式
  • java提供的零拷贝方式

1 什么是零拷贝

零拷贝字面上的意思包括两个,“零”和“拷贝”:

“拷贝”:就是指数据从一个存储区域转移到另一个存储区域。
“零” :表示次数为0,它表示拷贝数据的次数为0。
合起来,那零拷贝就是不需要将数据从一个存储区域复制到另一个存储区域咯。

零拷贝是指计算机执行IO操作时,CPU不需要将数据从一个存储区域复制到另一个存储区域,从而可以减少上下文切换以及CPU的拷贝时间。它是一种I/O操作优化技术。

2 传统 IO 的执行流程

做服务端开发的小伙伴,文件下载功能应该实现过不少了吧。如果你实现的是一个web程序,前端请求过来,服务端的任务就是:将服务端主机磁盘中的文件从已连接的socket发出去。关键实现代码如下:

while((n = read(diskfd, buf, BUF_SIZE)) > 0)
    write(sockfd, buf , n);

传统的IO流程,包括readwrite的过程。

  • read:把数据从磁盘读取到内核缓冲区,再拷贝到用户缓冲区
  • write:先把数据写入到socket缓冲区,最后写入网卡设备。

流程图如下:


[软件体系结构/架构]零拷贝技术(Zero-copy)[转发]-小白菜博客

  • 用户应用进程调用read函数,向操作系统发起IO调用,上下文从用户态转为内核态(切换1)
  • DMA控制器把数据从磁盘中,读取到内核缓冲区。
  • CPU把内核缓冲区数据,拷贝到用户应用缓冲区,上下文从内核态转为用户态(切换2),read函数返回
  • 用户应用进程通过write函数,发起IO调用,上下文从用户态转为内核态(切换3)
  • CPU将用户缓冲区中的数据,拷贝到socket缓冲区
  • DMA控制器把数据从socket缓冲区,拷贝到网卡设备,上下文从内核态切换回用户态(切换4),write函数返回

从流程图可以看出:

传统IO的读写流程,包括了:

  • 4次上下文切换(4次用户态和内核态的切换)
  • 4次数据拷贝(2次CPU拷贝、2次的DMA拷贝)

什么是DMA拷贝呢?我们一起来回顾下,零拷贝涉及的操作系统知识点哈。

3 零拷贝相关的知识点回顾

3.1 内核空间和用户空间

我们电脑上跑着的应用程序,其实是需要经过操作系统,才能做一些特殊操作,如磁盘文件读写、内存的读写等等。因为这些都是比较危险的操作,不可以由应用程序乱来,只能交给底层操作系统来。

因此,操作系统为每个进程都分配了内存空间,一部分是用户空间,一部分是内核空间。内核空间是操作系统内核访问的区域,是受保护的内存空间,而用户空间是用户应用程序访问的内存区域。 以32位操作系统为例,它会为每一个进程都分配了4G(2的32次方)的内存空间。

  • 内核空间:主要提供进程调度、内存分配、连接硬件资源等功能
  • 用户空间:提供给各个程序进程的空间,它不具有访问内核空间资源的权限,如果应用程序需要使用到内核空间的资源,则需要通过系统调用来完成。进程从用户空间切换到内核空间,完成相关操作后,再从内核空间切换回用户空间。

3.2 什么是用户态、内核态

如果进程运行于内核空间,被称为进程的内核态
如果进程运行于用户空间,被称为进程的用户态

3.3 什么是上下文切换

3.3.1 什么是CPU上下文?

CPU 寄存器,是CPU内置的容量小、但速度极快的内存。
程序计数器,则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。
它们都是 CPU 在运行任何任务前,必须的依赖环境,因此叫做CPU上下文

3.3.2 什么是CPU上下文切换?

它是指,先把前一个任务的CPU上下文(也就是CPU寄存器和程序计数器)保存起来;
然后,加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

一般我们说的上下文切换,就是指内核(操作系统的核心)在CPU上对进程或者线程进行切换。

进程从用户态内核态的转变,需要通过系统调用来完成。系统调用的过程,会发生CPU上下文的切换

CPU 寄存器里原来用户态的指令位置,需要先保存起来。
接着,为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。最后才是跳转到内核态运行内核任务。


[软件体系结构/架构]零拷贝技术(Zero-copy)[转发]-小白菜博客

3.4 虚拟内存

现代操作系统使用虚拟内存,即虚拟地址取代物理地址,使用虚拟内存可以有2个好处:

  • 虚拟内存空间可以远远大于物理内存空间
  • 多个虚拟内存可以指向同一个物理地址

正是多个虚拟内存可以指向同一个物理地址,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,这样的话,就可以减少IO的数据拷贝次数啦,示意图如下


[软件体系结构/架构]零拷贝技术(Zero-copy)[转发]-小白菜博客

3.5 DMA技术

DMA,英文全称是Direct Memory Access,即直接内存访问

DMA本质上是一块主板上独立的芯片,允许外设设备和内存存储器之间直接进行IO数据传输,其过程不需要CPU的参与。

我们一起来看下IO流程,DMA帮忙做了什么事情.


[软件体系结构/架构]零拷贝技术(Zero-copy)[转发]-小白菜博客

  • 用户应用进程调用read函数,向操作系统发起IO调用,进入阻塞状态,等待数据返回。
  • CPU收到指令后,对DMA控制器发起指令调度。
  • DMA收到IO请求后,将请求发送给磁盘;
  • 磁盘将数据放入磁盘控制缓冲区,并通知DMA
  • DMA将数据从磁盘控制器缓冲区拷贝到内核缓冲区。
  • DMA向CPU发出数据读完的信号,把工作交换给CPU,由CPU负责将数据从内核缓冲区拷贝到用户缓冲区。
  • 用户应用进程由内核态切换回用户态,解除阻塞状态

可以发现,DMA做的事情很清晰啦,它主要就是帮忙CPU转发一下IO请求,以及拷贝数据。为什么需要它的?

主要就是效率,它帮忙CPU做事情,这时候,CPU就可以闲下来去做别的事情,提高了CPU的利用效率。
大白话解释就是,CPU老哥太忙太累啦,所以他找了个小弟(名叫DMA) ,替他完成一部分的拷贝工作,这样CPU老哥就能着手去做其他事情。

4 零拷贝实现的几种方式

零拷贝并不是没有拷贝数据,而是**减少【用户态】/【内核态】的切换次数、以及【CPU拷贝】的次数。

零拷贝实现有多种方式,分别是

  • mmap+write
  • sendfile
  • 带有DMA收集拷贝功能的sendfile

4.1 mmap + write实现的零拷贝

api

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  • addr:指定映射的虚拟内存地址
  • length:映射的长度
  • prot:映射内存的保护模式
  • flags:指定映射的类型
  • fd: 进行映射的文件句柄
  • offset: 文件偏移量

前面一小节,零拷贝相关的知识点回顾,我们介绍了虚拟内存,可以把内核空间用户空间虚拟地址映射到同一个物理地址,从而减少数据拷贝次数

mmap就是用了虚拟内存这个特点,它将内核中的读缓冲区与用户空间的缓冲区进行映射,所有的IO都在内核中完成。

process

mmap+write实现的零拷贝流程如下:


[软件体系结构/架构]零拷贝技术(Zero-copy)[转发]-小白菜博客

  • 用户进程通过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拷贝‘’并且用户进程内存是虚拟的,只是映射到内核的读缓冲区,可以节省一半的内存空间。

4.2 sendfile实现的零拷贝 [Linux 2.1]

api

sendfile是Linux2.1内核版本后引入的一个系统调用函数,API如下:

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
  • out_fd:为待写入内容的文件描述符,一个socket描述符。,
  • in_fd:为待读出内容的文件描述符,必须是真实的文件,不能是socket和管道。
  • offset:指定从读入文件的哪个位置开始读,如果为NULL,表示文件的默认起始位置。
  • count:指定在fdout和fdin之间传输的字节数。

sendfile表示在两个文件描述符之间传输数据,它是在操作系统内核中操作的,避免了数据从内核缓冲区和用户缓冲区之间的拷贝操作,因此可以使用它来实现零拷贝。

process

sendfile实现的零拷贝流程如下:


[软件体系结构/架构]零拷贝技术(Zero-copy)[转发]-小白菜博客

  • 用户进程发起sendfile系统调用,上下文(切换1)从用户态转向内核态
  • DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
  • CPU将读缓冲区中数据拷贝到socket缓冲区
  • DMA控制器,异步把数据从socket缓冲区拷贝到网卡,
  • 上下文(切换2)从内核态切换回用户态,sendfile调用返回。

可以发现:

  • sendfile实现的零拷贝,I/O发生了2次用户空间与内核空间的上下文切换,以及3次数据拷贝。
  • 其中,3次数据拷贝中,包括了2次DMA拷贝和1次CPU拷贝。

那能不能把CPU拷贝的次数减少到0次呢?有的,即带有DMA收集拷贝功能的sendfile!

4.3 sendfile+DMA scatter/gather实现的零拷贝 [Linux 2.4]

linux 2.4版本之后,对sendfile做了优化升级,引入SG-DMA技术,其实就是对DMA拷贝加入了scatter/gather操作,它可以直接从内核空间缓冲区中将数据读取到网卡。使用这个特点搞零拷贝,即还可以多省去一次CPU拷贝。

process

sendfile+DMA scatter/gather实现的零拷贝流程如下:


[软件体系结构/架构]零拷贝技术(Zero-copy)[转发]-小白菜博客

  • 用户进程发起sendfile系统调用,上下文(切换1)从用户态转向内核态
  • DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
  • CPU把内核缓冲区中的文件描述符信息(包括内核缓冲区的内存地址和偏移量)发送到socket缓冲区
  • DMA控制器根据文件描述符信息,直接把数据从内核缓冲区拷贝到网卡
  • 上下文(切换2)从内核态切换回用户态,sendfile调用返回。

可以发现,sendfile+DMA scatter/gather实现的零拷贝,I/O发生了2次用户空间与内核空间的上下文切换,以及2次数据拷贝。
其中:2次数据拷贝都是DMA拷贝
这就是真正的 零拷贝(Zero-copy) 技术,全程都没有通过CPU来搬运数据,所有的数据都是通过DMA来进行传输的。

5 java提供的零拷贝方式

Linux提供的零拷贝技术,Java也并不是全支持,仅支持2种(mmapsendfile)


[软件体系结构/架构]零拷贝技术(Zero-copy)[转发]-小白菜博客

> Java NIO概览与变迁
> + `NIO` = New IO
> + `NIO 1` : JSR 51
>>> JDK 1.4 引入
>>> + http://jcp.org/en/jsr/detail?id=051
> + `NIO 2` : JSR 203
>>> JDK 7
>>> + http://jcp.org/en/jsr/detail?id=203

  • Java NIO对mmap的支持
  • Java NIO对sendfile的支持

5.0 Channel

5.0.1 概念阐释

Java NIO 中的所有IO都是从 Channel(通道) 开始的:

  • 从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。
  • 从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。

5.0.2 Java 中 [NIO Channel] 通道 与 [Stream] 流 的区别

Java NIO Channel通道和Stream流非常相似,主要有以下几点区别:

  • 通道可以也可以,流一般来说是单向的(只能读或者写,所以之前我们用流进行IO操作的时候需要分别创建一个输入流和一个输出流)。
  • 通道可以异步读写
  • 通道总是基于缓冲区Buffer来读写

5.0.3 重要实现

Java NIO 中最重要的几个Channel的实现:

  • FileChannel: 用于文件的数据读写
  • DatagramChannel: 用于UDP的数据读写
  • SocketChannel: 用于TCP的数据读写,一般是客户端实现
  • ServerSocketChannel: 允许我们监听TCP链接请求,每个请求会创建会一个SocketChannel,一般是服务器实现

5.0.4 FileChannel

使用FileChannel读取数据到Buffer(缓冲区)以及利用Buffer(缓冲区)写入数据到FileChannel:

package filechannel;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelTxt {
    public static void main(String args[]) throws IOException {
        //1.创建一个RandomAccessFile(随机访问文件)对象,
        RandomAccessFile raf=new RandomAccessFile("D:\\niodata.txt", "rw");
        //通过RandomAccessFile对象的getChannel()方法。FileChannel是抽象类。
        FileChannel inChannel=raf.getChannel();
        //2.创建一个读数据缓冲区对象
        ByteBuffer buf=ByteBuffer.allocate(48);
        //3.从通道中读取数据
        int bytesRead = inChannel.read(buf);
        //创建一个写数据缓冲区对象
        ByteBuffer buf2=ByteBuffer.allocate(48);
        //写入数据
        buf2.put("filechannel test".getBytes());
        buf2.flip();
        inChannel.write(buf);
        while (bytesRead != -1) {

            System.out.println("Read " + bytesRead);
            //Buffer有两种模式,写模式和读模式。在写模式下调用flip()之后,Buffer从写模式变成读模式。
            buf.flip();
           //如果还有未读内容
            while (buf.hasRemaining()) {
                System.out.print((char) buf.get());
            }
            //清空缓存区
            buf.clear();
            bytesRead = inChannel.read(buf);
        }
        //关闭RandomAccessFile(随机访问文件)对象
        raf.close();
    }
}

通过上述实例代码,我们可以大概总结出FileChannel的一般使用规则:

    1. 开启FileChannel

使用之前,FileChannel必须被打开 ,但是你无法直接打开FileChannel(FileChannel是抽象类)。
需要通过 InputStreamOutputStreamRandomAccessFile 获取FileChannel

我们上面的例子是通过RandomAccessFile打开FileChannel的:

//1.创建一个RandomAccessFile(随机访问文件)对象,
RandomAccessFile raf=new RandomAccessFile("D:\\niodata.txt", "rw");

//通过RandomAccessFile对象的getChannel()方法。FileChannel是抽象类。
FileChannel inChannel=raf.getChannel();
    1. 从FileChannel读取数据/写入数据

从FileChannel中读取数据/写入数据之前首先要创建一个Buffer(缓冲区)对象,Buffer(缓冲区)对象的使用我们在上一篇文章中已经详细说明了,如果不了解的话可以看我的上一篇关于Buffer的文章。

使用FileChannel的read()方法读取数据:

//2.创建一个读数据缓冲区对象
ByteBuffer buf=ByteBuffer.allocate(48);
//3.从通道中读取数据
int bytesRead = inChannel.read(buf);

使用FileChannel的write()方法写入数据:

 //创建一个写数据缓冲区对象
ByteBuffer buf2=ByteBuffer.allocate(48);
//写入数据
buf2.put("filechannel test".getBytes());
buf2.flip();
inChannel.write(buf);
    1. 关闭FileChannel

完成使用后,FileChannel您必须关闭它。

channel.close(); 

5.0.5 SocketChannel (Client-End) & ServerSocketChannel (Server-End)

利用SocketChannel和ServerSocketChannel实现客户端与服务器端简单通信:

SocketChannel 用于创建基于tcp协议的客户端对象。

因为SocketChannel中不存在accept()方法。所以,它不能成为一个服务端程序。
通过 connect()方法 ,SocketChannel对象可以连接到其他tcp服务器程序。

  • 客户端 样例Code
package socketchannel;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class WebClient {
    public static void main(String[] args) throws IOException {
        //1.通过SocketChannel的open()方法创建一个SocketChannel对象
        SocketChannel socketChannel = SocketChannel.open();
        //2.连接到远程服务器(连接此通道的socket)
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 3333));
        // 3.创建写数据缓存区对象
        ByteBuffer writeBuffer = ByteBuffer.allocate(128);
        writeBuffer.put("hello WebServer this is from WebClient".getBytes());
        writeBuffer.flip();
        socketChannel.write(writeBuffer);
        //创建读数据缓存区对象
        ByteBuffer readBuffer = ByteBuffer.allocate(128);
        socketChannel.read(readBuffer);
        //String 字符串常量,不可变;StringBuffer 字符串变量(线程安全),可变;StringBuilder 字符串变量(非线程安全),可变
        StringBuilder stringBuffer=new StringBuilder();
        //4.将Buffer从写模式变为可读模式
        readBuffer.flip();
        while (readBuffer.hasRemaining()) {
            stringBuffer.append((char) readBuffer.get());
        }
        System.out.println("从服务端接收到的数据:"+stringBuffer);

        socketChannel.close();
    }
}

ServerSocketChannel 允许我们监听TCP链接请求
通过ServerSocketChannelImpl的 accept()方法 可以创建一个SocketChannel对象用户从客户端读/写数据。

  • 服务端 样例代码
package socketchannel;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class WebServer {
    public static void main(String args[]) throws IOException {
        try {
            //1.通过ServerSocketChannel 的open()方法创建一个ServerSocketChannel对象,open方法的作用:打开套接字通道
            ServerSocketChannel ssc = ServerSocketChannel.open();
            //2.通过ServerSocketChannel绑定ip地址和port(端口号)
            ssc.socket().bind(new InetSocketAddress("127.0.0.1", 3333));
            //通过ServerSocketChannelImpl的accept()方法创建一个SocketChannel对象用户从客户端读/写数据
            SocketChannel socketChannel = ssc.accept();
            //3.创建写数据的缓存区对象
            ByteBuffer writeBuffer = ByteBuffer.allocate(128);
            writeBuffer.put("hello WebClient this is from WebServer".getBytes());
            writeBuffer.flip();
            socketChannel.write(writeBuffer);
            //创建读数据的缓存区对象
            ByteBuffer readBuffer = ByteBuffer.allocate(128);
            //读取缓存区数据
            socketChannel.read(readBuffer);
            StringBuilder stringBuffer=new StringBuilder();
            //4.将Buffer从写模式变为可读模式
            readBuffer.flip();
            while (readBuffer.hasRemaining()) {
                stringBuffer.append((char) readBuffer.get());
            }
            System.out.println("从客户端接收到的数据:"+stringBuffer);
            socketChannel.close();
            ssc.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

【小结】通过上述实例代码,我们可以大概总结出SocketChannel和ServerSocketChannel的使用的一般使用规则:
考虑到篇幅问题,下面只给出大致步骤,不贴代码,可以结合上述实例理解。

  • 客户端
    1.通过SocketChannel连接到远程服务器
    2.创建读数据/写数据缓冲区对象来读取服务端数据或向服务端发送数据
    3.关闭SocketChannel

  • 服务端
    1.通过ServerSocketChannel 绑定ip地址和端口号
    2.通过ServerSocketChannelImpl的accept()方法创建一个SocketChannel对象用户从客户端读/写数据
    3.创建读数据/写数据缓冲区对象来读取客户端数据或向客户端发送数据

  1. 关闭SocketChannel和ServerSocketChannel

5.0.5 DatagramChannel

DataGramChannel,类似于java 网络编程的DatagramSocket类;
使用UDP进行网络传输, UDP是无连接,面向数据报文段的协议,对传输的数据不保证安全与完整 ;
和上面介绍的SocketChannel和ServerSocketChannel的使用方法类似,所以这里就简单介绍一下如何使用。

  1. 获取DataGramChannel
        //1.通过DatagramChannel的open()方法创建一个DatagramChannel对象
        DatagramChannel datagramChannel = DatagramChannel.open();
        //绑定一个port(端口)
        datagramChannel.bind(new InetSocketAddress(1234));

上面代码表示程序可以在1234端口接收数据报。

  1. 接收/发送消息

接收消息:

先创建一个缓存区对象,然后通过receive方法接收消息,这个方法返回一个SocketAddress对象,表示发送消息方的地址:

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);

发送消息:

由于UDP下,服务端和客户端通信并不需要建立连接,只需要知道对方地址即可发出消息,但是是否发送成功或者成功被接收到是没有保证的;发送消息通过send方法发出,改方法返回一个int值,表示成功发送的字节数:

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put("datagramchannel".getBytes());
buf.flip();
int send = channel.send(buffer, new InetSocketAddress("localhost",1234));

这个例子发送一串字符:“datagramchannel”到主机名为”localhost”服务器的端口1234上。

5.1 Scatter / Gather (本地矢量 I/O)

Channel 提供了一种被称为 Scatter/Gather 的新功能,也称为本地矢量 I/O
Scatter/Gather 是指在多个缓冲区上实现一个简单的 I/O 操作。
正确使用 Scatter / Gather可以明显提高性能。

大多数现代操作系统都支持本地矢量I/O(native vectored I/O)操作。
当您在一个通道上请求一个Scatter/Gather操作时,该请求会被翻译为适当的本地调用来直接填充或抽取缓冲区,减少或避免了缓冲区拷贝系统调用

Scatter/Gather应该使用直接的ByteBuffers以从本地I/O获取最大性能优势。

Scatter/Gather功能是通道(Channel)提供的 并不是Buffer

  • Scatter: 从一个Channel读取的信息分散到N个缓冲区中(Buufer).

  • Gather: 将N个Buffer里面内容按照顺序发送到一个Channel.

Scattering Reads

scattering read是把数据从单个Channel写入到多个buffer

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);

read()方法内部会负责把数据按顺序写进传入的buffer数组内。一个buffer写满后,接着写到下一个buffer中。
举个例子,假如通道中有200个字节数据,那么header会被写入128个字节数据,body会被写入72个字节数据;

注意:无论是scatter还是gather操作,都是按照buffer在数组中的顺序来依次读取或写入的;

Gathering Writes

gathering write把多个buffer的数据写入到同一个channel中

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

//write data into buffers

ByteBuffer[] bufferArray = { header, body };

channel.write(bufferArray);

write()方法内部会负责把数据按顺序写入到channel中。

注意:

  • 并不是所有数据都写入到通道,写入的数据要根据position和limit的值来判断,只有position和limit之间的数据才会被写入;
  • 举个例子,假如以上header缓冲区中有128个字节数据,但此时position=0,limit=58;那么只有下标索引为0-57的数据才会被写入到通道中。

通道之间的数据传输

在Java NIO中如果一个channel是FileChannel类型的,那么他可以直接把数据传输到另一个channel。

  • transferFrom() :transferFrom方法把数据从通道源传输到FileChannel
  • transferTo() :transferTo方法把FileChannel数据传输到另一个channel

5.2 Java NIO对mmap的支持

5.2.1 MappedByteBuffer --> mmap(...)

Java NIO有一个MappedByteBuffer的类,可以用来实现内存映射。它的底层是调用了Linux内核的mmap的API。

JavaNIO中 的Channel (通道) 有可能是内核缓冲区,也有可能是网络缓冲区
而Buffer就相当于操作系统中的用户缓冲区。

mmap的小demo如下:

public class MmapTest {

    public static void main(String[] args) {
        try {
            FileChannel readChannel = FileChannel.open(Paths.get("./jay.txt"), StandardOpenOption.READ);
            MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, 1024 * 1024 * 40);
            FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
            //数据传输
            writeChannel.write(data);
            readChannel.close();
            writeChannel.close();
        }catch (Exception e){
            System.out.println(e.getMessage());
        }
    }
}

5.3 Java NIO对sendfile的支持

5.3.1 FileChannel --> sendfile(...)

FileChannel的transferTo()/transferFrom(),底层就是sendfile() 系统调用函数。Kafka 这个开源项目就用到它,平时面试的时候,回答面试官为什么这么快,就可以提到零拷贝sendfile这个点。

@Override
public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
   return fileChannel.transferTo(position, count, socketChannel);
}

X 参考文献