零拷贝技术(Zero-Copy)是一个大家耳熟能详的技术名词了,它关键用于优化 IO(Input & Output)的传输性能。
那么疑问来了,为什么零拷贝技术能优化 IO 性能?
一、零拷贝技术和性能
在传统的 IO 操作中,当咱们须要读取并传输数据时,咱们须要在用户态(用户空间)和内核态(内核空间)中启动数据拷贝,它的口头流程如下:
从上述流程咱们可以看出,在传统的 IO 操作中,咱们是须要 4 次拷贝和 4 次高低文切换(用户态和内核态的切换)的。
而每次数据拷贝和高低文切换都有时期老本,会让程序的口头时期变成,所以零拷贝技术的出现就是为了缩小数据的拷贝次数以及高低文的切换次数的。
1.1 什么是用户态和内核态?
操作系统有用户态和内核态之分,这是由于计算机体系结构中的操作系统设计了两个不同的口头环境,以提供不同的配置和特权级别。
1.2 什么是DMA?
DMA(Direct Memory Access,间接内存访问)技术,绕过 CPU,间接在内存和外设之间启动数据传输。这样可以缩小 CPU 的介入,提高数据传输的效率。
二、Linux零拷贝技术
Linux 下成功零拷贝的关键成功技术是 MMap、sendFile,它们的详细引见如下。
MMap(Memory Map)是 Linux 操作系统中提供的一种将文件映射到进程地址空间的一种机制,经过 MMap 进程可以像访问内存一样访问文件,而无需显式的复制操作。
经常使用 MMap 可以把 IO 口头流程优化成以下口头步骤:
传统的 IO 须要四次拷贝和四次高低文(用户态和内核态)切换,而 MMap 只要要三次拷贝和四次高低文切换,从而能够优化程序全体的口头效率,并且节俭了程序的内存空间。
2.2 senFile 方法
在 Linux 操作系统中 sendFile() 是一个系统调用函数,用于高效地将文件数据从内核空间间接传输到网络套接字(Socket)上,从而成功零拷贝技术。这个函数的关键目的是缩小 CPU 高低文切换以及内存复制操作,提高文件传输性能。
经常使用 sendFile() 可以把 IO 口头流程优化成以下口头步骤:
三、Netty零拷贝技术
Netty 中的零拷贝和传统 Linux 的零拷贝技术的成功不太一样,Netty 中的零拷贝技术关键是经过优化用户态的操作来优化 IO 的口头速度,从而成功零拷贝的。
Netty 中的零拷贝技术关键有以下 5 种成功:
它们的详细成功如下。
3.1 经常使用堆外内存
反常状况下,JVM 须要将数据从 JVM 堆内存拷贝到堆外内存启动业务口头的,这是由于:
而 Netty 在启动 I/O 操作时都是经常使用的堆外内存,可以防止数据从 JVM 堆内存到堆外内存的拷贝。
3.2 经常使用CompositeByteBuf兼并对象
CompositeByteBuf 可以了解为一个虚构的 Buffer 对象,它是由多个 ByteBuf 组合而成,然而在 CompositeByteBuf 外部保留着每个 ByteBuf 的援用相关,从逻辑上导致一个全体。经常使用 CompositeByteBuf 咱们可以兼并两个 ByteBuf 对象,从而防止两个对象兼并时须要两次 CPU 拷贝操作的疑问,在没有经常使用 CompositeByteBuf 时,咱们的操作是这样的:
ByteBuf httpBuf = Unpooled.buffer(header.readableBytes() + body.readableBytes());httpBuf.writeBytes(header);httpBuf.writeBytes(body);
而成功 header 和 body 这两个 ByteBuf 的兼并,须要先初始化一个新的 httpBuf,而后再将 header 和 body 区分拷贝到新的 httpBuf。兼并环节中触及两次 CPU 拷贝,这十分糜费性能,所以咱们就可以经常使用 CompositeByteBuf 了,它的经常使用如下:
CompositeByteBuf httpBuf = Unpooled.compositeBuffer();httpBuf.addComponents(true, header, body);
CompositeByteBuf 经过调用 addComponents() 方法来增加多个 ByteBuf,然而底层的 byte 数组是复用的,不会出现内存拷贝。
3.3 经过Unpooled.wrappedBuffer兼并数据
Unpooled.wrappedBuffer 的操作相似,经常使用它可以将不同的数据源的一个或许多个数据包装成一个大的 ByteBuf 对象,其中数据源的类型包含 byte[]、ByteBuf、ByteBuffer。包装的环节中不会出现数据拷贝操作,包装后生成的 ByteBuf 对象和原始 ByteBuf 对象是共享底层的 byte 数组。
3.4 经常使用 ByteBuf.slice 共享对象
ByteBuf.slice 和 Unpooled.wrappedBuffer 的逻辑正好同样,ByteBuf.slice 是将一个 ByteBuf 对象切分红多个共享同一个底层存储的 ByteBuf 对象,从而防止对象宰割时的数据拷贝,它的经常使用如下:
ByteBuf httpBuf = ...ByteBuf header = httpBuf.slice(0, 6);ByteBuf body = httpBuf.slice(6, 4);
3.5 经常使用 FileRegion 成功文件零拷贝
FileRegion 底层封装了 FileChannel#transferTo() 方法,可以将文件缓冲区的数据间接传输到指标 Channel,防止内核缓冲区和用户态缓冲区之间的数据拷贝,这属于操作系统级别的零拷贝。
以下是 FileRegion 的自动成功类 DefaultFileRegion 的经常使用案例:
@Overridepublic void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {RandomAccessFile raf = null;long length = -1;try {raf = new RandomAccessFile(msg, "r");length = raf.length();} catch (Exception e) {ctx.writeAndFlush("ERR: " + e.getClass().getSimpleName() + ": " + e.getMessage() + '\n');return;} finally {if (length < 0 && raf != null) {raf.close();}}ctx.write("OK: " + raf.length() + '\n');if (ctx.pipeline().get(SslHandler.class) == null) {// SSL not enabled - can use zero-copy file transfer.ctx.write(new DefaultFileRegion(raf.getChannel(), 0, length));} else {// SSL enabled - cannot use zero-copy file transfer.ctx.write(new ChunkedFile(raf));}ctx.writeAndFlush("\n");}
从上述代码可以看出,可以经过 DefaultFileRegion 将文件内容间接写入到 NioSocketChannel 中,从而防止了内核缓冲区和用户态缓冲区之间的数据拷贝。