sendfile
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 秒。