文章目录
- 1 零拷贝的介绍
- 2 传统的IO的执行流程
- 3 零拷贝涉及的技术点
- 3.1 内核空间和用户空间
- 3.2 用户态和内核态
- 3.3 DMA技术
- 4 零拷贝实现的几种方式
- 4.1 mmap+write
- 4.2 sendfile
- 4.3 sendfile+DMA scatter/gather实现的零拷贝
- 5 java提供的零拷贝方式
- 5.1 Java NIO对mmap的支持
- 5.2 Java NIO对sendfile的支持
1 零拷贝的介绍
零拷贝是指计算机执行IO操作时,CPU不需要将数据从一个存储区域复制到另一个存储区域,从而可以减少上下文切换以及CPU的拷贝时间。它是一种I/O操作优化技术。
2 传统的IO的执行流程
传统的IO流程包括read和write的过程
read:把数据从磁盘读到内核缓冲区,再拷贝到用户缓冲区
write:先把数据写到socket缓冲区,再写入到网卡设备
用户应用进程调用read函数,向操作系统发起IO调用, 上下文从用户态转为内核态(切换1)
DMA控制器把数据从磁盘中,读取到内核缓冲区。
CPU把内核缓冲区数据,拷贝到用户应用缓冲区, 上下文从内核态转为用户态(切换2),read函数返回
用户应用进程通过write函数,发起IO调用, 上下文从用户态转为内核态(切换3)
CPU将用户缓冲区中的数据,拷贝到socket缓冲区
DMA控制器把数据从socket缓冲区,拷贝到网卡设备, 上下文从内核态切换回用户态(切换4),write函数返回
从流程图可以看出,传统IO的读写流程,包括了4次上下文切换(4次用户态和内核态的切换),4次数据拷贝(两次CPU拷贝以及两次的DMA拷贝),什么是DMA拷贝呢?我们一起来回顾下,零拷贝涉及的操作系统知识点哈。
3 零拷贝涉及的技术点
3.1 内核空间和用户空间
内核空间:主要提供进程调度、内存分配、连接硬件资源等功能
用户空间:提供给各个程序进程的空间,它不具有访问内核空间资源的权限,如果应用程序需要使用到内核空间的资源,则需要通过系统调用来完成。进程从用户空间切换到内核空间,完成相关操作后,再从内核空间切换回用户空间。
3.2 用户态和内核态
如果进程运行于内核空间,被称为进程的内核态
如果进程运行于用户空间,被称为进程的用户态。
3.3 DMA技术
DMA,英文全称是Direct Memory Access,即直接内存访问。DMA本质上是一块主板上独立的芯片,允许外设设备和内存存储器之间直接进行IO数据传输,其过程不需要CPU的参与。
用户应用进程调用read函数,向操作系统发起IO调用,进入阻塞状态,等待数据返回。
CPU收到指令后,对DMA控制器发起指令调度。
DMA收到IO请求后,将请求发送给磁盘;
磁盘将数据放入磁盘控制缓冲区,并通知DMA
DMA将数据从磁盘控制器缓冲区拷贝到内核缓冲区。
DMA向CPU发出数据读完的信号,把工作交换给CPU,由CPU负责将数据从内核缓冲区拷贝到用户缓冲区。
用户应用进程由内核态切换回用户态,解除阻塞状态
4 零拷贝实现的几种方式
零拷贝并不是没有拷贝数据,而是减少用户态/内核态的切换次数以及CPU拷贝的次数。零拷贝实现有多种方式,分别是
mmap+write
sendfile
带有DMA收集拷贝功能的sendfile
4.1 mmap+write
addr:指定映射的虚拟内存地址
length:映射的长度
prot:映射内存的保护模式
flags:指定映射的类型
fd:进行映射的文件句柄
offset:文件偏移量
用户进程通过 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
out_fd:为待写入内容的文件描述符,一个socket描述符。,
in_fd:为待读出内容的文件描述符,必须是真实的文件,不能是socket和管道。
offset:指定从读入文件的哪个位置开始读,如果为NULL,表示文件的默认起始位置。
count:指定在fdout和fdin之间传输的字节数。
sendfile表示在两个文件描述符之间传输数据,它是在操作系统内核中操作的,避免了数据从内核缓冲区和用户缓冲区之间的拷贝操作,因此可以使用它来实现零拷贝。
用户进程发起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版本之后,对sendfile做了优化升级,引入SG-DMA技术,其实就是对DMA拷贝加入了scatter/gather操作,它可以直接从内核空间缓冲区中将数据读取到网卡。使用这个特点搞零拷贝,即还可以多省去一次CPU拷贝。
用户进程发起sendfile系统调用, 上下文(切换1)从用户态转向内核态
DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
CPU把内核缓冲区中的 文件描述符信息(包括内核缓冲区的内存地址和偏移量)发送到socket缓冲区
DMA控制器根据文件描述符信息,直接把数据从内核缓冲区拷贝到网卡
上下文(切换2)从内核态切换回用户态,sendfile调用返回。
可以发现,sendfile+DMA scatter/gather实现的零拷贝,I/O发生了2次用户空间与内核空间的上下文切换,以及2次数据拷贝。其中2次数据拷贝都是包DMA拷贝。这就是真正的 零拷贝(Zero-copy) 技术,全程都没有通过CPU来搬运数据,所有的数据都是通过DMA来进行传输的。
5 java提供的零拷贝方式
5.1 Java NIO对mmap的支持
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.2 Java NIO对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);
}
public class SendFileTest {
public static void main(String[] args) {
try {
FileChannel readChannel = FileChannel.open(Paths.get("./jay.txt"), StandardOpenOption.READ);
long len = readChannel.size();
long position = readChannel.position();
FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//数据传输
readChannel.transferTo(position, len, writeChannel);
readChannel.close();
writeChannel.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}