猿博园

记录生活及工作中的点点滴滴

0%

一文搞懂何为 `零拷贝`

一、缓冲I/O和直接I/O

理解缓冲I/O和直接I/O,先搞清以下几个概念:

1、应用程序内存

通常是指在代码中通过 malloc/freenew/delete 等分配出来的内存。

2、用户缓冲区

用户进程通过系统调用访问系统资源的时候,需要切换到内核态,而这对应一些特殊的堆栈和内存环境,必须在系统调用前建 立好。而在系统调用结束后,cpu 会从核心模式切回到用户模式,而堆栈又必须恢复成用户进程的上下文。而这种切换就会有大量的耗时。

你看一些程序在读取文件时,会先申请一块内存数组,称为buffer,然后每次调用read,读取设定字节长度的数据,写入buffer。(用较小的次数填满buffer)。之后的程序都是从buffer中获取数据,当buffer使用完后,在进行下一次调用,填充buffer。所以说:用户缓冲区的目的是为了减少系统调用次数,从而降低操作系统在用户态与核心态切换所耗费的时间。除了在进程中设计缓冲区,内核也有自己的缓冲区。

比如在C语言的FILE结构体里面的bufferFILE结构体的定义如下,可以看到里面有定义的buffer

1
2
3
4
5
6
7
8
9
10
11
typedef struct {
short level;
short token;
short bsize;
char fd;
unsigned flags;
unsigned char hold;
unsigned char *buffer;
unsigned char * curp;
unsigned istemp;
}FILE;

3、内核缓冲区

Linux操作系统的Page Cache。为了加快磁盘的I/O,Linux系统会把磁盘上的数据以Page 为单位缓存在操作系统的内存里,这里的Page是Linux系统定义的一个逻辑概念,一个Page一般为4K。

对于缓冲I/O,一个读操作有3次数据拷贝,一个写操作,有反向的3次数据拷贝:

  • 读:磁盘 ——> 内核缓冲区 ——> 用户缓冲区 ——> 应用程序内存;
  • 写:应用程序内存 ——> 用户缓冲区 ——>内核缓冲区 ——> 磁盘

对于直接I/O,一个读操作会有2次数据拷贝,一个写操作,有反向的2次数据拷贝

  • 读:磁盘 ——> 内核缓冲区 ——> 应用程序内存

  • 写:应用程序内存 ——> 内核缓冲区 ——> 磁盘

    所以,所谓的“直接I/O”,其中直接的意思是指没有用户级的缓冲区,但操作系统本身的缓冲还是有的。

缓冲I/O和直接I/O两者对比如下图所示:

img

二、内存映射文件与零拷贝

1、内存映射文件

相比于直接I/O,内存映射文件往前更近了一步,当用户空间不再有物理内存,直接拿应用程序的逻辑内存地址映射到 Linux 操作系统内核缓冲区,应用程序虽然读写是自己的内存,但这个内存只是一个“逻辑地址”,实际读写的内存是内核缓冲区!

如:Java中的 MappedByteBuffer 类实现的就是内存映射文件

数据拷贝次数从缓冲 I/O3 次,到直接 I/O2 次,再到内存映射文件,变成了 1 次。

  • 读:磁盘 ——> 内核缓冲区
  • 写: 内核缓冲区 ——> 磁盘。

img

2、零拷贝

零拷贝(Zero Copy)是提升I/O效率的一大利器,熟悉 KafkaNetty 等原理的都知道,其实现原理就是通过零拷贝技术来实现读写性能的。

  • 实现方法1:利用直接I/O

当我们把数据发送到网络中时,如果不使用零拷贝。 伪代码:

1
2
3
4
5
fd1 = 打开文件描述符
fd2 = 打开Socket描述符
buffer = 应用程序内存
read(fd1, buffer) //先把数据从文件读取到应用程序内存
write(fd2, buffer) //再把应用内存中的数据写入到网络中发出

如下图所示:整个文件会有4次数据拷贝,读文件2次,写网络2次。

磁盘 ——> 内核缓冲区 ——> 应用程序内存 ——> Socket缓冲区 ——> 网络

img

  • 实现方法2:利用内存映射文件

    此种方式,整个过程会有3次数据拷贝,不再经过应用程序内存,直接把内核空间中从内核缓冲区拷贝到Socket缓冲区。,伪代码如下:

1
2
3
4
5
fd1 = 打开文件描述符
fd2 = 打开Socket描述符
buffer = 应用程序内存
mmap(fd1, buffer) //先把磁盘数据映射到buffer上
write(fd2, buffer) //再通过网络发送数据

如下图所示:

img

注意:在这里需要分清 映射 拷贝 的区别。

​ 拷贝:是把数据从一块内存中复制到另外一块内存里;

​ 映射:相当于只是持有了数据的一个引用(或者叫地址),数据本身只有1分。

  • 实现方式3:利用零拷贝技术

如果使用零拷贝,可能连内核缓冲区到 Socket 缓冲区的拷贝也省略了。在内核缓冲区和 Socket 缓冲区之间并没有做数据拷贝,只是一个地址的映射,底层的网卡驱动程序要读去数据并发送到网络的时候,看似读取的是Socket缓冲区的数据,但实际上直接读的是内核缓冲区的数据。

img

在这里,我们看到虽然叫零拷贝,实际是2次数据拷贝,1次是从磁盘到内核缓冲区,1次是从内核缓冲区到网络。之所以叫零拷贝,是从内存的角度来看的,数据在内存中没有发生过数据拷贝,只是在内存和I/O之间传输

说明:
对于把文件数据发送到网络的场景,直接I/O、内存映射文件、零拷贝对应的数据拷贝次数分别是4次、3次、2次,内存拷贝次数分别是2次、1次、0次。

--------------------------本文结束感谢您的阅读--------------------------
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!