sendfile

2020-08-27  本文已影响0人  欧阳_z

1、简介
服务器通过网络传输一个文件到客户端。通常我们可能会先用
ssize_t read(int fd, void *buf, size_t count);函数把文件读到应用程序的数组,再通过ssize_t send(int sockfd, const void *buf, size_t len, int flags);函数发送出去。但是如果文件很大,是数组的很多倍,比如数组是 1024 字节,而文件是 20MB ,那就要循环调用两万次左右。
而每一次循环的过程都有 4 次用户态与内核态的上下文切换和 4 次数据的复制:
(1)先 read 系统调用,从用户空间切换到内核空间,从磁盘中读取文件内容,存储在内核空间的缓冲区。
(2)文件内容从内核空间的缓冲区复制到用户空间的缓冲区,系统调用 read 返回,从内核空间切换到用户空间。
(3)send 函数调用 write 系统调用,从用户空间切换到内核空间,文件内容从用户空间缓冲区被复制到内核空间socket缓冲区。
(4)write 系统调用返回,从内核空间切换到用户空间。文件内容从socket缓冲区复制到网卡。

再乘以上万次的循环,开销就被放大了。而这中间的过程有很多冗余,比如上下文切换次数太多,用户缓冲区没有存在的必要。

linux 提供了 sendfile 系统调用来解决这一问题。函数原型是
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
执行 man 2 sendfile 可以看到man手册说明了sendfile()复制只在内核完成,所以比read()write()的组合更高效。

DESCRIPTION
       sendfile()  copies data between one file descriptor and another.  Because this copying is done within the kernel, sendfile() is more
       efficient than the combination of read(2) and write(2), which would require transferring data to and from user space.

2、代码测试
send.c:

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

#define IPADDR "your ip"
#define PORT 6666

unsigned long get_file_size(const char *path)
{  
    unsigned long filesize = -1;      
    struct stat statbuff;  
    if(stat(path, &statbuff) < 0)
    {  
        return filesize;  
    }
    else
    {  
        filesize = statbuff.st_size;  
    }  
    return filesize;  
}  

void read_send_file(int sockfd)
{
    int fd, ret;
    size_t size;
    clock_t start, finish;  
    double  duration;
    char buffer[1024];

    fd = open("./read.bin", O_RDONLY );
    if (fd <= 0)
    {
        printf("__LINE__ = %d \n", __LINE__);
        exit(1);
    }
    unsigned long filesize = get_file_size("./read.bin");

    start = clock();
#ifndef SENDFILE
    while (filesize > 0)
    {
        size = read(fd, buffer, sizeof(buffer));
        if (size > 0 && size != sizeof(buffer) )
        {
            printf("read return = %lu !!!\n", size);
        }
        ret = send(sockfd, buffer, size, 0);
        if (ret > 0 && ret != size )
        {
            printf("send return = %d !!!\n", ret);
        }
        if (size <= 0)
        {
            printf("send return = %lu.\n", size);
            exit(1);
        }
        filesize -= size;
    }
    finish = clock();
    duration = (double)(finish - start) / CLOCKS_PER_SEC;
    printf("read and send: %f\n", duration);
#else
    size = sendfile(sockfd, fd, NULL, filesize);
    if (size != filesize )
    {
        printf("send size = %lu !!!\n", size);
    }
    finish = clock();
    duration = (double)(finish - start) / CLOCKS_PER_SEC;
    printf("sendfile: %f\n", duration);
#endif

    close(fd);
    close(sockfd);
}

int main(void) 
{
    int sockfd;
    struct sockaddr_in servaddr;
    char *ip = IPADDR;
 
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    if (inet_pton(AF_INET, ip, &servaddr.sin_addr) <= 0)
    {
        printf("inet_pton error for %s\n", ip);
        return 0;
    }

    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
    {
        printf("connect error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    read_send_file(sockfd);
    return 0;
}

recv.c:

#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define PORT 6666

void recv_file(int connfd)
{
    int ret;
    char buffer[1024];
    while (1)
    {
        ret = recv(connfd, buffer, sizeof(buffer), 0);
        if (ret == 0)
        {
            close(connfd);
            return ;
        }
    }
}

int main(void) 
{
    int listenfd, connfd;
    struct sockaddr_in servaddr;
 
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
 
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(PORT);
 
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
    {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
 
    if (listen(listenfd, 10) == -1)
    {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    printf("========waiting for client's request========\n");

    if ((connfd = accept(listenfd, (struct sockaddr *)NULL, NULL)) == -1)
    {
        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
    }
    recv_file(connfd);

    close(listenfd);
    return 0;
}

Makefile:

all:
    rm -f recv send sendfile
    gcc recv.c -g -o recv
    gcc send.c -g -o send
    gcc send.c -g -D SENDFILE -o sendfile
    @echo "dd if=/dev/urandom of=./read.bin bs=1024 count=20000"

time命令分别测试 send 和 sendfile 各 5 次,每次之间间隔 5 秒。send 平均耗时 0.4 秒,sendfile 平均耗时 0.1 秒。

上一篇下一篇

猜你喜欢

热点阅读