可算是有文章,把Linux零拷贝技术讲透彻了

本文探讨linux中 主要的几种零拷贝技术 以及零拷贝技术 适用的场景。为了迅速建立起零拷贝的概念,我们拿一个常用的场景进行引入:
01 引文在写一个服务端程序时(Web Server或者文件服务器),文件下载是一个基本功能 。这时候服务端的任务是:将服务端主机磁盘中的文件不做修改地从已连接的socket发出去,我们通常用下面的代码完成:
while((n = read(diskfd, buf, BUF_SIZE)) > 0)write(sockfd, buf , n);基本操作就是循环的从磁盘读入文件内容到缓冲区,再将缓冲区的内容发送到socket 。但是由于Linux的I/O操作默认是缓冲I/O 。这里面主要使用的也就是read和write两个系统调用,我们并不知道操作系统在其中做了什么 。实际上在以上I/O操作中,发生了多次的数据拷贝 。
当应用程序访问某块数据时,操作系统首先会检查,是不是最近访问过此文件,文件内容是否缓存在内核缓冲区,如果是,操作系统则直接根据read系统调用提供的buf地址,将内核缓冲区的内容拷贝到buf所指定的用户空间缓冲区中去 。如果不是,操作系统则首先将磁盘上的数据拷贝的内核缓冲区,这一步目前主要依靠DMA来传输,然后再把内核缓冲区上的内容拷贝到用户缓冲区中 。
接下来,write系统调用再把用户缓冲区的内容拷贝到网络堆栈相关的内核缓冲区中,最后socket再把内核缓冲区的内容发送到网卡上 。说了这么多,不如看图清楚:

可算是有文章,把Linux零拷贝技术讲透彻了

文章插图
 
数据拷贝
从上图中可以看出,共产生了四次数据拷贝,即使使用了DMA来处理了与硬件的通讯,CPU仍然需要处理两次数据拷贝,与此同时,在用户态与内核态也发生了多次上下文切换,无疑也加重了CPU负担 。
在此过程中,我们没有对文件内容做任何修改,那么在内核空间和用户空间来回拷贝数据无疑就是一种浪费,而零拷贝主要就是为了解决这种低效性 。
02 什么是零拷贝技术(zero-copy)?零拷贝主要的任务就是避免CPU将数据从一块存储拷贝到另外一块存储,主要就是利用各种零拷贝技术,避免让CPU做大量的数据拷贝任务,减少不必要的拷贝,或者让别的组件来做这一类简单的数据传输任务,让CPU解脱出来专注于别的任务 。这样就可以让系统资源的利用更加有效 。
我们继续回到引文中的例子,我们如何减少数据拷贝的次数呢?一个很明显的着力点就是减少数据在内核空间和用户空间来回拷贝,这也引入了零拷贝的一个类型:
让数据传输不需要经过 user space 。
03 使用 mmap我们减少拷贝次数的一种方法是调用mmap()来代替read调用:
buf = mmap(diskfd, len);write(sockfd, buf, len);应用程序调用mmap(),磁盘上的数据会通过DMA被拷贝的内核缓冲区,接着操作系统会把这段内核缓冲区与应用程序共享,这样就不需要把内核缓冲区的内容往用户空间拷贝 。应用程序再调用write(),操作系统直接将内核缓冲区的内容拷贝到socket缓冲区中,这一切都发生在内核态,最后,socket缓冲区再把数据发到网卡去 。同样的,看图很简单:
可算是有文章,把Linux零拷贝技术讲透彻了

文章插图
 
mmap
使用mmap替代read很明显减少了一次拷贝,当拷贝数据量很大时,无疑提升了效率 。但是使用mmap是有代价的 。当你使用mmap时,你可能会遇到一些隐藏的陷阱 。例如,当你的程序map了一个文件,但是当这个文件被另一个进程截断(truncate)时, write系统调用会因为访问非法地址而被SIGBUS信号终止 。SIGBUS信号默认会杀死你的进程并产生一个coredump,如果你的服务器这样被中止了,那会产生一笔损失 。
通常我们使用以下解决方案避免这种问题:
1. 为SIGBUS信号建立信号处理程序
当遇到SIGBUS信号时,信号处理程序简单地返回,write系统调用在被中断之前会返回已经写入的字节数,并且errno会被设置成success,但是这是一种糟糕的处理办法,因为你并没有解决问题的实质核心 。
2. 使用文件租借锁
通常我们使用这种方法,在文件描述符上使用租借锁,我们为文件向内核申请一个租借锁,当其它进程想要截断这个文件时,内核会向我们发送一个实时的RTSIGNALLEASE信号,告诉我们内核正在破坏你加持在文件上的读写锁 。这样在程序访问非法内存并且被SIGBUS杀死之前,你的write系统调用会被中断 。write会返回已经写入的字节数,并且置errno为success 。
我们应该在mmap文件之前加锁,并且在操作完文件后解锁:


推荐阅读