sendfile函数在两个文件描述符之间传递数据(完全在内核中操作),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,被称为零拷贝。函数定义为:
#include<sys/sendfile.h>
ssize_t sendfile(int out_fd,int in_fd,off_t* offset,size_t count);
in_fd参数是待读出内容的文件描述符,out_fd参数是待写入内容的文件描述符。offset参数指定从读入文件流的哪个位置开始读,如果为空,则使用读入文件流默认的起始位置。count参数指定文件描述符in_fd和out_fd之间传输的字节数。
in_fd必须是一个支持类似mmap函数的文件描述符,即它必须指向真实的文件,不能是socket和管道,而out_fd必须是一个socket
sendfile
系统调用函数体在fs/read_write.c
文件中
SYSCALL_DEFINE4(sendfile, int, out_fd, int, in_fd, off_t __user *, offset, size_t, count)
{
loff_t pos;
off_t off;
ssize_t ret;
if (offset) {
if (unlikely(get_user(off, offset)))
return -EFAULT;
pos = off;
ret = do_sendfile(out_fd, in_fd, &pos, count, MAX_NON_LFS);
if (unlikely(put_user(pos, offset)))
return -EFAULT;
return ret;
}
return do_sendfile(out_fd, in_fd, NULL, count, 0);
}
首先我们来看看传统的read/write方式进行socket的传输。
当需要对一个文件进行传输的时候,具体流程细节如下:
1:调用read函数,文件数据copy到内核缓冲区
2:read函数返回,文件数据从内核缓冲区copy到用户缓冲区
3:write函数调用,将文件数据从用户缓冲区copy到内核与socket相关的缓冲区
4:数据从socket缓冲区copy到相关协议引擎。
在这个过程中发生了四次copy操作:
硬盘->内核->用户->socket缓冲区(内核)->协议引擎。
sendfile的工作原理
1、系统调用 sendfile() 通过 DMA 把硬盘数据拷贝到 kernel buffer,然后数据被 kernel 直接拷贝到另外一个与 socket 相关的 kernel buffer。这里没有 用户态和核心态 之间的切换,在内核中直接完成了从一个 buffer 到另一个 buffer 的拷贝。
2、DMA 把数据从 kernel buffer 直接拷贝给协议栈,没有切换,也不需要数据从用户态和核心态,因为数据就在 kernel 里。
测试代码:
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>
#include<assert.h>
#include<stdio.h>
#include<string.h>
#include<sys/sendfile.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<errno.h>
int main(int argc,char *argv[])
{
if(argc<=3)
{
printf("usage:%s ip_address port_number filename\n",basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
const char* file_name = argv[3];
int filefd = open(file_name,O_RDONLY);
assert(filefd>0);
struct stat stat_buf;
fstat(filefd,&stat_buf);
struct sockaddr_in address;
bzero(&address,sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET,ip,&address.sin_addr);
address.sin_port = htons(port);
int sock = socket(PF_INET,SOCK_STREAM,0);
assert(sock>=0);
int ret = bind(sock,(struct sockaddr*)&address,sizeof(address));
assert(ret!=-1);
ret = listen(sock,5);
assert(ret!=-1);
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
int connfd = accept(sock,(struct sockaddr*)&client,&client_addrlength);
if(connfd<0)
{
printf("errno is %d\n",errno);
}
else
{
sendfile(connfd,filefd,NULL,stat_buf.st_size);
close(connfd);
}
close(sock);
return 0;
}
然后进行
在另外一个虚拟机上telnet