零拷贝

零拷贝

传统 IO

基于传统的 IO 方式,底层实际上通过调用 read()和 write()来实现。基本操作就是循环的从磁盘读入文件内容到缓冲区,再将缓冲区的内容发送到 Socket。但是由于 Linux 的 IO 操作默认是缓冲 I/O,在这两个系统调用之后,数据已经被复制了至少四次,并且几乎执行了同样次数的用户/内核空间的上下文切换。

while((n = read(diskfd, buf, BUF_SIZE)) > 0)
    write(sockfd, buf, n);

传统 Linux IO 流程示意图

整个过程发生了 4 次用户态和内核态的上下文切换和 4 次拷贝,具体流程如下:

  • read 系统调用导致上下文从用户模式切换到内核模式。第一个副本由 DMA 引擎执行,DMA 引擎从磁盘读取文件内容并将它们存储到内核地址空间缓冲区中。
  • 将数据从内核缓冲区复制到用户缓冲区,并且 read 系统调用返回。read 调用返回导致上下文从内核切换回用户模式。现在数据存储在用户地址空间缓冲区中。
  • write 系统调用导致上下文从用户模式切换到内核模式。执行第三次复制,以再次将数据放入内核地址空间缓冲区。这个时候,数据被放入一个不同的缓冲区,一个与 sockets 相关联的缓冲区。
  • 写系统调用返回,创建我们的第四个上下文切换。独立和异步地,当 DMA 引擎将数据从内核缓冲区传递到协议引擎时,发生第四次复制。

很多数据复制操作并不是真正需要的,可以消除一些复制操作以减少开销并提高性能,在此过程中,我们没有对文件内容做任何修改,那么在内核空间和用户空间来回拷贝数据无疑就是一种浪费,而零拷贝主要就是为了解决这种低效性。零拷贝主要的任务就是避免 CPU 将数据从一块存储拷贝到另外一块存储,主要就是利用各种零拷贝技术,避免让 CPU 做大量的数据拷贝任务,减少不必要的拷贝,或者让别的组件来做这一类简单的数据传输任务,让 CPU 解脱出来专注于别的任务。这样就可以让系统资源的利用更加有效。

COW

零拷贝技术都是减少数据在用户空间和内核空间拷贝技术实现的,但是有些时候,数据必须在用户空间和内核空间之间拷贝。这时候,我们只能针对数据在用户空间和内核空间拷贝的时机上下功夫了。Linux 通常利用写时复制(copy on write)来减少系统开销,这个技术又时常称作 COW。

如果多个程序同时访问同一块数据,那么每个程序都拥有指向这块数据的指针,在每个程序看来,自己都是独立拥有这块数据的,只有当程序需要对数据内容进行修改时,才会把数据内容拷贝到程序自己的应用空间里去,这时候,数据才成为该程序的私有数据。如果程序不需要对数据进行修改,那么永远都不需要拷贝数据到自己的应用空间里。这样就减少了数据的拷贝。

Links