sendfile() 在2个文件描述符之间传递数据(完全在内核中),避免在内核缓冲区和用户缓冲区之间进行数据拷贝,效率很高。是一种重要的零拷贝技术。

sendfile原理

sendfile()系统调用利用DMA引擎将文件中的数据拷贝到操作系统的内核缓冲区中,然后数据被拷贝到与socket相关的内核缓冲区中。然后,DMA将数据从内核socket缓冲区拷贝到协议引擎中。

sendfile系统调用不需要将数据拷贝,或映射到应用程序地址空间,因此sendfile只是适用于应用程序地址空间不需要对所访问的数据进行处理的情况。因为sendfile传输的数据没有越过应用程序/操作系统内核的边界,sendfile也因此极大地减少了存储管理的开销。

为什么说sendfile能避免数据拷贝?

通常,对指定文件进行读写,需要调用read、write,将数据从内核缓冲区拷贝到用户缓冲区,或者从用户缓冲区拷贝到内核缓冲区。而sendfile()可以直接让数据在两个fd之间传递,而无需经过用户缓冲区。这样,可以有效减少数据拷贝次数。

传统数据read/write模型:

sendfile声明

#include <sys/sendfile.h>

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

注意:sendfile是Linux特有功能,不可移植到其他系统。

参数

  • out_fd 待写入内容的文件描述符。
  • in_fd 待读出内容的文件描述符。
  • offset 指定从读入文件流的哪个位置开始读。如果为空,则使用读入文件流默认的起始位置(通常是0)。
  • count 指定在文件描述符in_fd和out_fd之间传输的字节数。

返回值
成功时,返回传输的字节数;失败,返回-1,errno被设置。

in_fd必须支持类似mmap函数,而在Linux内核2.6.33以前,out_fd必须是一个socket fd;在2.6.33之后,out_fd可以是任意文件。如果out_fd是普通文件,sendfile()将适当地改变file的offset。

参见sendfile(2) man手册

The in_fd argument must correspond to a file which supports mmap(2)-like operations
(i.e., it cannot be a socket).

In  Linux kernels before 2.6.33, out_fd must refer to a socket.  Since Linux 2.6.33
it can be any file.  If it is a regular file, then sendfile() changes the file off‐
set appropriately.

示例

利用sendfile,将server本地文件内容,通过TCP连接发送给client。

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 st;
    fstat(filefd, &st);

    struct sockaddr_in addr;
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &addr.sin_addr);
    addr.sin_port = htons(static_cast<uint16_t>(port));

    int sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);
    int on = 1;
    setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on)); // reuse port

    int ret = bind(sock, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr));
    assert(ret != -1);

    ret = listen(sock, 5);
    assert(ret != -1);

    struct sockaddr_in client;
    socklen_t client_addrlen = sizeof(client);
    int connfd = accept(sock, reinterpret_cast<struct sockaddr*>(&client), &client_addrlen);
    if (connfd < 0) {
        printf("errno is: %d\n", errno);
    }
    else {
        // sendfile from filefd to connfd
        sendfile(connfd, filefd, NULL, static_cast<size_t>(st.st_size));
        close(connfd);
    }

    close(filefd);
    close(sock);
    return 0;
}

参考

https://www.jianshu.com/p/028cf0008ca5