程序员|为什么 P8 程序员的代码你写不出来?零拷贝了解一下( 二 )



  1. read函数会涉及一次用户态到内核态的切换 , 操作系统会向磁盘发起一次IO请求 , 当数据准备好后通过DMA技术把数据拷贝到内核的buffer中 , 注意本次数据拷贝无需CPU参与 。
  2. 此后操作系统开始把这块数据从内核拷贝到用户态的buffer中 , 此时read()函数返回 , 并从内核态切换回用户态 , 到这时read(fileDesc buf len);这行代码就返回了 , buf中装好了新鲜出炉的数据 。
  3. 接下来send函数再次导致用户态与内核态的切换 , 此时数据需要从用户态buf拷贝到网络协议子系统的buf中 , 具体点该buf属于在代码中使用的这个socket 。
  4. 此后send函数返回 , 再次由内核态返回到用户态;此时在程序员看来数据已经成功发出去了 , 但实际上数据可能依然停留在内核中 , 此后第四次数据copy开始 , 利用DMA技术把数据从socket buf拷贝给网卡 , 然后真正的发送出去 。
这就是看似简单的这两行代码在底层的完整过程 。
你觉得这个过程有什么问题吗?
发现问题有的同学肯定已经注意到了 , 既然在用户态没有对数据进行任何修改 , 那为什么要这么麻烦地让数据在用户态来个一日游呢?直接在内核态从磁盘给到网卡不就可以了吗?
恭喜你 , 答对了!
这种优化思路就是所谓的零拷贝技术 , Zero Copy 。
总体上来看 , 优化数据拷贝会有以下三个方向:
  1. 用户态不需要真正的去访问数据 , 就像上面这个示例 , 用户态根本不需要知道buf里面装的是什么 。 在这种情况下无需把数据从内核态拷贝到用户态然后再把数据从用户态拷贝回内核态 。 数据无需用户态感知 , 数据拷贝完全发生在内核态 。
  2. 内核态不要真正的去访问数据 , 用户态程序可以绕过内核直接和硬件交互 , 这样就避免了内核的参与 , 从而减少数据拷贝的可能 。 内核无需感知数据 。
  3. 如果内核态和用户态不得不进行数据交互 , 则优化用户态与内核态数据的交互方式 。
知道了解决问题的思路 , 我们来看下为了实现零拷贝 , 计算机系统中都有哪些巧妙的设计 。
mmap是的 , 就是mmap , 你能想到mmap还可以实现零拷贝吗?
对于本文提到的网络服务器我们可以这样修改代码:
buf = mmap(file len);write(socket buf len);

你可能会想仅仅将read替换为mmap会有什么优化吗?
如果你真的理解了mmap就会知道 , mmap仅仅将文件内容映射到了进程地址空间中 , 并没有真正的拷贝到进程地址空间 , 这节省了一次从内核态到用户态的数据拷贝 。
同样的 , 当调用write时数据直接从内核buf拷贝给了socket buf , 而不是像read/write方法中把用户态数据拷贝给socket buf 。
【程序员|为什么 P8 程序员的代码你写不出来?零拷贝了解一下】
我们可以看到 , 利用mmap我们节省了一次数据拷贝 , 上下文切换依然是四次 。

尽管mmap可以节省数据拷贝 , 但维护文件与地址空间的映射关系也是有代价的 , 除非CPU拷贝数据的时间超过维系映射关系的代价 , 否则基于mmap的程序性能可能不及传统的read/write 。
此外 , 如果映射的文件被其它进程截断 , 在Linux系统下你的进程将立即接收到SIGBUS信号 , 因此这种异常情况也需要正确处理 。
除了mmap之外 , 还有其它办法也可以实现零拷贝 。
sendfile你没有看错 , 在Linux系统下为了解决数据拷贝问题专门设计了这一系统调用:
#include <sys/sendfile.h>ssize_t sendfile(int out_fd int in_fd off_t *offset size_t count);