sendfile函数在两个文件描述符之间直接传递数据,完全在内核操作,从而避免了内核缓冲区和用户缓冲区的数据拷贝,效率很高,被称为零拷贝。
ssize_t sendfile(int out_fd , int in_fd ,off_t* offset ,size_t count );
out_fd 是待写入内容的文件描述符,它必须是一个socket
in_fd 是待读出内容的文件描述符,它必须是一个支持类似mmap函数的文件描述符,即它必须指向真实的文件,不能是socket和管道
可以看出这个函数几乎就是专门为在网络上传输文件设置的,有socket,有文件描述符
offset参数指定从读入文件流的哪个位置开始读,如果为空,则使用读入文件流默认的起始位置
count参数指定在out_fd 和in_fd之间传输的字节数
成功返回传输的字节数,失败返回-1并设置errno
与read和write系统调用函数的比较:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
如果我们使用read和write函数,需要进行如下几个步骤:
步骤一:系统调用read导致了从用户空间到内核空间的上下文切换。DMA模块(直接内存访问,是一种不经过CPU而直接从内存存取数据的数据交换模式。在DMA模式下,CPU只须向DMA控制器下达指令,让DMA控制器来处理数据的传送,数据传送完毕再把信息反馈给CPU)从磁盘中读取文件内容,并将其存储在内核空间的缓冲区内,完成了第1次复制。
步骤二:数据从内核空间缓冲区复制到用户空间缓冲区,之后系统调用read返回,这导致了从内核空间向用户空间的上下文切换。此时,需要的数据已存放在指定的用户空间缓冲区内(参数tmp_buf),程序可以继续下面的操作。
步骤三:系统调用write导致从用户空间到内核空间的上下文切换。数据从用户空间缓冲区被再次复制到内核空间缓冲区,完成了第3次复制。不过,这次数据存放在内核空间中与使用的socket相关的特定缓冲区中,而不是步骤一中的缓冲区。
步骤四:系统调用返回,导致了第4次上下文切换。第4次复制在DMA模块将数据从内核空间缓冲区传递至协议引擎的时候发生,这与我们的代码的执行是独立且异步发生的。
而使用sendfile函数,需要进行如下几个步骤:
步骤一:sendfile系统调用导致文件内容通过DMA模块被复制到某个内核缓冲区,之后再被复制到与socket相关联的缓冲区内。
步骤二:当DMA模块将位于socket相关联缓冲区中的数据传递给协议引擎时,执行第3次复制。
对比我们可以得出,相比使用sendfile函数,Read & Write方式带来的性能损耗主要有两点:
1. 不必要的内存拷贝。即多了一次从内核缓冲区到用户缓冲区的数据拷贝。
2. 系统调用带来的额外的用户态/内核态上下文切换。我们知道,使用系统调用会发生从内核态到用户态之间的相互转换。使用read&write方式,多使用了一次系统调用,就多了一次上下文切换,降低了性能。
sendfile函数可能不属于真正意义的零拷贝,但是它减少切换次数和拷贝次数,总体来说是一个非常高效的数据拷贝函数,尤其在传输一个非常大的文件时,sendfile函数的高性能体现的更为明显。所以,一般网络传输文件时,应该使用sendfile函数,提高效率。
参考:https://blog.csdn.net/eydwyz/article/details/76637947