浅谈零拷贝机制

1,770 阅读6分钟

预备知识

我相信来看本文章的应该都对操作系统有了一些了解,不过在谈零拷贝之前,还需要讲一些别的东西,避免到时候大家看的晕乎

  • 内核态和用户态:这两种不同的状态分别赋予进程不同的权限,内核态下可以访问内存的所有数据,能够访问外围设备;而用户态下则只能访问受限的内存。所以如果一个进程想要执行深度操作,就需要涉及用户态到内核态的切换
  • 系统调用:系统调用不是一种调用行为,可以理解成操作系统开放的编程接口,通过使用“系统调用”接口,可以达到一些在用户态下无法完成的行为,当然,会涉及到用户态到内核态的状态切换
  • 缓冲区:缓冲区是内存中的一块区域,是IO操作的基础。任何IO操作都可以理解为数据在缓冲区之间的拷贝,用户空间和内核空间都有对应的缓冲区。操作系统不能直接将数据从磁盘拷贝到用户空间的缓冲区中
  • DMA:DMA即Direct Memory Access,负责将数据从一个地址空间转移到另一个地址空间,传输动作由CPU初始化,但由DMA来执行。在DMA执行操作期间,CPU可以处理其他的事情
  • 虚拟内存:内存地址分为虚拟内存地址和物理内存地址两种,进程可见的是虚拟内存地址,操作系统会将虚拟内存地址映射到物理内存上,虚拟内存是连续的,但是物理内存可以是不连续的

零拷贝解决什么问题

在讲零拷贝之前,先得明白拷贝是什么。这里的拷贝指“I/O操作时,数据在缓冲区之间的拷贝”。不明白没关系,我们先来看linux中的将数据从磁盘读取,并通过网络发送出去的一整个过程是怎样的:

  1. DMA将数据从磁盘拷贝到内核缓冲区中

  2. cpu将数据从内核缓冲区拷贝到用户缓冲区中(读过程结束,进程可以从用户缓冲区直接读取数据)

  3. cpu将数据从用户缓冲区拷贝到内核socket缓冲区中

  4. DMA将数据从socket缓冲区发送到协议引擎中(写过程结束,数据被协议引擎通过网络发送)

这里借用一下理解零拷贝原理中的图:

整个读写的过程大致是这样的,涉及2次用户态和内核态的状态切换,2次DMA拷贝,和2次cpu拷贝

我们同样也发现,在一个读写过程中,第2步和第3步仿佛在做“无用功”,数据如果直接从内核缓冲拷贝到Socket缓冲就可以了,为什么还要通过用户缓冲中转呢?没错,操作系统的开发者也意识到了这个问题,所以零拷贝就是为了解决这个问题,所以利用零拷贝机制,可以避免状态切换,并减少了cpu的拷贝次数

零拷贝的实现方式

虽然看起来只需要允许数据从内核缓冲拷贝到socket缓冲,即可解决数据拷贝的问题,但是具体却有很多种实现方式,我们来一一介绍一下

sendFile

sendFile方式的系统调用为sendfile system call,也是最经典的零拷贝解决方案。采用sendFile方式的的读写过程为:

  1. DMA将数据从磁盘拷贝到内核缓冲
  2. cpu将数据从内核缓冲拷贝到内核socket缓冲
  3. DMA将数据从socket缓冲拷贝到协议引擎中

这种标准方式就不用过多介绍了,就是我们在刚才提到的解决方案,接下来我们看优化版本的sendFile的读写过程是怎样的:

  1. DMA将数据从磁盘拷贝到内核缓冲
  2. cpu将描述符[1]从内核缓冲拷贝到socket缓冲
  3. DMA将数据从socket缓冲拷贝到协议引擎中

在优化的sendFile方式中,第2步不再是拷贝数据,而是拷贝描述符,这样就真正实现了数据的零拷贝

mmap

mmap,也可以成为内存映射,效果比传统的I/O要好,但是代价比sendFile要大。我们来看mmap方式下的读写操作:

  1. DMA将数据从磁盘拷贝到内核缓冲/用户缓冲
  2. cpu将数据从内核缓冲/用户缓冲拷贝到socket缓冲
  3. DMA将数据从socket缓冲拷贝到协议引擎中

这里画了个斜线,表示内核缓冲和用户缓冲是同一块区域,mmap采用虚拟内存映射,虽然进程认为自己的用户缓冲区域是内存中独立的区域,但是实际上用户缓冲和内核缓冲指向的同一块区域,这种方式也能避免数据拷贝问题

FileChannel

FileChannel是Java NIO的解决方案,提供transferTo/transferFrom接口,用于将一个通道连接到另一个通道,中间就避免了不必要的数据拷贝

更严格地说,FileChannel并不能算是零拷贝的一种解决方案,实际上还是需要依赖操作系统的sendFile

Netty Zero-Copy

Netty中在FileRegion封装了Java NIO的transferTo/transferFrom,也可以实现零拷贝,但是这不是重点,Netty还提供了另一种形式的零拷贝,也是我们学习的重点 在数据传输时,通常一份完整的消息数据会被切分成多个数据包进行传输,而这些单个的数据包是没有意义的,只有组合在一起才有具体的含义,才能被程序进行处理。Netty可以通过零拷贝的方式,将这些数据组合成完成的消息来供使用,减少数据拷贝次数,此时零拷贝的作用范围仅局限于用户空间

同时,Netty可以直接在堆外分配内存,避免了数据从堆内拷贝到堆外的过程

总结

零拷贝的这些方式中,sendFile虽然看起来最美好,但是如果我们的应用在读取数据后还需要进行更改的话,这种方式就不适用了,就需要使用代价更高的mmap内存映射方式

零拷贝的内容大致就是这些了,如果想更为深入地去学习零拷贝知识的话,可以去学习一下netty的源码,还是很有必要的


  1. 这里我不敢确定是内存描述符还是数据描述符,我倾向于内存描述符,是一个类似指针的数据,如果我这里理解错误欢迎指正 ↩︎