目录

1. 关系梳理

2. 我们先看虚拟内存到底解决什么问题?

3. Linux IO体系重点解决什么问题?

4. 零拷贝重点解决什么问题?

4.1 为什么会谈零拷贝?

4.2 传统IO方式有什么问题?

4.2.1. 传统读操作

4.2.2. 传统写操作

4.3. 零拷贝方式如何实现解决这些问题

4.3.1. 用户态直接I/O

4.3.2 减少数据拷贝次数的4中零拷贝实现方式

4.3.2.2 sendfile

4.3.2.3 sendfile + DMA gather copy

4.3.2.4 splice

4.3.3. 写时复制

4.4. 缓冲区共享

4.5. Linux零拷贝对比

5. 零拷贝应用

5.1 Java NIO 中的零拷贝

5.1.1 MappedByteBuffer

5.1.2 DirectByteBuffer

5.1 Netty 中的零拷贝实现

5.1.1 内存使用优化(其实不算是零拷贝)

5.1.2 内存缓存池使用了DirectByteBuffer


参考文章:

https://zhuanlan.zhihu.com/p/83398714《深入剖析Linux IO原理和几种零拷贝机制的实现》

https://zhuanlan.zhihu.com/p/308054212《Linux I/O 原理和 Zero-copy 技术全面揭秘》

https://www.cnblogs.com/rickiyang/p/13265043.html《零拷贝(Zero-copy) 浅析及其应用》

https://www.cnblogs.com/yougewe/p/14353111.html?share_token=6de5b86b-0d91-4eda-9760-b57b05c7fe4f&tt_from=copy_link&utm_source=copy_link&utm_medium=toutiao_android&utm_campaign=client_share《Netty(三): 直接内存原理及应用》

1. 关系梳理

经常看到网上的文章讲IO体系和零拷贝的时候前面大篇幅在讲虚拟存储和虚拟内存的事情,所以看这些文章感觉有点重点抓不住,要点不突出,关系连不起来,今天我根据自己的理解,重新梳理下这三者的关系:

1.1 讲IO和零拷贝的时候讲到虚拟内存是因为涉及到用户进程在用户态和内核态的转换,离开进程谈虚拟内存就没任何意义。

1.2 IO体系和零拷贝是有有关系,但是跟虚拟内存没有直接关系。

2. 我们先看虚拟内存到底解决什么问题?

  1. 虚拟内存是计算机系统内存管理的一种技术。 它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间)。比如用户进程A要读一个文件,但是文件很大(比如一个文件100G大于物理内存大小16G),但是100G肯定不可能全部放入16G的物理内存,怎么办?所以诞生了虚拟内存这个概念,虚拟内存是相对物理内存而言的,虚拟内存让用户进程感觉自己真的有100G的内存可以用,但是实际上只有16G,所以虚拟内存做了一个事:用户进程分页,物理内存也分页,把100G跟16G的空间分页相互做个映射,操作系统管理着这个进程的页和内存页的关系。然后进行要执行或者读取这个文件的时候(要读到内存里面),通过分页的映射关系,就能够做到把这个文件里面所有数据都读入内存。

  2. 虚拟内存地址和用户进程紧密相关,虚拟内存是属于每个进程独有的,操作系统帮助管理每个进程的虚拟内存地址跟某部分物理内存地址的映射关系,所以离开进程谈虚拟内存没有任何意义,所以跟IO和零拷贝也没有直接关系。上面第一点讲了16G内存全部给一个进程用,但是实际上一个操作系统会有多进程运行,所以一个进程的虚拟内存会映射到某一小块物理内存,到底怎么映射,怎么更好的在磁盘和内存之间进行页面的换入换出,这是操作系统要去做的事情。

3. Linux IO体系重点解决什么问题?

  1. linux IO体系重点说操作系统怎么玩输入输出的,进程间如何通过输入输出能力进行数据交换,本机进程和外部设备(比如磁盘和网卡)之间如何进行数据读写。

  2. 操作系统实现IO的手段只有两种:DMA拷贝和CPU拷贝。DMA专门负责外设和进程的数据拷贝;CPU除了负责数据拷贝(比如内核数据到进程缓冲区的数据拷贝,后面会细讲),还负责各种进程调度,中断监听,上下文切换后的场景保存等等,很多事情。所以CPU资源需要非常珍惜(让cpu去频繁做IO的事情一定不是好注意,因为IO这个事相对其他调度等这些事情来说是长耗时的)。

  3. 当然什么情况下触发IO事件,场景会比较多,比如:传统的通过 write() 和 read() 两个系统调用,通过 read() 函数读取文件到到缓存区中,然后通过 write()方法把缓存中的数据输出到网络端口等等。

4. 零拷贝重点解决什么问题?

(后面的内容大部门拷贝自网上,可以在附录和引用里面查看,看了几篇文章,我摘取了最优的内容)

4.1 为什么会谈零拷贝?

这是专门针对传统的IO方式(传统的通过 write() 和 read() 两个系统调用实现的IO)性能和效率低下问题而提出的几种新的IO方案:

  • 用户态直接 I/O:应用程序可以直接访问硬件存储,操作系统内核只是辅助数据传输。这种方式依旧存在用户空间和内核空间的上下文切换,硬件上的数据直接拷贝至了用户空间,不经过内核空间。因此,直接 I/O 不存在内核空间缓冲区和用户空间缓冲区之间的数据拷贝。
  • 减少数据拷贝次数:在数据传输过程中,避免数据在用户空间缓冲区和系统内核空间缓冲区之间的CPU拷贝,以及数据在系统内核空间内的CPU拷贝,这也是当前主流零拷贝技术的实现思路。
  • 写时复制技术:写时复制指的是当多个进程共享同一块数据时,如果其中一个进程需要对这份数据进行修改,那么将其拷贝到自己的进程地址空间中,如果只是数据读取操作则不需要进行拷贝操作。

4.2 传统IO方式有什么问题?

为了更好的理解零拷贝解决的问题,我们首先了解一下传统 I/O 方式存在的问题。在 Linux 系统中,传统的访问方式是通过 write() 和 read() 两个系统调用实现的,通过 read() 函数读取文件到到缓存区中,然后通过 write() 方法把缓存中的数据输出到网络端口,伪代码如下:

read(file_fd, tmp_buf, len);
write(socket_fd, tmp_buf, len);

下图分别对应传统 I/O 操作的数据读写流程,整个过程涉及 2 次 CPU 拷贝、2 次 DMA 拷贝总共 4 次拷贝,以及 4 次上下文切换,下面简单地阐述一下相关的概念。

  • 上下文切换:当用户程序向内核发起系统调用时,CPU 将用户进程从用户态切换到内核态;当系统调用返回时,CPU 将用户进程从内核态切换回用户态。
  • CPU拷贝:由 CPU 直接处理数据的传送,数据拷贝时会一直占用 CPU 的资源。
  • DMA拷贝:由 CPU 向DMA磁盘控制器下达指令,让 DMA 控制器来处理数据的传送,数据传送完毕再把信息反馈给 CPU,从而减轻了 CPU 资源的占有率。

4.2.1. 传统读操作

当应用程序执行 read 系统调用读取一块数据的时候,如果这块数据已经存在于用户进程的页内存中,就直接从内存中读取数据;如果数据不存在,则先将数据从磁盘加载数据到内核空间的读缓存(read buffer)中,再从读缓存拷贝到用户进程的页内存中。

read(file_fd, tmp_buf, len);

基于传统的 I/O 读取方式,read 系统调用会触发 2 次上下文切换,1 次 DMA 拷贝和 1 次 CPU 拷贝,发起数据读取的流程如下:

  1. 用户进程通过 read() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
  2. CPU利用DMA控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
  3. CPU将读缓冲区(read buffer)中的数据拷贝到用户空间(user space)的用户缓冲区(user buffer)。
  4. 上下文从内核态(kernel space)切换回用户态(user space),read 调用执行返回。

4.2.2. 传统写操作

当应用程序准备好数据,执行 write 系统调用发送网络数据时,先将数据从用户空间的页缓存拷贝到内核空间的网络缓冲区(socket buffer)中,然后再将写缓存中的数据拷贝到网卡设备完成数据发送。

write(socket_fd, tmp_buf, len);

基于传统的 I/O 写入方式,write() 系统调用会触发 2 次上下文切换,1 次 CPU 拷贝和 1 次 DMA 拷贝,用户程序发送网络数据的流程如下:

  1. 用户进程通过 write() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
  2. CPU 将用户缓冲区(user buffer)中的数据拷贝到内核空间(kernel space)的网络缓冲区(socket buffer)。
  3. CPU 利用 DMA 控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。
  4. 上下文从内核态(kernel space)切换回用户态(user space),write 系统调用执行返回。

4.3. 零拷贝方式如何实现解决这些问题

在 Linux 中零拷贝技术主要有 3 个实现思路:用户态直接 I/O、减少数据拷贝次数以及写时复制技术。

  • 用户态直接 I/O:应用程序可以直接访问硬件存储,操作系统内核只是辅助数据传输。这种方式依旧存在用户空间和内核空间的上下文切换,硬件上的数据直接拷贝至了用户空间,不经过内核空间。因此,直接 I/O 不存在内核空间缓冲区和用户空间缓冲区之间的数据拷贝。
  • 减少数据拷贝次数:在数据传输过程中,避免数据在用户空间缓冲区和系统内核空间缓冲区之间的CPU拷贝,以及数据在系统内核空间内的CPU拷贝,这也是当前主流零拷贝技术的实现思路。
  • 写时复制技术:写时复制指的是当多个进程共享同一块数据时,如果其中一个进程需要对这份数据进行修改,那么将其拷贝到自己的进程地址空间中,如果只是数据读取操作则不需要进行拷贝操作。

4.3.1. 用户态直接I/O

用户态直接 I/O 使得应用进程或运行在用户态(user space)下的库函数直接访问硬件设备,数据直接跨过内核进行传输,内核在数据传输过程除了进行必要的虚拟存储配置工作之外,不参与任何其他工作,这种方式能够直接绕过内核,极大提高了性能。

用户态直接 I/O 只能适用于不需要内核缓冲区处理的应用程序,这些应用程序通常在进程地址空间有自己的数据缓存机制,称为自缓存应用程序,如数据库管理系统就是一个代表。其次,这种零拷贝机制会直接操作磁盘 I/O,由于 CPU 和磁盘 I/O 之间的执行时间差距,会造成大量资源的浪费,解决方案是配合异步 I/O 使用。

4.3.2 减少数据拷贝次数的4中零拷贝实现方式

4.3.2.1 mmap + write

一种零拷贝方式是使用 mmap + write 代替原来的 read + write 方式,减少了 1 次 CPU 拷贝操作。mmap 是 Linux 提供的一种内存映射文件方法,即将一个进程的地址空间中的一段虚拟地址映射到磁盘文件地址,mmap + write 的伪代码如下:

tmp_buf = mmap(file_fd, len);
write(socket_fd, tmp_buf, len);

使用 mmap 的目的是将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射,从而实现内核缓冲区与应用程序内存的共享,省去了将数据从内核读缓冲区(read buffer)拷贝到用户缓冲区(user buffer)的过程,然而内核读缓冲区(read buffer)仍需将数据到内核写缓冲区(socket buffer),大致的流程如下图所示:

基于 mmap + write 系统调用的零拷贝方式,整个拷贝过程会发生 4 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝,用户程序读写数据的流程如下:

  1. 用户进程通过 mmap() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
  2. 将用户进程的内核空间的读缓冲区(read buffer)与用户空间的缓存区(user buffer)进行内存地址映射。
  3. CPU利用DMA控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
  4. 上下文从内核态(kernel space)切换回用户态(user space),mmap 系统调用执行返回。
  5. 用户进程通过 write() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
  6. CPU将读缓冲区(read buffer)中的数据拷贝到的网络缓冲区(socket buffer)。
  7. CPU利用DMA控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。
  8. 上下文从内核态(kernel space)切换回用户态(user space),write 系统调用执行返回。

mmap 主要的用处是提高 I/O 性能,特别是针对大文件。对于小文件,内存映射文件反而会导致碎片空间的浪费,因为内存映射总是要对齐页边界,最小单位是 4 KB,一个 5 KB 的文件将会映射占用 8 KB 内存,也就会浪费 3 KB 内存。

mmap 的拷贝虽然减少了 1 次拷贝,提升了效率,但也存在一些隐藏的问题。当 mmap 一个文件时,如果这个文件被另一个进程所截获,那么 write 系统调用会因为访问非法地址被 SIGBUS 信号终止,SIGBUS 默认会杀死进程并产生一个 coredump,服务器可能因此被终止。

4.3.2.2 sendfile

sendfile 系统调用在 Linux 内核版本 2.1 中被引入,目的是简化通过网络在两个通道之间进行的数据传输过程。sendfile 系统调用的引入,不仅减少了 CPU 拷贝的次数,还减少了上下文切换的次数,它的伪代码如下:

sendfile(socket_fd, file_fd, len);

通过 sendfile 系统调用,数据可以直接在内核空间内部进行 I/O 传输,从而省去了数据在用户空间和内核空间之间的来回拷贝。与 mmap 内存映射方式不同的是, sendfile 调用中 I/O 数据对用户空间是完全不可见的。也就是说,这是一次完全意义上的数据传输过程。

基于 sendfile 系统调用的零拷贝方式,整个拷贝过程会发生 2 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝,用户程序读写数据的流程如下:

  1. 用户进程通过 sendfile() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
  2. CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
  3. CPU 将读缓冲区(read buffer)中的数据拷贝到的网络缓冲区(socket buffer)。
  4. CPU 利用 DMA 控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。
  5. 上下文从内核态(kernel space)切换回用户态(user space),sendfile 系统调用执行返回。

相比较于 mmap 内存映射的方式,sendfile 少了 2 次上下文切换,但是仍然有 1 次 CPU 拷贝操作。sendfile 存在的问题是用户程序不能对数据进行修改,而只是单纯地完成了一次数据传输过程。

4.3.2.3 sendfile + DMA gather copy

Linux 2.4 版本的内核对 sendfile 系统调用进行修改,为 DMA 拷贝引入了 gather 操作。它将内核空间(kernel space)的读缓冲区(read buffer)中对应的数据描述信息(内存地址、地址偏移量)记录到相应的网络缓冲区( socket buffer)中,由 DMA 根据内存地址、地址偏移量将数据批量地从读缓冲区(read buffer)拷贝到网卡设备中,这样就省去了内核空间中仅剩的 1 次 CPU 拷贝操作,sendfile 的伪代码如下:

sendfile(socket_fd, file_fd, len);

在硬件的支持下,sendfile 拷贝方式不再从内核缓冲区的数据拷贝到 socket 缓冲区,取而代之的仅仅是缓冲区文件描述符和数据长度的拷贝,这样 DMA 引擎直接利用 gather 操作将页缓存中数据打包发送到网络中即可,本质就是和虚拟内存映射的思路类似。

基于 sendfile + DMA gather copy 系统调用的零拷贝方式,整个拷贝过程会发生 2 次上下文切换、0 次 CPU 拷贝以及 2 次 DMA 拷贝,用户程序读写数据的流程如下:

  1. 用户进程通过 sendfile() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
  2. CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
  3. CPU 把读缓冲区(read buffer)的文件描述符(file descriptor)和数据长度拷贝到网络缓冲区(socket buffer)。
  4. 基于已拷贝的文件描述符(file descriptor)和数据长度,CPU 利用 DMA 控制器的 gather/scatter 操作直接批量地将数据从内核的读缓冲区(read buffer)拷贝到网卡进行数据传输。
  5. 上下文从内核态(kernel space)切换回用户态(user space),sendfile 系统调用执行返回。

sendfile + DMA gather copy 拷贝方式同样存在用户程序不能对数据进行修改的问题,而且本身需要硬件的支持,它只适用于将数据从文件拷贝到 socket 套接字上的传输过程。

4.3.2.4 splice

sendfile 只适用于将数据从文件拷贝到 socket 套接字上,同时需要硬件的支持,这也限定了它的使用范围。Linux 在 2.6.17 版本引入 splice 系统调用,不仅不需要硬件支持,还实现了两个文件描述符之间的数据零拷贝。splice 的伪代码如下:

splice(fd_in, off_in, fd_out, off_out, len, flags);

splice 系统调用可以在内核空间的读缓冲区(read buffer)和网络缓冲区(socket buffer)之间建立管道(pipeline),从而避免了两者之间的 CPU 拷贝操作。

基于 splice 系统调用的零拷贝方式,整个拷贝过程会发生 2 次上下文切换,0 次 CPU 拷贝以及 2 次 DMA 拷贝,用户程序读写数据的流程如下:

  1. 用户进程通过 splice() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
  2. CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
  3. CPU 在内核空间的读缓冲区(read buffer)和网络缓冲区(socket buffer)之间建立管道(pipeline)。
  4. CPU 利用 DMA 控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。
  5. 上下文从内核态(kernel space)切换回用户态(user space),splice 系统调用执行返回。

splice 拷贝方式也同样存在用户程序不能对数据进行修改的问题。除此之外,它使用了 Linux 的管道缓冲机制,可以用于任意两个文件描述符中传输数据,但是它的两个文件描述符参数中有一个必须是管道设备。

4.3.3. 写时复制

在某些情况下,内核缓冲区可能被多个进程所共享,如果某个进程想要这个共享区进行 write 操作,由于 write 不提供任何的锁操作,那么就会对共享区中的数据造成破坏,写时复制的引入就是 Linux 用来保护数据的。

写时复制指的是当多个进程共享同一块数据时,如果其中一个进程需要对这份数据进行修改,那么就需要将其拷贝到自己的进程地址空间中。这样做并不影响其他进程对这块数据的操作,每个进程要修改的时候才会进行拷贝,所以叫写时拷贝。这种方法在某种程度上能够降低系统开销,如果某个进程永远不会对所访问的数据进行更改,那么也就永远不需要拷贝。

4.4. 缓冲区共享

缓冲区共享方式完全改写了传统的 I/O 操作,因为传统 I/O 接口都是基于数据拷贝进行的,要避免拷贝就得去掉原先的那套接口并重新改写,所以这种方法是比较全面的零拷贝技术,目前比较成熟的一个方案是在 Solaris 上实现的 fbuf(Fast Buffer,快速缓冲区)。

fbuf 的思想是每个进程都维护着一个缓冲区池,这个缓冲区池能被同时映射到用户空间(user space)和内核态(kernel space),内核和用户共享这个缓冲区池,这样就避免了一系列的拷贝操作。

缓冲区共享的难度在于管理共享缓冲区池需要应用程序、网络软件以及设备驱动程序之间的紧密合作,而且如何改写 API 目前还处于试验阶段并不成熟。

4.5. Linux零拷贝对比

无论是传统 I/O 拷贝方式还是引入零拷贝的方式,2 次 DMA Copy 是都少不了的,因为两次 DMA 都是依赖硬件完成的。下面从 CPU 拷贝次数、DMA 拷贝次数以及系统调用几个方面总结一下上述几种 I/O 拷贝方式的差别。

5. 零拷贝应用

5.1 Java NIO 中的零拷贝

在 Java NIO 中的通道(Channel)就相当于操作系统的内核空间(kernel space)的缓冲区,而缓冲区(Buffer)对应的相当于操作系统的用户空间(user space)中的用户缓冲区(user buffer)。

  • 通道(Channel)是全双工的(双向传输),它既可能是读缓冲区(read buffer),也可能是网络缓冲区(socket buffer)。
  • 缓冲区(Buffer)分为堆内存(HeapBuffer)和堆外内存(DirectBuffer),这是通过 malloc() 分配出来的用户态内存。

堆外内存(DirectBuffer)在使用后需要应用程序手动回收,而堆内存(HeapBuffer)的数据在 GC 时可能会被自动回收。因此,在使用 HeapBuffer 读写数据时,为了避免缓冲区数据因为 GC 而丢失,NIO 会先把 HeapBuffer 内部的数据拷贝到一个临时的 DirectBuffer 中的本地内存(native memory),这个拷贝涉及到 sun.misc.Unsafe.copyMemory() 的调用,背后的实现原理与 memcpy() 类似。 最后,将临时生成的 DirectBuffer 内部的数据的内存地址传给 I/O 调用函数,这样就避免了再去访问 Java 对象处理 I/O 读写。

5.1.1 MappedByteBuffer

MappedByteBuffer 是 NIO 基于内存映射(mmap)这种零拷贝方式的提供的一种实现,它继承自 ByteBuffer。FileChannel 定义了一个 map() 方法,它可以把一个文件从 position 位置开始的 size 大小的区域映射为内存映像文件。抽象方法 map() 方法在 FileChannel 中的定义如下:

public abstract MappedByteBuffer map(MapMode mode, long position, long size)throws IOException;
  • mode:限定内存映射区域(MappedByteBuffer)对内存映像文件的访问模式,包括只可读(READ_ONLY)、可读可写(READ_WRITE)和写时拷贝(PRIVATE)三种模式。
  • position:文件映射的起始地址,对应内存映射区域(MappedByteBuffer)的首地址。
  • size:文件映射的字节长度,从 position 往后的字节数,对应内存映射区域(MappedByteBuffer)的大小。

MappedByteBuffer 相比 ByteBuffer 新增了 fore()、load() 和 isLoad() 三个重要的方法:

  • fore():对于处于 READ_WRITE 模式下的缓冲区,把对缓冲区内容的修改强制刷新到本地文件。
  • load():将缓冲区的内容载入物理内存中,并返回这个缓冲区的引用。
  • isLoaded():如果缓冲区的内容在物理内存中,则返回 true,否则返回 false。

下面给出一个利用 MappedByteBuffer 对文件进行读写的使用示例:

private final static String CONTENT = "Zero copy implemented by MappedByteBuffer";
private final static String FILE_NAME = "/mmap.txt";
private final static String CHARSET = "UTF-8";
  • 写文件数据:打开文件通道 fileChannel 并提供读权限、写权限和数据清空权限,通过 fileChannel 映射到一个可写的内存缓冲区 mappedByteBuffer,将目标数据写入 mappedByteBuffer,通过 force() 方法把缓冲区更改的内容强制写入本地文件。
@Test
public void writeToFileByMappedByteBuffer() {Path path = Paths.get(getClass().getResource(FILE_NAME).getPath());byte[] bytes = CONTENT.getBytes(Charset.forName(CHARSET));try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ,StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {MappedByteBuffer mappedByteBuffer = fileChannel.map(READ_WRITE, 0, bytes.length);if (mappedByteBuffer != null) {mappedByteBuffer.put(bytes);mappedByteBuffer.force();}} catch (IOException e) {e.printStackTrace();}
}
  • 读文件数据:打开文件通道 fileChannel 并提供只读权限,通过 fileChannel 映射到一个只可读的内存缓冲区 mappedByteBuffer,读取 mappedByteBuffer 中的字节数组即可得到文件数据。
@Test
public void readFromFileByMappedByteBuffer() {Path path = Paths.get(getClass().getResource(FILE_NAME).getPath());int length = CONTENT.getBytes(Charset.forName(CHARSET)).length;try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ)) {MappedByteBuffer mappedByteBuffer = fileChannel.map(READ_ONLY, 0, length);if (mappedByteBuffer != null) {byte[] bytes = new byte[length];mappedByteBuffer.get(bytes);String content = new String(bytes, StandardCharsets.UTF_8);assertEquals(content, "Zero copy implemented by MappedByteBuffer");}} catch (IOException e) {e.printStackTrace();}
}

下面介绍 map() 方法的底层实现原理。map() 方法是 java.nio.channels.FileChannel 的抽象方法,由子类 sun.nio.ch.FileChannelImpl.java 实现,下面是和内存映射相关的核心代码:

public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException {int pagePosition = (int)(position % allocationGranularity);long mapPosition = position - pagePosition;long mapSize = size + pagePosition;try {addr = map0(imode, mapPosition, mapSize);} catch (OutOfMemoryError x) {System.gc();try {Thread.sleep(100);} catch (InterruptedException y) {Thread.currentThread().interrupt();}try {addr = map0(imode, mapPosition, mapSize);} catch (OutOfMemoryError y) {throw new IOException("Map failed", y);}}int isize = (int)size;Unmapper um = new Unmapper(addr, mapSize, isize, mfd);if ((!writable) || (imode == MAP_RO)) {return Util.newMappedByteBufferR(isize, addr + pagePosition, mfd, um);} else {return Util.newMappedByteBuffer(isize, addr + pagePosition, mfd, um);}
}

map() 方法通过本地方法 map0() 在用户态进程中为文件分配一块虚拟内存,作为它的内存映射区域(这块虚拟内存会映射到内核一块缓冲区),函数然后返回这块内存映射区域的起始地址。

  1. 文件映射需要在 Java 堆中创建一个 MappedByteBuffer 的实例。如果第一次文件映射导致 OOM,则手动触发垃圾回收,休眠 100ms 后再尝试映射,如果失败则抛出异常。
  2. 通过 Util 的 newMappedByteBuffer (可读可写)方法或者 newMappedByteBufferR(仅读) 方法方法反射创建一个 DirectByteBuffer 实例,其中 DirectByteBuffer 是 MappedByteBuffer 的子类。

通过map()方法的实现可以看到,最后调用的DirectByteBuffer的构造函数没有生产用户空间的缓冲数组,而是直接使用map0()返回的地址,生成DirectByteBuffer实例,填充里面的参数,之所以去调用DirectByteBuffer的构造函数,是为了利用DirectByteBuffer提供的一系列读写操作而已。请细读构造函数的代码,调用了如下方法。所以可以发现map方法就是做映射,并没有生成缓冲区

protected DirectByteBuffer(int cap, long addr,FileDescriptor fd,Runnable unmapper){super(-1, 0, cap, cap, fd); // 这里面的super调用了MappedBytebuf构造方法address = addr;cleaner = Cleaner.create(this, unmapper);att = null;}

map() 方法返回的是内存映射区域的起始地址,通过(起始地址 + 偏移量)就可以获取指定内存的数据。这样一定程度上替代了 read() 或 write() 方法,底层直接采用 sun.misc.Unsafe 类的 getByte() 和 putByte() 方法对数据进行读写。

private native long map0(int prot, long position, long mapSize) throws IOException;

上面是本地方法(native method)map0 的定义,它通过 JNI(Java Native Interface)调用底层 C 的实现,这个 native 函数(Java_sun_nio_ch_FileChannelImpl_map0)的实现位于 JDK 源码包下的 native/sun/nio/ch/FileChannelImpl.c 这个源文件里面。

JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this,jint prot, jlong off, jlong len)
{void *mapAddress = 0;jobject fdo = (*env)->GetObjectField(env, this, chan_fd);jint fd = fdval(env, fdo);int protections = 0;int flags = 0;if (prot == sun_nio_ch_FileChannelImpl_MAP_RO) {protections = PROT_READ;flags = MAP_SHARED;} else if (prot == sun_nio_ch_FileChannelImpl_MAP_RW) {protections = PROT_WRITE | PROT_READ;flags = MAP_SHARED;} else if (prot == sun_nio_ch_FileChannelImpl_MAP_PV) {protections =  PROT_WRITE | PROT_READ;flags = MAP_PRIVATE;}mapAddress = mmap64(0,                    /* Let OS decide location */len,                  /* Number of bytes to map */protections,          /* File permissions */flags,                /* Changes are shared */fd,                   /* File descriptor of mapped file */off);                 /* Offset into file */if (mapAddress == MAP_FAILED) {if (errno == ENOMEM) {JNU_ThrowOutOfMemoryError(env, "Map failed");return IOS_THROWN;}return handle(env, -1, "Map failed");}return ((jlong) (unsigned long) mapAddress);
}

可以看出 map0() 函数最终是通过 mmap64() 这个函数对 Linux 底层内核发出内存映射的调用, mmap64() 函数的原型如下:

#include <sys/mman.h>void *mmap64(void *addr, size_t len, int prot, int flags, int fd, off64_t offset);

下面详细介绍一下 mmap64() 函数各个参数的含义以及参数可选值:

  • addr:文件在用户进程空间的内存映射区中的起始地址,是一个建议的参数,通常可设置为 0 或 NULL,此时由内核去决定真实的起始地址。当 flags 为 MAP_FIXED 时,addr 就是一个必选的参数,即需要提供一个存在的地址。
  • len:文件需要进行内存映射的字节长度
  • prot:控制用户进程对内存映射区的访问权限
  • PROT_READ:读权限
  • PROT_WRITE:写权限
  • PROT_EXEC:执行权限
  • PROT_NONE:无权限
  • flags:控制内存映射区的修改是否被多个进程共享
  • MAP_PRIVATE:对内存映射区数据的修改不会反映到真正的文件,数据修改发生时采用写时复制机制
  • MAP_SHARED:对内存映射区的修改会同步到真正的文件,修改对共享此内存映射区的进程是可见的
  • MAP_FIXED:不建议使用,这种模式下 addr 参数指定的必须的提供一个存在的 addr 参数
  • fd:文件描述符。每次 map 操作会导致文件的引用计数加 1,每次 unmap 操作或者结束进程会导致引用计数减 1
  • offset:文件偏移量。进行映射的文件位置,从文件起始地址向后的位移量

下面总结一下 MappedByteBuffer 的特点和不足之处:

  • MappedByteBuffer 使用是堆外的虚拟内存(这点很重点,不在java堆里面,而是堆外的虚拟内存,但是一定在这个JVM进程里面),因此分配(map)的内存大小不受 JVM 的 -Xmx 参数限制,但是也是有大小限制的。
  • 如果当文件超出 Integer.MAX_VALUE 字节限制时,可以通过 position 参数重新 map 文件后面的内容。
  • MappedByteBuffer 在处理大文件时性能的确很高,但也存内存占用、文件关闭不确定等问题,被其打开的文件只有在垃圾回收的才会被关闭,而且这个时间点是不确定的。
  • MappedByteBuffer 提供了文件映射内存的 mmap() 方法,也提供了释放映射内存的 unmap() 方法。然而 unmap() 是 FileChannelImpl 中的私有方法,无法直接显示调用。因此,用户程序需要通过 Java 反射的调用 sun.misc.Cleaner 类的 clean() 方法手动释放映射占用的内存区域。
public static void clean(final Object buffer) throws Exception {AccessController.doPrivileged((PrivilegedAction<Void>) () -> {try {Method getCleanerMethod = buffer.getClass().getMethod("cleaner", new Class[0]);getCleanerMethod.setAccessible(true);Cleaner cleaner = (Cleaner) getCleanerMethod.invoke(buffer, new Object[0]);cleaner.clean();} catch(Exception e) {e.printStackTrace();}});
}

5.1.2 DirectByteBuffer

  • DirectByteBuffer 和零拷贝到底有什么关系?网上有写笔友说DirectByteBuffer就是内存映射文件,因为继承了MappedByteBuffer,但是经过代码分析后发现,完全不是一码事,仔细看下面分析。

DirectByteBuffer 的对象引用位于 Java 内存模型的堆里面,JVM 可以对 DirectByteBuffer 的对象进行内存分配和回收管理,一般使用 DirectByteBuffer 的静态方法 allocateDirect() 创建 DirectByteBuffer 实例并分配内存。

public static ByteBuffer allocateDirect(int capacity) {return new DirectByteBuffer(capacity);
}

DirectByteBuffer 内部的字节缓冲区位在于堆外的(用户态)直接内存,它是通过 Unsafe 的本地方法 allocateMemory() 进行内存分配,底层调用的是操作系统的 malloc() 函数。所以allocateDirect调用的构造函数在堆外内存生成了一个真实的内存缓冲区。怎么访问这个缓冲区中的数据?也是通过地址+偏移量的方式进行访问,具体看后面的put()和get()方法。

DirectByteBuffer(int cap) {super(-1, 0, cap, cap);boolean pa = VM.isDirectMemoryPageAligned();int ps = Bits.pageSize();long size = Math.max(1L, (long)cap + (pa ? ps : 0));Bits.reserveMemory(size, cap);long base = 0;try {base = unsafe.allocateMemory(size); // 在堆外内存申请了缓冲区,并返回缓冲区的地址} catch (OutOfMemoryError x) {Bits.unreserveMemory(size, cap);throw x;}unsafe.setMemory(base, size, (byte) 0);if (pa && (base % ps != 0)) {address = base + ps - (base & (ps - 1));} else {address = base;}cleaner = Cleaner.create(this, new Deallocator(base, size, cap));att = null;
}

除此之外,初始化 DirectByteBuffer 时还会创建一个 Deallocator 线程,并通过 Cleaner 的 freeMemory() 方法来对直接内存进行回收操作,freeMemory() 底层调用的是操作系统的 free() 函数。

private static class Deallocator implements Runnable {private static Unsafe unsafe = Unsafe.getUnsafe();private long address;private long size;private int capacity;private Deallocator(long address, long size, int capacity) {assert (address != 0);this.address = address;this.size = size;this.capacity = capacity;}public void run() {if (address == 0) {return;}unsafe.freeMemory(address);address = 0;Bits.unreserveMemory(size, capacity);}
}

由于使用 DirectByteBuffer 分配的是系统本地的内存,不在 JVM 的管控范围之内,因此直接内存的回收和堆内存的回收不同,直接内存如果使用不当,很容易造成 OutOfMemoryError。

  • 内存映像文件的随机读操作
public byte get() {return ((unsafe.getByte(ix(nextGetIndex()))));
}public byte get(int i) {return ((unsafe.getByte(ix(checkIndex(i)))));
}
  • 内存映像文件的随机写操作
public ByteBuffer put(byte x) {unsafe.putByte(ix(nextPutIndex()), ((x)));return this;
}public ByteBuffer put(int i, byte x) {unsafe.putByte(ix(checkIndex(i)), ((x)));return this;
}

内存映像文件的随机读写都是借助 ix() 方法实现定位的, ix() 方法通过内存映射空间的内存首地址(address)和给定偏移量计算出指针地址,然后由 unsafe 类的 get() 和 put() 方法和对指针指向的数据进行读取或写入。

private long ix(int i) {return address + ((long)i << 0);
}

直接内存如何跟零拷贝关联上?

DirectByteBuffer 自身是(Java)堆内的,它背后真正承载数据的buffer是在(Java)堆外——native memory中的。这是 malloc() 分配出来的内存,也是用户态的。用户态到内核态的数据传输如果不是通过mmap方式,就必须通过cpu拷贝。在JAVA NIO的SocketChannelImpl中我们可以看到read和write方法最后都会转化为直接内存来进行输入输出,重点在IOUtil.read()方法,可以看出,如果传入的是非directbytebuffer,IOUtil会先读入directbytebuffer再拷贝入用户的缓存,这里面多了一层拷贝,如果传入的是directbytebuffer,就直接调用readIntoNativeBuffer(var0, var1, var2, var4)拷贝入directbytebuffer,这样就少了一次拷贝

另一种可能的思考(感觉是不对的,大家可以探讨):其实直接内存产生是为了用户态和内核态可以共享一块内存,从而降低cpu拷贝次数。直接内存分配的是系统的本地内存,不在JVM的管辖范围内,但是内核和用户进程都能访问到直接内存,从而实现共享,减少拷贝次数。具体实现是把直接内存的描述符信息传递给DMA让DMA发起拷贝动作,不涉及cpu的拷贝。

// SocketChannelImpl的read方法
public int read(ByteBuffer var1) throws IOException {if (var1 == null) {throw new NullPointerException();} else {synchronized(this.readLock) {if (!this.ensureReadOpen()) {return -1;} else {int var3 = 0;boolean var20 = false;byte var10000;int var4;label369: {byte var5;try {label363: {var20 = true;this.begin();synchronized(this.stateLock) {if (!this.isOpen()) {var5 = 0;var20 = false;break label363;}this.readerThread = NativeThread.current();}do {var3 = IOUtil.read(this.fd, var1, -1L, nd);} while(var3 == -3 && this.isOpen());var4 = IOStatus.normalize(var3);var20 = false;break label369;}} finally {if (var20) {label273: {this.readerCleanup();this.end(var3 > 0 || var3 == -2);synchronized(this.stateLock) {if (var3 > 0 || this.isInputOpen) {break label273;}var10000 = -1;}return var10000;}assert IOStatus.check(var3);}}this.readerCleanup();this.end(var3 > 0 || var3 == -2);synchronized(this.stateLock) {if (var3 <= 0 && !this.isInputOpen) {var10000 = -1;return var10000;}}assert IOStatus.check(var3);return var5;}label305: {this.readerCleanup();this.end(var3 > 0 || var3 == -2);synchronized(this.stateLock) {if (var3 > 0 || this.isInputOpen) {break label305;}var10000 = -1;}return var10000;}assert IOStatus.check(var3);return var4;}}}}

在JAVA NIO中大规模使用直接内存进行读写,直接内存直接让DMA把数据从网卡拷贝到直接内存区域内(因为有物理内存地址和数据大小,直接就能做这个事情),所以减少了一次拷贝过程。

static int read(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {if (var1.isReadOnly()) {throw new IllegalArgumentException("Read-only buffer");} else if (var1 instanceof DirectBuffer) { // 如果是直接内存就读入直接内存中return readIntoNativeBuffer(var0, var1, var2, var4);} else { // 如果不是直接内存,先转换成直接内存再进行读取ByteBuffer var5 = Util.getTemporaryDirectBuffer(var1.remaining());int var7;try {int var6 = readIntoNativeBuffer(var0, var5, var2, var4);var5.flip();if (var6 > 0) {var1.put(var5);}var7 = var6;} finally {Util.offerFirstTemporaryDirectBuffer(var5);}return var7;}}

5.1 Netty 中的零拷贝实现

在netty中的零拷贝重点在两个方面:

1. 内存使用优化(其实不算是零拷贝)

2. 内存缓存池使用了DirectByteBuffer

5.1.1 内存使用优化(其实不算是零拷贝)

  • Netty 通过 DefaultFileRegion 类对 java.nio.channels.FileChannel 的 tranferTo() 方法进行包装,在文件传输时可以将文件缓冲区的数据直接发送到目的通道(Channel)
  • ByteBuf 可以通过 wrap 操作把字节数组、ByteBuf、ByteBuffer 包装成一个 ByteBuf 对象, 进而避免了拷贝操作
  • ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf,避免了内存的拷贝
  • Netty 提供了 CompositeByteBuf 类,它可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf,避免了各个 ByteBuf 之间的拷贝

5.1.2 内存缓存池使用了DirectByteBuffer

  • 先看看netty中如何从远程socket读取数据,读取数据过程中直接就分配了DirectByteBuffer
// io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe#read@Overridepublic final void read() {final ChannelConfig config = config();final ChannelPipeline pipeline = pipeline();final ByteBufAllocator allocator = config.getAllocator();final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();allocHandle.reset(config);ByteBuf byteBuf = null;boolean close = false;try {do {// 分配创建ByteBuffer, 此处实际就是直接内存的体现byteBuf = allocHandle.allocate(allocator);// 将数据读取到ByteBuffer中allocHandle.lastBytesRead(doReadBytes(byteBuf));if (allocHandle.lastBytesRead() <= 0) {// nothing was read. release the buffer.byteBuf.release();byteBuf = null;close = allocHandle.lastBytesRead() < 0;break;}allocHandle.incMessagesRead(1);readPending = false;// 读取到一部分数据,就向pipeline的下游传递,而非全部完成后再传递pipeline.fireChannelRead(byteBuf);byteBuf = null;} while (allocHandle.continueReading());allocHandle.readComplete();pipeline.fireChannelReadComplete();if (close) {closeOnRead(pipeline);}} catch (Throwable t) {handleReadException(pipeline, byteBuf, t, close, allocHandle);} finally {// Check if there is a readPending which was not processed yet.// This could be for two reasons:// * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method// * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method//// See https://github.com/netty/netty/issues/2254if (!readPending && !config.isAutoRead()) {removeReadOp();}}}}// io.netty.channel.DefaultMaxMessagesRecvByteBufAllocator.MaxMessageHandle#allocate@Overridepublic ByteBuf allocate(ByteBufAllocator alloc) {return alloc.ioBuffer(guess());}// io.netty.buffer.AbstractByteBufAllocator#ioBuffer(int)@Overridepublic ByteBuf ioBuffer(int initialCapacity) {if (PlatformDependent.hasUnsafe()) {return directBuffer(initialCapacity);}return heapBuffer(initialCapacity);}
  • netty中如何把数据写到远程socket?
// 写过程,将msg转换为直接内存存储的二进制数据// io.netty.handler.codec.MessageToByteEncoder#write@Overridepublic void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {ByteBuf buf = null;try {if (acceptOutboundMessage(msg)) {@SuppressWarnings("unchecked")I cast = (I) msg;// 默认 preferDirect = true;buf = allocateBuffer(ctx, cast, preferDirect);try {// 调用子类的实现,编码数据,以便实现私有协议encode(ctx, cast, buf);} finally {ReferenceCountUtil.release(cast);}if (buf.isReadable()) {// 写数据到远端ctx.write(buf, promise);} else {buf.release();ctx.write(Unpooled.EMPTY_BUFFER, promise);}buf = null;} else {ctx.write(msg, promise);}} catch (EncoderException e) {throw e;} catch (Throwable e) {throw new EncoderException(e);} finally {if (buf != null) {buf.release();}}}// io.netty.handler.codec.MessageToByteEncoder#allocateBuffer/*** Allocate a {@link ByteBuf} which will be used as argument of {@link #encode(ChannelHandlerContext, I, ByteBuf)}.* Sub-classes may override this method to return {@link ByteBuf} with a perfect matching {@code initialCapacity}.*/protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, @SuppressWarnings("unused") I msg,boolean preferDirect) throws Exception {if (preferDirect) {// PooledByteBufAllocatorreturn ctx.alloc().ioBuffer();} else {return ctx.alloc().heapBuffer();}}// io.netty.buffer.AbstractByteBufAllocator#ioBuffer()@Overridepublic ByteBuf ioBuffer() {if (PlatformDependent.hasUnsafe()) {return directBuffer(DEFAULT_INITIAL_CAPACITY);}return heapBuffer(DEFAULT_INITIAL_CAPACITY);}// io.netty.buffer.AbstractByteBufAllocator#directBuffer(int)@Overridepublic ByteBuf directBuffer(int initialCapacity) {return directBuffer(initialCapacity, DEFAULT_MAX_CAPACITY);}@Overridepublic ByteBuf directBuffer(int initialCapacity, int maxCapacity) {if (initialCapacity == 0 && maxCapacity == 0) {return emptyBuf;}validate(initialCapacity, maxCapacity);return newDirectBuffer(initialCapacity, maxCapacity);}// io.netty.buffer.PooledByteBufAllocator#newDirectBuffer@Overrideprotected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {PoolThreadCache cache = threadCache.get();PoolArena<ByteBuffer> directArena = cache.directArena;final ByteBuf buf;if (directArena != null) {buf = directArena.allocate(cache, initialCapacity, maxCapacity);} else {buf = PlatformDependent.hasUnsafe() ?UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity) :new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);}return toLeakAwareBuffer(buf);}// io.netty.buffer.PoolArena#allocate(io.netty.buffer.PoolThreadCache, int, int)PooledByteBuf<T> allocate(PoolThreadCache cache, int reqCapacity, int maxCapacity) {PooledByteBuf<T> buf = newByteBuf(maxCapacity);allocate(cache, buf, reqCapacity);return buf;}// io.netty.buffer.PoolArena.DirectArena#newByteBuf@Overrideprotected PooledByteBuf<ByteBuffer> newByteBuf(int maxCapacity) {if (HAS_UNSAFE) {return PooledUnsafeDirectByteBuf.newInstance(maxCapacity);} else {return PooledDirectByteBuf.newInstance(maxCapacity);}}private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {final int normCapacity = normalizeCapacity(reqCapacity);if (isTinyOrSmall(normCapacity)) { // capacity < pageSizeint tableIdx;PoolSubpage<T>[] table;boolean tiny = isTiny(normCapacity);if (tiny) { // < 512if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {// was able to allocate out of the cache so move onreturn;}tableIdx = tinyIdx(normCapacity);table = tinySubpagePools;} else {if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {// was able to allocate out of the cache so move onreturn;}tableIdx = smallIdx(normCapacity);table = smallSubpagePools;}final PoolSubpage<T> head = table[tableIdx];/*** Synchronize on the head. This is needed as {@link PoolChunk#allocateSubpage(int)} and* {@link PoolChunk#free(long)} may modify the doubly linked list as well.*/synchronized (head) {final PoolSubpage<T> s = head.next;if (s != head) {assert s.doNotDestroy && s.elemSize == normCapacity;long handle = s.allocate();assert handle >= 0;s.chunk.initBufWithSubpage(buf, handle, reqCapacity);incTinySmallAllocation(tiny);return;}}synchronized (this) {allocateNormal(buf, reqCapacity, normCapacity);}incTinySmallAllocation(tiny);return;}if (normCapacity <= chunkSize) {if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {// was able to allocate out of the cache so move onreturn;}synchronized (this) {allocateNormal(buf, reqCapacity, normCapacity);++allocationsNormal;}} else {// Huge allocations are never served via the cache so just call allocateHugeallocateHuge(buf, reqCapacity);}}// io.netty.util.internal.PlatformDependent0#newDirectBufferstatic ByteBuffer newDirectBuffer(long address, int capacity) {ObjectUtil.checkPositiveOrZero(capacity, "capacity");try {return (ByteBuffer) DIRECT_BUFFER_CONSTRUCTOR.newInstance(address, capacity);} catch (Throwable cause) {// Not expected to ever throw!if (cause instanceof Error) {throw (Error) cause;}throw new Error(cause);}}
// io.netty.buffer.AbstractByteBuf#writeBytes(byte[])@Overridepublic ByteBuf writeBytes(byte[] src) {writeBytes(src, 0, src.length);return this;}@Overridepublic ByteBuf writeBytes(byte[] src, int srcIndex, int length) {ensureWritable(length);setBytes(writerIndex, src, srcIndex, length);writerIndex += length;return this;}// io.netty.buffer.PooledUnsafeDirectByteBuf#setBytes(int, byte[], int, int)@Overridepublic ByteBuf setBytes(int index, byte[] src, int srcIndex, int length) {// addr() 将会得到一个内存地址UnsafeByteBufUtil.setBytes(this, addr(index), index, src, srcIndex, length);return this;}// io.netty.buffer.PooledUnsafeDirectByteBuf#addrprivate long addr(int index) {return memoryAddress + index;}// io.netty.buffer.UnsafeByteBufUtil#setBytes(io.netty.buffer.AbstractByteBuf, long, int, byte[], int, int)static void setBytes(AbstractByteBuf buf, long addr, int index, byte[] src, int srcIndex, int length) {buf.checkIndex(index, length);if (length != 0) {// 将字节数据copy到DirectByteBuffer中PlatformDependent.copyMemory(src, srcIndex, addr, length);}}// io.netty.util.internal.PlatformDependent#copyMemory(byte[], int, long, long)public static void copyMemory(byte[] src, int srcIndex, long dstAddr, long length) {PlatformDependent0.copyMemory(src, BYTE_ARRAY_BASE_OFFSET + srcIndex, null, dstAddr, length);}// io.netty.util.internal.PlatformDependent0#copyMemory(java.lang.Object, long, java.lang.Object, long, long)static void copyMemory(Object src, long srcOffset, Object dst, long dstOffset, long length) {//UNSAFE.copyMemory(src, srcOffset, dst, dstOffset, length);while (length > 0) {long size = Math.min(length, UNSAFE_COPY_THRESHOLD);// 最终由jvm的本地方法,进行内存的copy, 此处dst为null, 即数据只会copy到对应的 dstOffset 中// 偏移基数就是: 各种基础地址 ARRAY_OBJECT_BASE_OFFSET...UNSAFE.copyMemory(src, srcOffset, dst, dstOffset, size);length -= size;srcOffset += size;dstOffset += size;}}
// io.netty.channel.AbstractChannelHandlerContext#write(java.lang.Object, io.netty.channel.ChannelPromise)@Overridepublic ChannelFuture write(final Object msg, final ChannelPromise promise) {if (msg == null) {throw new NullPointerException("msg");}try {if (isNotValidPromise(promise, true)) {ReferenceCountUtil.release(msg);// cancelledreturn promise;}} catch (RuntimeException e) {ReferenceCountUtil.release(msg);throw e;}write(msg, false, promise);return promise;}private void write(Object msg, boolean flush, ChannelPromise promise) {AbstractChannelHandlerContext next = findContextOutbound();final Object m = pipeline.touch(msg, next);EventExecutor executor = next.executor();if (executor.inEventLoop()) {if (flush) {next.invokeWriteAndFlush(m, promise);} else {next.invokeWrite(m, promise);}} else {AbstractWriteTask task;if (flush) {task = WriteAndFlushTask.newInstance(next, m, promise);}  else {task = WriteTask.newInstance(next, m, promise);}safeExecute(executor, task, promise, m);}}private void invokeWrite(Object msg, ChannelPromise promise) {if (invokeHandler()) {invokeWrite0(msg, promise);} else {write(msg, promise);}}private void invokeWrite0(Object msg, ChannelPromise promise) {try {((ChannelOutboundHandler) handler()).write(this, msg, promise);} catch (Throwable t) {notifyOutboundHandlerException(t, promise);}}// io.netty.channel.DefaultChannelPipeline.HeadContext#write@Overridepublic void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {unsafe.write(msg, promise);}// io.netty.channel.AbstractChannel.AbstractUnsafe#write@Overridepublic final void write(Object msg, ChannelPromise promise) {assertEventLoop();ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;if (outboundBuffer == null) {// If the outboundBuffer is null we know the channel was closed and so// need to fail the future right away. If it is not null the handling of the rest// will be done in flush0()// See https://github.com/netty/netty/issues/2362safeSetFailure(promise, WRITE_CLOSED_CHANNEL_EXCEPTION);// release message now to prevent resource-leakReferenceCountUtil.release(msg);return;}int size;try {// 转换msg为直接内存,如有必要msg = filterOutboundMessage(msg);size = pipeline.estimatorHandle().size(msg);if (size < 0) {size = 0;}} catch (Throwable t) {safeSetFailure(promise, t);ReferenceCountUtil.release(msg);return;}// 将msg放入outboundBuffer中,即相当于写完了数据outboundBuffer.addMessage(msg, size, promise);}// io.netty.channel.nio.AbstractNioByteChannel#filterOutboundMessage@Overrideprotected final Object filterOutboundMessage(Object msg) {if (msg instanceof ByteBuf) {ByteBuf buf = (ByteBuf) msg;if (buf.isDirect()) {return msg;}return newDirectBuffer(buf);}if (msg instanceof FileRegion) {return msg;}throw new UnsupportedOperationException("unsupported message type: " + StringUtil.simpleClassName(msg) + EXPECTED_TYPES);}// io.netty.channel.ChannelOutboundBuffer#addMessage/*** Add given message to this {@link ChannelOutboundBuffer}. The given {@link ChannelPromise} will be notified once* the message was written.*/public void addMessage(Object msg, int size, ChannelPromise promise) {Entry entry = Entry.newInstance(msg, size, total(msg), promise);if (tailEntry == null) {flushedEntry = null;} else {Entry tail = tailEntry;tail.next = entry;}tailEntry = entry;if (unflushedEntry == null) {unflushedEntry = entry;}// increment pending bytes after adding message to the unflushed arrays.// See https://github.com/netty/netty/issues/1619// 如有必要,立即触发 fireChannelWritabilityChanged 事件,从而使立即向网络写入数据incrementPendingOutboundBytes(entry.pendingSize, false);}

Linux IO体系、零拷贝和虚拟内存关系的重新思考相关推荐

  1. Linux 中的零拷贝技术,第 2 部分

    技术实现 本系列由两篇文章组成,介绍了当前用于 Linux 操作系统上的几种零拷贝技术,简单描述了各种零拷贝技术的实现,以及它们的特点和适用场景.第一部分主要介绍了一些零拷贝技术的相关背景知识,简要概 ...

  2. Linux网络编程 | 零拷贝 :sendfile、mmap、splice、tee

    文章目录 传统文件传输的问题 Linux中实现零拷贝的方法 传统文件传输的问题 在网络编程中,如果我们想要提供文件传输的功能,最简单的方法就是用read将数据从磁盘上的文件中读取出来,再将其用writ ...

  3. Linux 中的零拷贝技术

    引言 传统的 Linux 操作系统的标准 I/O 接口是基于数据拷贝操作的,即 I/O 操作会导致数据在操作系统内核地址空间的缓冲区和应用程序地址空间定义的缓冲区之间进行传输.这样做最大的好处是可以减 ...

  4. 操作系统-IO与零拷贝【万字文,比较详细的解析】

    文章目录 IO 阻塞与非阻塞 I/O 和 同步与异步 I/O 阻塞IO 非阻塞IO IO多路复用 异步IO 直接与非直接I/O 缓冲与非缓冲I/O 零拷贝 标准设备 标准协议 利用中断减少CPU开销 ...

  5. io多路复用·零拷贝·while死循环cpu

    文章目录 引用文章 问题 io多路复用效率为什么这么高 epoll和select/poll什么时候用 epoll的LT和ET 从 jdk 的 nio 到 epoll 源码与实现内幕全面解析 io多路复 ...

  6. 详解磁盘IO、网络IO、零拷贝IO、BIO、NIO、AIO、IO多路复用(select、poll、epoll)

    文章很长,但是很用心! 文章目录 1. 什么是I/O 2. 磁盘IO 3. 网络IO 4. IO中断与DMA 5. 零拷贝IO 6. BIO 7. NIO 8. IO多路复用 8.1 select 8 ...

  7. Linux网络处理“零拷贝”技术mmap()内核进程间通信设计8086分页管理——摆在一起来谈谈...

    Jack:最近听说了网络处理的"零拷贝"技术,觉得非常神奇,在网上查阅了很多资料.不过,并不是太明白--知其然,而不知其所以然.你能通俗地解释一下吗? 我:这是一个相对比较复杂的话 ...

  8. linux 中的零拷贝技术,第 2 部分,Linux的零拷贝技术

    8种机械键盘轴体对比 本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选? 不是零拷贝的情况下是如何进行的,有什么不好的地方? 一次读取磁盘文件发送到网络的数据的拷贝过程: 用户态:用户进程开辟的 ...

  9. Linux 操作系统原理 — 零拷贝技术

    目录 文章目录 目录 Linux I/O 缓存背景 为什么需要零拷贝? 零拷贝技术(Zero-Copy) 方法一:用户态直接 I/O 方法二:mmap + write 方法三:Sendfile 方法四 ...

最新文章

  1. 交通运输部部长李小鹏谈及自动驾驶:包容失败、反对垄断,力争在国家层面出台指导意见...
  2. 漏洞高危 中危 低危的划分标准
  3. 阅读代码和修改别人代码的一些技巧以及注意事项
  4. Linux中关于useradd、chmod、chown、getfacl、setfact等权限设置
  5. spring18-1:采用jdk的动态代理 proxy。
  6. OAuth:服务给第三方app授权的协议
  7. 用SQL进行用户留存率计算
  8. 清远机器人编程_致敬逆行者:棒棒贝贝为清远援鄂人员子女免费提供一年乐高编程课...
  9. 程序员,其实你可以做的更好
  10. Kubernetes 学习总结(19)—— Kubernetes 集群管理平台如何选择?Rancher vs KubeSphere
  11. OpenGL---GLUT教程(一) GLUT简介,体系
  12. Python urllib爬取百度首页
  13. 2、http网络编程——libcurl的使用
  14. 中国史上最牛的网管——李兴平
  15. Matlab uicontrol 用法
  16. C++头文件和cpp文件的原理
  17. BZOJ 3505 【CQOI2014】 数三角形
  18. vs2015遇见问题:后面有“::”的名称一定是类名或命名空间名
  19. mac更新系统版本后的安装包路径
  20. 福州市仓山区融丰锦秀山庄别墅设计

热门文章

  1. mysql skip 1062_【20180205】MySQL 1032和1062跳过错误总结
  2. Win7系统输入法突然不见了
  3. 信号完整性(SI)电源完整性(PI)学习笔记(五)电容的物理基础
  4. Telegram、Telethon
  5. java计算机毕业设计网课系统源码+系统+数据库+lw文档+mybatis+运行部署
  6. PS 羽化工具使用
  7. 跑步机行业研究及十四五规划分析报告
  8. mongoose用模型更新不了,因为模型对象中默认带有_id会提示errmsg: “Performing an update on the path ‘_id‘ would modify the i
  9. 订单拆单,电商开发时 经常遇到的问题
  10. Android 构建简单app 步骤