零拷贝
本文最后更新于:2024年3月18日 晚上
零拷贝
- 零拷贝是指计算机执行 IO 操作时,CPU 不需要将数据从一个存储区域复制到另一个存储区域,从而可以减少上下文切换以及 CPU 的拷贝时间,它是一种
I/O
操作优化技术。
传统 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 拷贝)
零拷贝实现的几种方式
- 零拷贝并不是没有拷贝数据,而是减少用户态/内核态的切换次数以及 CPU 拷贝的次数,零拷贝实现有多种方式,分别是:
- mmap+write
- sendfile
- 带有 DMA 收集拷贝功能的 sendfile
mmap+write 实现的零拷贝
- mmap 的函数原型如下:
1 |
|
-
addr:指定映射的虚拟内存地址。
-
length:映射的长度。
-
prot:映射内存的保护模式。
-
flags:指定映射的类型。
-
fd:进行映射的文件句柄。
-
offset:文件偏移量。
-
mmap 利用用了虚拟内存的特点,将内核中的读缓冲区与用户空间的缓冲区进行映射,所有的 IO 都在内核中完成,从而减少数据拷贝次数。
mmap+write
实现的零拷贝流程如下:- 用户进程通过
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
是 Linux 2.1 内核版本后引入的一个系统调用函数,API 如下:
1 |
|
-
out_fd:为待写入内容的文件描述符,一个 Socket 描述符。
-
in_fd:为待读出内容的文件描述符,必须是真实的文件,不能是 Socket 和管道。
-
offset:指定从读入文件的哪个位置开始读,如果为 NULL,表示文件的默认起始位置。
-
count:指定在 fdout 和 fdin 之间传输的字节数。
-
sendfile 表示在两个文件描述符之间传输数据,它是在操作系统内核中操作的,避免了数据从内核缓冲区和用户缓冲区之间的拷贝操作,因此可以使用它来实现零拷贝。
- sendfile 实现的零拷贝流程如下:
- 用户进程发起 sendfile 系统调用,上下文(切换 1)从用户态转向内核态
- DMA 控制器,把数据从硬盘中拷贝到内核缓冲区。
- CPU 将读缓冲区中数据拷贝到 Socket 缓冲区。
- DMA 控制器,异步把数据从 Socket 缓冲区拷贝到网卡。
- 上下文(切换 2)从内核态切换回用户态, sendfile 调用返回。
- 可以发现,
sendfile
实现的零拷贝,I/O 发生了2次用户空间与内核空间的上下文切换,以及 3 次数据拷贝,其中 3 次数据拷贝中,包括了2 次 DMA 拷贝和 1 次 CPU 拷贝
sendfile+DMA scatter/gather 实现的零拷贝
- linux 2.4 版本之后,对
sendfile
做了优化升级,引入 SG-DMA 技术,其实就是对 DMA 拷贝加入了scatter/gather
操作,它可以直接从内核空间缓冲区中将数据读取到网卡,使用这个特点实现零拷贝,还可以多省去一次 CPU 拷贝
- 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 来进行传输的。
Java 提供的零拷贝方式
- Java NIO 对 mmap 的支持。
- Java NIO 对 sendfile 的支持。
Java NIO 对 mmap 的支持
- Java NIO 有一个
MappedByteBuffer
的类,可以用来实现内存映射,它的底层是调用了 Linux 内核的mmap的 API
1 |
|
Java NIO 对 sendfile 的支持
- FileChannel 的
transferTo()/transferFrom()
,底层就是 sendfile ()系统调用函数,Kafka 这个开源项目就用到它,平时面试的时候,回答面试官为什么这么快,就可以提到零拷贝sendfile
这个点。
1 |
|
1 |
|
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!