「我正在参加会员专属活动-源码共读第一期,点击参加」

前语

零仿制是老生常谈的话题了, 不管是Kafka仍是Netty都用到了零仿制的常识, 本篇侧重讲解了什么是零仿制, 一起在JavaNetty平分别是怎样完成零仿制的

往期文章:

  • Netty源码剖析(一) backlog 参数 – ()
  • Netty服务端初始化详解 – ()
  • Netty服务端发动流程剖析 – ()
  • Netty之第一次 TCP 衔接时产生了什么 – ()
  • Netty之服务发动且注册成功之后 – ()
  • Netty之服务端channel的初始化 – ()
  • Netty「源码阅览」之 EventLoop 简单介绍到源码剖析 – ()
  • Netty「源码阅览」之怎样处理 Java 的 epoll 空轮询 bug – ()

什么是零仿制

零仿制是指计算机在执行IO操作的时分, CPU不需要将数据从一个存储区仿制到另一个存储区, 从而减少上下文切换以及 CPU 仿制的时间, 这是一种IO操作优化技能

零仿制不是没有仿制数据, 而是减少用户态, 内核态的切换次数 和 CPU仿制次数, 目前完成零仿制的首要三种办法分别是:

  • mmap + write
  • sendfile
  • 带有DMA搜集仿制功能的 sendfile

mmap

虚拟内存把内核空间和用户空间的虚拟地址映射到同一个物理地址, 从而减少数据仿制次数, mmap技能便是利用了虚拟内存的这个特色, 它将内核中的读缓冲区与用户空间的缓冲区进行映射, 一切的IO操作都在内核中完结

sendfile

sendfileLinux 2.1 版别之后内核引进的一个体系调用函数

sendfile表明在两个文件描述符之间传输数据, 他是在操作体系内核中完结的, 避免了数据从内核缓冲区和用户缓冲区之间的仿制操作, 因而能够用其来完成零仿制

Linux 2.4版别之后, 对sendfile进行了晋级, 引进了SG-DMA技能, 能够直接从缓冲区中将数据读取到网卡, 这样的话能够省去CPU仿制

Java 完成的零仿制

mmap

Java NIO有一个ByteBuffer的子类MappedByteBuffer, 这个类采用direct buffer也便是内存映射的办法读写文件内容. 这种办法直接调用体系底层的缓存, 没有JVM和体系之间的仿制操作, 首要用户操作大文件

sendfile

FileChanneltransferTo()办法或许transferFrom()办法,底层便是sendfile() 体系调用函数。 完成了数据直接从内核的读缓冲区传输到套接字缓冲区, 避免了用户态与内核态之间的数据仿制

Kafka便是使用到它

Netty 的零仿制

Netty的哦零仿制首要体现在以下几个方面

  • slice
  • duplicate
  • CompositeByteBuf
  • ….

咱们首要讲一下slice, 其他的下次必定

log 工具类

import io.netty.buffer.ByteBuf;
import static io.netty.buffer.ByteBufUtil.appendPrettyHexDump;
import static io.netty.util.internal.StringUtil.NEWLINE;
public class ByteBufUtil {
    // 打印
    public static void log(ByteBuf buf){
        final int length = buf.readableBytes();
        int rows = length / 16 + (length % 15 == 0 ? 0 : 1) + 4;
        StringBuilder str = new StringBuilder(rows * 80 * 2)
                .append("read index:").append(buf.readerIndex())
                .append(" write index: ").append(buf.writerIndex())
                .append(" capacity:").append(buf.capacity())
                .append(NEWLINE);
        appendPrettyHexDump(str, buf);
        System.out.println(str.toString());
    }
}

slice

对原始的ByteBuf进行切片成多个ByteBuf, 切片后的ByteBuf并没有产生内存仿制, 仍是使用原始的ByteBuf内存, 可是切片后的ByteBuf各自有独立的read, write指针

留意:

  • slice不允许更改切片的容量, 切片时设置的长度是多少便是多少, 不允许扩容
  • 当咱们开释原始ByteBuf内存之后, 切片后的ByteBuf就不能再访问了

测试:

  • 首要创建一个ByteBuf, 然后对其进行切片
  • 更改某一个切片查看原始ByteBuf是否更改
  • 原始数据跟着更改了阐明内存地址没有产生改动

测试类

public static void main(String[] args) {
    // 创建 ByteBuf
    ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(10);
    // 向 byteBuf 缓冲区写入数据
    StringBuilder str = new StringBuilder();
    for (int i = 0; i < 5; i++) {
        str.append("nx");
    }
    byteBuf.writeBytes(str.toString().getBytes());
    // 打印当时 byteBug
    ByteBufUtil.log(byteBuf);
    // 切片的过程中并没有产生数据仿制
    final ByteBuf slice = byteBuf.slice(0, 5);
    final ByteBuf slice1 = byteBuf.slice(5, 5);
    // 打印第一个切片
    ByteBufUtil.log(slice);
    // 打印第二个切片
    ByteBufUtil.log(byteBuf);
    slice.setByte(0, 'a');
    System.out.println("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
    // 打印第一个切片
    ByteBufUtil.log(slice);
    // 打印原始数组
    ByteBufUtil.log(byteBuf);
}

打印成果如下

什么是零拷贝, 从 Java 到 Netty




本文内容到此结束了

如有收成欢迎点赞收藏关注✔️,您的鼓舞是我最大的动力。

如有过错❌疑问欢迎各位大佬指出。

我是 宁轩 , 咱们下次再会