【干货】可算是有文章,把 Linux 零拷贝技术讲透彻了!( 二 )


我们应该在mmap文件之前加锁 , 并且在操作完文件后解锁:
if(fcntl(diskfd,F_SETSIG,RT_SIGNAL_LEASE)==-1){perror("kernelleasesetsignal");return-1;}/*l_typecanbeF_RDLCKF_WRLCK加锁*//*l_typecanbeF_UNLCK解锁*/if(fcntl(diskfd,F_SETLEASE,l_type)){perror("kernelleasesettype");return-1;}使用sendfile
从2.1版内核开始 , Linux引入了sendfile来简化操作:
#include<sys/sendfile.h>ssize_tsendfile(intout_fd,intin_fd,off_t*offset,size_tcount);系统调用sendfile()在代表输入文件的描述符infd和代表输出文件的描述符outfd之间传送文件内容(字节) 。 描述符outfd必须指向一个套接字 , 而infd指向的文件必须是可以mmap的 。 这些局限限制了sendfile的使用 , 使sendfile只能将数据从文件传递到套接字上 , 反之则不行 。
使用sendfile不仅减少了数据拷贝的次数 , 还减少了上下文切换 , 数据传送始终只发生在kernelspace 。

【干货】可算是有文章,把 Linux 零拷贝技术讲透彻了!
文章图片
sendfile系统调用过程
在我们调用sendfile时 , 如果有其它进程截断了文件会发生什么呢?假设我们没有设置任何信号处理程序 , sendfile调用仅仅返回它在被中断之前已经传输的字节数 , errno会被置为success 。 如果我们在调用sendfile之前给文件加了锁 , sendfile的行为仍然和之前相同 , 我们还会收到RTSIGNALLEASE的信号 。
目前为止 , 我们已经减少了数据拷贝的次数了 , 但是仍然存在一次拷贝 , 就是页缓存到socket缓存的拷贝 。 那么能不能把这个拷贝也省略呢?
借助于硬件上的帮助 , 我们是可以办到的 。 之前我们是把页缓存的数据拷贝到socket缓存中 , 实际上 , 我们仅仅需要把缓冲区描述符传到socket缓冲区 , 再把数据长度传过去 , 这样DMA控制器直接将页缓存中的数据打包发送到网络中就可以了 。
总结一下 , sendfile系统调用利用DMA引擎将文件内容拷贝到内核缓冲区去 , 然后将带有文件位置和长度信息的缓冲区描述符添加socket缓冲区去 , 这一步不会将内核中的数据拷贝到socket缓冲区中 , DMA引擎会将内核缓冲区的数据拷贝到协议引擎中去 , 避免了最后一次拷贝 。

【干货】可算是有文章,把 Linux 零拷贝技术讲透彻了!
文章图片
带DMA的sendfile
不过这一种收集拷贝功能是需要硬件以及驱动程序支持的 。
使用splice
sendfile只适用于将数据从文件拷贝到套接字上 , 限定了它的使用范围 。 Linux在2.6.17版本引入splice系统调用 , 用于在两个文件描述符中移动数据:
#define_GNU_SOURCE/*Seefeature_test_macros(7)*/#include<fcntl.h>ssize_tsplice(intfd_in,loff_t*off_in,intfd_out,loff_t*off_out,size_tlen,unsignedintflags);splice调用在两个文件描述符之间移动数据 , 而不需要数据在内核空间和用户空间来回拷贝 。 他从fdin拷贝len长度的数据到fdout , 但是有一方必须是管道设备 , 这也是目前splice的一些局限性 。 flags参数有以下几种取值:
SPLICEFMOVE:尝试去移动数据而不是拷贝数据 。 这仅仅是对内核的一个小提示:如果内核不能从pipe移动数据或者pipe的缓存不是一个整页面 , 仍然需要拷贝数据 。 Linux最初的实现有些问题 , 所以从2.6.21开始这个选项不起作用 , 后面的Linux版本应该会实现 。 SPLICEFNONBLOCK:splice操作不会被阻塞 。 然而 , 如果文件描述符没有被设置为不可被阻塞方式的I/O , 那么调用splice有可能仍然被阻塞 。 SPLICEFMORE:后面的splice调用会有更多的数据 。splice调用利用了Linux提出的管道缓冲区机制 , 所以至少一个描述符要为管道 。
以上几种零拷贝技术都是减少数据在用户空间和内核空间拷贝技术实现的 , 但是有些时候 , 数据必须在用户空间和内核空间之间拷贝 。 这时候 , 我们只能针对数据在用户空间和内核空间拷贝的时机上下功夫了 。 Linux通常利用写时复制(copyonwrite)来减少系统开销 , 这个技术又时常称作COW 。
由于篇幅原因 , 本文不详细介绍写时复制 。 大概描述下就是:如果多个程序同时访问同一块数据 , 那么每个程序都拥有指向这块数据的指针 , 在每个程序看来 , 自己都是独立拥有这块数据的 , 只有当程序需要对数据内容进行修改时 , 才会把数据内容拷贝到程序自己的应用空间里去 , 这时候 , 数据才成为该程序的私有数据 。 如果程序不需要对数据进行修改 , 那么永远都不需要拷贝数据到自己的应用空间里 。 这样就减少了数据的拷贝 。 写时复制的内容可以再写一篇文章了 。。。
除此之外 , 还有一些零拷贝技术 , 比如传统的LinuxI/O中加上O_DIRECT标记可以直接I/O , 避免了自动缓存 , 还有尚未成熟的fbufs技术 , 本文尚未覆盖所有零拷贝技术 , 只是介绍常见的一些 , 如有兴趣 , 可以自行研究 , 一般成熟的服务端项目也会自己改造内核中有关I/O的部分 , 提高自己的数据传输速率 。