文章目录
- 什么是零拷贝?
- 传统IO执行过程
- 零拷贝的意义
- 零拷贝的主要实现方式
- 实际应用场景
- 零拷贝的优势
- 零拷贝的局限性
- Java 中的零拷贝实现
- `FileChannel.transferTo()`
- `FileChannel.transferFrom()`
- 相关知识点解释
- 什么是DMA
- 内核空间和用户空间
- 什么是用户态、内核态
- 什么是上下文切换
- 虚拟内存
什么是零拷贝?
零拷贝(Zero-copy)是一种计算机编程技术和体系结构设计,旨在减少数据在不同缓冲区或内存区域之间的拷贝次数,从而提高系统性能和效率。零拷贝技术主要用于提高数据传输和处理的速度,特别是在涉及大量数据传输的场景中,如网络通信、文件系统操作和多媒体处理等。
传统IO执行过程
- 用户应用进程调用read函数,向操作系统发起IO调用, 上下文从用户态转为内核态(切换1)
- DMA控制器把数据从磁盘中,读取到内核缓冲区。
- CPU把内核缓冲区数据,拷贝到用户应用缓冲区, 上下文从内核态转为用户态(切换2),read函数返回
- 用户应用进程通过write函数,发起IO调用, 上下文从用户态转为内核态(切换3)
- CPU将用户缓冲区中的数据,拷贝到socket缓冲区
- DMA控制器把数据从socket缓冲区,拷贝到网卡设备, 上下文从内核态切换回用户态(切换4),write函数返回
零拷贝的意义
传统上,数据在操作系统和应用程序之间传输时通常需要多次拷贝,例如从网络接口卡(NIC)到用户空间的缓冲区,再从用户空间的缓冲区到内核空间的缓冲区,等等。每次拷贝都会消耗 CPU 时间和内存带宽。零拷贝技术试图通过减少这些不必要的拷贝来提高性能。
零拷贝的主要实现方式
零拷贝并不是没有拷贝数据,而是减少用户态/内核态的切换次数以及CPU拷贝的次数。零拷贝实现有多种方式,分别是
-
- mmap+write
-
- sendfile
-
- 带有DMA收集拷贝功能的sendfile
- mmap+write 实现方式:
- 使用
mmap()
系统调用将文件映射到内存,这样应用程序可以直接操作内存中的数据,而无需显式地读取或写入文件。当应用程序对内存进行修改时,这些修改会被自动同步到文件中。
- 用户进程通过 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拷贝‘’并且用户进程内存是虚拟的,只是映射到内核的读缓冲区,可以节省一半的内存空间。
- 发送页面(Sendfile):
sendfile()
系统调用可以从一个文件描述符直接将数据发送到另一个文件描述符,通常用于将数据从磁盘文件直接发送到网络套接字,而无需经过用户空间。
- 用户进程发起sendfile系统调用, 上下文(切换1)从用户态转向内核态
- DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
- CPU将读缓冲区中数据拷贝到socket缓冲区
- DMA控制器,异步把数据从socket缓冲区拷贝到网卡,
- 上下文(切换2)从内核态切换回用户态,sendfile调用返回。
sendfile实现的零拷贝,I/O发生了2次用户空间与内核空间的上下文切换,以及3次数据拷贝。其中3次数据拷贝中,包括了2次DMA拷贝和1次CPU拷贝。那能不能把CPU拷贝的次数减少到0次呢?有的,即带有DMA收集拷贝功能的sendfile!
- sendfile+DMA scatter/gather:
- 用户进程发起sendfile系统调用, 上下文(切换1)从用户态转向内核态
- DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
- CPU把内核缓冲区中的 文件描述符信息(包括内核缓冲区的内存地址和偏移量)发送到socket缓冲区
- DMA控制器根据文件描述符信息,直接把数据从内核缓冲区拷贝到网卡
- 上下文(切换2)从内核态切换回用户态,sendfile调用返回。
sendfile+DMA scatter/gather
实现的零拷贝,I/O发生了2次用户空间与内核空间的上下文切换,以及2次数据拷贝。其中2次数据拷贝都是包DMA拷贝。这就是真正的 零拷贝(Zero-copy) 技术,全程都没有通过CPU来搬运数据,所有的数据都是通过DMA来进行传输的。
实际应用场景
- 网络服务器:
- 在高性能网络服务器中,零拷贝技术可以显著提高数据传输速度。例如,使用
sendfile()
可以直接将文件内容发送到网络,而无需将其复制到用户空间。
- 在高性能网络服务器中,零拷贝技术可以显著提高数据传输速度。例如,使用
- 文件系统:
- 在文件系统中,使用内存映射技术可以让应用程序直接读写文件,而无需通过传统的读写系统调用。
- 数据库系统:
- 在数据库中,直接 I/O 技术可以用来减少数据页在内存和磁盘之间的拷贝,提高数据库性能。
- 多媒体处理:
- 在视频流媒体服务器或音频处理软件中,零拷贝技术可以减少数据从磁盘到网络传输过程中的延迟。
零拷贝的优势
- 性能提升: 减少了数据拷贝的次数,降低了 CPU 负载,提高了数据传输速度。
- 内存带宽节省: 减少内存带宽的占用,提高了系统的整体吞吐量。
- 降低延迟: 对于实时应用,零拷贝可以显著减少数据处理的延迟。
零拷贝的局限性
- 适用范围有限: 零拷贝技术并不是在所有场景下都能有效应用,特别是在需要对数据进行复杂处理的情况下。
- 编程复杂度: 实现零拷贝通常需要更复杂的编程技巧和对底层系统的深入理解。
总的来说,零拷贝技术是一种重要的优化手段,可以显著提高数据密集型应用的性能。
Java 中的零拷贝实现
在 Java 中,零拷贝主要通过 FileChannel.transferTo()
和 FileChannel.transferFrom()
方法来实现。这两个方法允许直接在两个文件通道之间传输数据,而不需要通过中间缓冲区。
FileChannel.transferTo()
transferTo()
方法用于将一个文件通道中的数据直接传输到另一个文件通道。这种方法适用于从文件读取数据并直接发送到网络的情况。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
public class ZeroCopyExample {
public static void main(String[] args) throws Exception {
// 打开文件输入流和输出流
FileInputStream fileInputStream = new FileInputStream("input.txt");
FileOutputStream fileOutputStream = new FileOutputStream("output.txt");
// 获取文件通道
FileChannel inputChannel = fileInputStream.getChannel();
FileChannel outputChannel = fileOutputStream.getChannel();
// 使用 transferTo() 方法直接传输数据
long transferred = inputChannel.transferTo(0, inputChannel.size(), outputChannel);
System.out.println("Transferred bytes: " + transferred);
// 关闭资源
inputChannel.close();
outputChannel.close();
}
}
在这个例子中,transferTo()
方法直接将 input.txt
文件的内容传输到了 output.txt
文件中,而不需要先读取到内存缓冲区再写入。
FileChannel.transferFrom()
transferFrom()
方法用于将一个文件通道中的数据直接传输到另一个文件通道。这种方法适用于从网络接收数据并直接写入文件的情况。
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
import java.net.Socket;
public class ZeroCopyReceiveExample {
public static void main(String[] args) throws Exception {
// 连接到服务器
Socket socket = new Socket("hostname", 1234);
SocketChannel socketChannel = socket.getChannel();
// 打开文件输出流
FileOutputStream fileOutputStream = new FileOutputStream("received.txt");
FileChannel fileChannel = fileOutputStream.getChannel();
// 使用 transferFrom() 方法直接传输数据
long transferred = fileChannel.transferFrom(socketChannel, 0, Long.MAX_VALUE);
System.out.println("Transferred bytes: " + transferred);
// 关闭资源
fileChannel.close();
socketChannel.close();
}
}
在这个例子中,transferFrom()
方法直接将从网络接收到的数据写入到 received.txt
文件中,而不需要先读取到内存缓冲区再写入。
相关知识点解释
什么是DMA
DMA(Direct Memory Access,直接存储器访问)是一种让外设(如硬盘、网络接口卡等)与计算机内存之间直接进行数据传输的技术,而不需要通过CPU来传输每一个数据块。这意味着,在DMA操作期间,数据可以直接在内存和外设之间复制,从而减轻了CPU的负担,并提高了数据传输的速度和效率。
DMA通常用于高速设备的数据传输,例如磁盘控制器、声卡、图形卡和其他需要大量数据传输的硬件设备。当一个设备请求DMA时,它会暂时接管总线并直接与内存通信,这样CPU就可以处理其他任务或者等待DMA操作完成的通知。
DMA操作一般分为几个步骤:
- 请求:外设向DMA控制器发出DMA请求。
- 响应:DMA控制器如果可以接受请求,则会向CPU申请总线控制权。
- 暂停CPU:CPU暂停当前的工作,释放总线控制权给DMA控制器。
- 数据传输:DMA控制器接管总线后,直接在外设和内存之间传输数据。
- 恢复CPU控制:当数据传输完成后,DMA控制器释放总线控制权,CPU恢复执行。
DMA技术对于提高系统的整体性能非常重要,尤其是在处理大量数据传输的情况下。
内核空间和用户空间
我们电脑上跑着的应用程序,其实是需要经过操作系统,才能做一些特殊操作,如磁盘文件读写、内存的读写等等。因为这些都是比较危险的操作,不可以由应用程序乱来,只能交给底层操作系统来。
因此,操作系统为每个进程都分配了内存空间,一部分是用户空间,一部分是内核空间。内核空间是操作系统内核访问的区域,是受保护的内存空间,而用户空间是用户应用程序访问的内存区域。 以32位操作系统为例,它会为每一个进程都分配了4G(2的32次方)的内存空间。
- 内核空间:主要提供进程调度、内存分配、连接硬件资源等功能
- 用户空间:提供给各个程序进程的空间,它不具有访问内核空间资源的权限,如果应用程序需要使用到内核空间的资源,则需要通过系统调用来完成。进程从用户空间切换到内核空间,完成相关操作后,再从内核空间切换回用户空间。
什么是用户态、内核态
- 如果进程运行于内核空间,被称为进程的内核态
- 如果进程运行于用户空间,被称为进程的用户态。
什么是上下文切换
- 什么是CPU上下文?
CPU 寄存器,是CPU内置的容量小、但速度极快的内存。而程序计数器,则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。它们都是 CPU 在运行任何任务前,必须的依赖环境,因此叫做CPU上下文。
- 什么是 CPU上下文切换?
它是指,先把前一个任务的CPU上下文(也就是CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
一般我们说的上下文切换,就是指内核(操作系统的核心)在CPU上对进程或者线程进行切换。进程从用户态到内核态的转变,需要通过系统调用来完成。系统调用的过程,会发生CPU上下文的切换。
CPU 寄存器里原来用户态的指令位置,需要先保存起来。接着,为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。最后才是跳转到内核态运行内核任务。
虚拟内存
现代操作系统使用虚拟内存,即虚拟地址取代物理地址,使用虚拟内存可以有2个好处:
- 虚拟内存空间可以远远大于物理内存空间
- 多个虚拟内存可以指向同一个物理地址