程序员必知必会的零拷贝技术

传统IO过程考虑这样一个过程:我们从磁盘中读取一个文件数据 , 然后将数据通过网络传输到另一个机器 。对用户来说可能就是简单的理解为两步操作 。
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
但是 , 如果我们看传输中涉及的内核部分的内部工作原理 , 我们将看到
即使是使用DMA传输的硬件支持 , 这种方法也效率很低 。首先 , 内核将使用DMA将磁盘中的数据加载到其自己的内核缓冲区中 , 除非在先前访问同一文件之后 , 该数据仍被缓存在内核缓冲区中 。
这样传输不需要太多的CPU工作 , CPU只需要进行缓冲区管理和DMA创建和处理 。linux 操作系统会根据 read() 系统调用指定的应用程序地址空间的地址 , 把这块数据存放到请求这块数据的应用程序的地址空间中去 , 在接下来的处理过程中 , 操作系统需要将数据再一次从用户应用程序地址空间的缓冲区拷贝到与网络堆栈相关的内核缓冲区中去 , 这个过程也是需要占用 CPU 的 。
数据拷贝操作结束以后 , 数据会被打包 , 然后发送到网络接口卡上去 。在数据传输的过程中 , 应用程序可以先返回进而执行其他的操作 。
之后 , 在调用 write() 系统调用的时候 , 用户应用程序缓冲区中的数据内容可以被安全的丢弃或者更改 , 因为操作系统已经在内核缓冲区中保留了一份数据拷贝 , 当数据被成功传送到硬件上之后 , 这份数据拷贝就可以被丢弃 。
所以我们会发现这个过程涉及到了3次上下文切换 , 和4次数据拷贝的过程:

程序员必知必会的零拷贝技术

文章插图
 
利用mmap()在 Linux 中 , 减少拷贝次数的一种方法是调用 mmap() 来代替调用 read , 比如:
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);
首先 , 应用程序调用了 mmap() 之后 , 数据会先通过 DMA 拷贝到操作系统内核的缓冲区中去 。接着 , 应用程序跟操作系统共享这个缓冲区 , 这样 , 操作系统内核和应用程序存储空间就不需要再进行任何的数据拷贝操作 。应用程序调用了 write() 之后 , 操作系统内核将数据从原来的内核缓冲区中拷贝到与 socket 相关的内核缓冲区中 。接下来 , 数据从内核 socket 缓冲区拷贝到协议引擎中去 , 这是第三次数据拷贝操作
程序员必知必会的零拷贝技术

文章插图
 
尽管mmap()可以减少一次 I/O 拷贝 , 但由于mmap()的实现很复杂 , 调用mmap()将会带来额外的开销 , 因此在一些情况下 , 没有使用mmap()的必要:
访问小文件时 , 直接使用read()或write()将更加高效 。
单个进程对文件执行顺序访问时(sequential access) , 使用mmap()几乎不会带来性能上的提升 。譬如说 , 使用read()顺序读取文件时 , 文件系统会使用 read-ahead 的方式提前将文件内容缓存到文件系统的缓冲区 , 因此使用read()将很大程度上可以命中缓存 。
那么 , 在什么情况下使用mmap()去访问文件会更高效呢?
对文件执行随机访问时 , 如果使用read()或write() , 则意味着较低的 cache 命中率 。这种情况下使用mmap()通常将更高效 。
多个进程同时访问同一个文件时(无论是顺序访问还是随机访问) , 如果使用mmap() , 那么 OS 缓冲区的文件内容可以在多个进程之间共享 , 从操作系统角度来看 , 使用mmap()可以大大节省内存 。
程序员必知必会的零拷贝技术

文章插图
 
sendfile()为了简化用户接口 , 同时还要继续保留 mmap()/write() 技术的优点:减少 CPU 的拷贝次数 , Linux 在版本 2.1 中引入了 sendfile() 这个系统调用 。
sendfile(sockfd, fd, NULL, len);
sendfile() 不仅减少了数据拷贝操作 , 它也减少了上下文切换 。首先:sendfile() 系统调用利用 DMA 引擎将文件中的数据拷贝到操作系统内核缓冲区中 , 然后数据被拷贝到与 socket 相关的内核缓冲区中去 。接下来 , DMA 引擎将数据从内核 socket 缓冲区中拷贝到协议引擎中去 。


推荐阅读