首页 > C/C++ > 网络编程 > sendfile零拷贝高效率发送文件
2017
01-11

sendfile零拷贝高效率发送文件

本文介绍使用sendfile函数进行零拷贝发送文件,实现高效数据传输,并对其进行验证。

那么什么是sendfile呢?

Linux系统使用man sendfile,查看sendfile原型如下:

#include <sys/sendfile.h>

       ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);


参数说明:
out_fd 是已经打开了,用于写操作(write)的文件描述符;
in_fd 是已经打开了,用于读操作(read)的文件描述符;
offset 偏移量;表示sendfile函数从in_fd中的哪一偏移量开始读取数据.如果是零表示从文件的开始读,否则从相应的便宜量读取.如果是循环读取的时候,下一次offset值应为sendfile函数返回值加上本次的offset的值.
count是在两个描述符之间拷贝的字节数(bytes)

参数特别注意的是:in_fd必须是一个支持mmap函数的文件描述符,也就是说必须指向真实文件,不能使socket描述符和管道。

out_fd必须是一个socket描述符。

由此可见sendfile几乎是专门为在网络上传输文件而设计的。

Sendfile 函数在两个文件描述符之间直接传递数据(完全在内核中操作,传送),从而避免了内核缓冲区数据和用户缓冲区数据之间的拷贝,操作效率很高,被称之为零拷贝。

传统方式read/write send/recv 
在传统的文件传输里面(read/write方式),在实现上其实是比较复杂的,需要经过多次上下文的切换,我们看一下如下两行代码:    
1. read(file, tmp_buf, len);        

2. write(socket, tmp_buf, len);   

以上两行代码是传统的read/write方式进行文件到socket的传输。    

当需要对一个文件进行传输的时候,其具体流程细节如下:   

1、调用read函数,文件数据被copy到内核缓冲区  

2、read函数返回,文件数据从内核缓冲区copy到用户缓冲区 

3、write函数调用,将文件数据从用户缓冲区copy到内核与socket相关的缓冲区。

 4、数据从socket缓冲区copy到相关协议引擎。    

以上细节是传统read/write方式进行网络文件传输的方式,我们可以看到,

在这个过程当中,文件数据实际上是经过了四次copy操作:    硬盘—>内核buf—>用户buf—>socket相关缓冲区(内核)—>协议引擎

 

新方式sendfile  

sendfile系统调用则提供了一种减少以上多次copy,提升文件传输性能的方法。

1、系统调用 sendfile() 通过 DMA 把硬盘数据拷贝到 kernel buffer,然后数据被 kernel 直接拷贝到另外一个与 socket 相关的 kernel buffer。这里没有 user mode 和 kernel mode 之间的切换,在 kernel 中直接完成了从一个 buffer 到另一个 buffer 的拷贝。
2、DMA 把数据从 kernel buffer 直接拷贝给协议栈,没有切换,也不需要数据从 user mode 拷贝到 kernel mode,因为数据就在 kernel 里。

 

服务端:

  1. #include <sys/socket.h>  
  2. #include <netinet/in.h>  
  3. #include <arpa/inet.h>  
  4. #include <assert.h>  
  5. #include <stdio.h>  
  6. #include <unistd.h>  
  7. #include <stdlib.h>  
  8. #include <errno.h>  
  9. #include <string.h>  
  10. #include <sys/types.h>  
  11. #include <sys/stat.h>  
  12. #include <fcntl.h>  
  13. #include <sys/sendfile.h>  
  14.   
  15. int main( int argc, char* argv[] )  
  16. {  
  17.     if( argc <= 3 )  
  18.     {  
  19.         printf( "usage: %s ip_address port_number filename\n", basename( argv[0] ) );  
  20.         return 1;  
  21.     }  
  22.     longnum=0,sum=0;  
  23.     static char buf[1024];  
  24.     memset(buf,'\0',sizeof(buf));  
  25.     const char* ip = argv[1];  
  26.     int port = atoi( argv[2] );  
  27.     const char* file_name = argv[3];  
  28.   
  29.     int filefd = open( file_name, O_RDONLY );  
  30.     assert( filefd > 0 );  
  31.     struct stat stat_buf;  
  32.     fstat( filefd, &stat_buf );  
  33.           
  34.         FILE *fp=fdopen(filefd,"r");  
  35.           
  36.     struct sockaddr_in address;  
  37.     bzero( &address, sizeof( address ) );  
  38.     address.sin_family = AF_INET;  
  39.     inet_pton( AF_INET, ip, &address.sin_addr );  
  40.     address.sin_port = htons( port );  
  41.   
  42.     int sock = socket( PF_INET, SOCK_STREAM, 0 );  
  43.     assert( sock >= 0 );  
  44.   
  45.     int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );  
  46.     assert( ret != -1 );  
  47.   
  48.     ret = listen( sock, 5 );  
  49.     assert( ret != -1 );  
  50.   
  51.     struct sockaddr_in client;  
  52.     socklen_t client_addrlength = sizeof( client );  
  53.     int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );  
  54.     if ( connfd < 0 )  
  55.     {  
  56.         printf( "errno is: %d\n", errno );  
  57.     }  
  58.     else  
  59.     {  
  60.         time_t begintime=time(NULL);  
  61.           
  62.         while((fgets(buf,1024,fp))!=NULL){  
  63.             num=send(connfd,buf,sizeof(buf),0);  
  64.             sum+=num;  
  65.             memset(buf,'\0',sizeof(buf));  
  66.         }  
  67.           
  68. //        sendfile( connfd, filefd, NULL, stat_buf.st_size );  
  69.         time_t endtime=time(NULL);  
  70.         printf("sum:%ld\n",sum);  
  71.         printf("need time:%d\n",endtime-begintime);  
  72.         close( connfd );  
  73.     }  
  74.   
  75.     close( sock );  
  76.     return 0;  
  77. }  

 

客户端:

  1. #include <sys/socket.h>  
  2. #include <netinet/in.h>  
  3. #include <arpa/inet.h>  
  4. #include <assert.h>  
  5. #include <stdio.h>  
  6. #include <unistd.h>  
  7. #include <stdlib.h>  
  8. #include <errno.h>  
  9. #include <string.h>  
  10. #include <sys/types.h>  
  11. #include <sys/stat.h>  
  12. #include <fcntl.h>  
  13. #include <sys/sendfile.h>  
  14.   
  15. int main( int argc, char* argv[] )  
  16. {  
  17.     if( argc <= 3 )  
  18.     {  
  19.         printf( "usage: %s ip_address port_number filename\n", basename( argv[0] ) );  
  20.         return 1;  
  21.     }  
  22.     static char buf[1024];  
  23.     memset(buf,'\0',sizeof(buf));  
  24.     const char* ip = argv[1];  
  25.     int port = atoi( argv[2] );  
  26.     const char* file_name = argv[3];  
  27.   
  28.     int filefd = open( file_name, O_WRONLY );  
  29.     if(filefd<=0)  
  30.         printf("open error:%s",strerror(errno));  
  31.     assert( filefd > 0 );  
  32.           
  33.         FILE *fp=fdopen(filefd,"w");  
  34.           
  35.     struct sockaddr_in address;  
  36.     socklen_t len=sizeof(address);  
  37.     bzero( &address, sizeof( address ) );  
  38.     address.sin_family = AF_INET;  
  39.     inet_pton( AF_INET, ip, &address.sin_addr );  
  40.     address.sin_port = htons( port );  
  41.   
  42.     int sock = socket( PF_INET, SOCK_STREAM, 0 );  
  43.     assert( sock >= 0 );  
  44.         int num;  
  45.         int ret=connect(sock,(struct sockaddr*)&address,len);  
  46.     if ( ret < 0 )  
  47.     {  
  48.         printf( "connect errno: %s\n", strerror(errno) );  
  49.     }  
  50.     else  
  51.     {  
  52.         while( (num=recv(sock,buf,sizeof(buf),0))>0 ){  
  53.             fprintf(fp,"%s",buf);  
  54.             memset(buf,'\0',sizeof(buf));  
  55.         }  
  56.           
  57.         close( sock );  
  58.     }  
  59.   
  60.     close( sock );  
  61.     return 0;  
  62. }  

 

 

测试环境:linux Ubuntu 32位系统 CPU Intel i5-4258U  @ 2.40GHz *4  内存2G

sendfile零拷贝高效率发送文件 - 第1张  | 刘建广学习笔记

 

根据以上对比,使用sendfile的系统负载要低一些,cpu使用率要低很多,整体速度和send基本差不多,估计是当今系统计算速度太快,看不出什么明显区别。不过对比web服务器的话区别还是很大的。

最后编辑:
作者:liujg
真实-不弄虚,不做假,做自己,不违心; 踏实-不浮躁,不盲从,不急功,不近利; 实学-不投机,不取巧,勤于学,精于业。