Linux网络编程--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);

参数特别注意的是: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 里。


服务端:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sendfile.h>

int main( int argc, char* argv[] )
{
    if( argc <= 3 )
    {
        printf( "usage: %s ip_address port_number filename\n", basename( argv[0] ) );
        return 1;
    }
    longnum=0,sum=0;
    static char buf[1024];
    memset(buf,'\0',sizeof(buf));
    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 );
		
		FILE *fp=fdopen(filefd,"r");
		
    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
    {
        time_t begintime=time(NULL);
        
        while((fgets(buf,1024,fp))!=NULL){
        	num=send(connfd,buf,sizeof(buf),0);
        	sum+=num;
        	memset(buf,'\0',sizeof(buf));
        }
        
//        sendfile( connfd, filefd, NULL, stat_buf.st_size );
        time_t endtime=time(NULL);
        printf("sum:%ld\n",sum);
        printf("need time:%d\n",endtime-begintime);
        close( connfd );
    }

    close( sock );
    return 0;
}

客户端:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sendfile.h>

int main( int argc, char* argv[] )
{
    if( argc <= 3 )
    {
        printf( "usage: %s ip_address port_number filename\n", basename( argv[0] ) );
        return 1;
    }
    static char buf[1024];
    memset(buf,'\0',sizeof(buf));
    const char* ip = argv[1];
    int port = atoi( argv[2] );
    const char* file_name = argv[3];

    int filefd = open( file_name, O_WRONLY );
    if(filefd<=0)
    	printf("open error:%s",strerror(errno));
    assert( filefd > 0 );
		
		FILE *fp=fdopen(filefd,"w");
		
    struct sockaddr_in address;
    socklen_t len=sizeof(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 num;
		int ret=connect(sock,(struct sockaddr*)&address,len);
    if ( ret < 0 )
    {
        printf( "connect errno: %s\n", strerror(errno) );
    }
    else
    {
        while( (num=recv(sock,buf,sizeof(buf),0))>0 ){
        	fprintf(fp,"%s",buf);
        	memset(buf,'\0',sizeof(buf));
        }
        
        close( sock );
    }

    close( sock );
    return 0;
}


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



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

阅读更多
版权声明:【博主微信公众号:不忘初心的行者】【本文为博主原创,未经博主允许不得转载】 https://blog.csdn.net/u010193457/article/details/50856268
上一篇Linux网络编程--定时器之时间堆
下一篇Linux网络编程--epoll模型之LT触发模式和ET触发模式
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭