Linux "零拷贝" sendfile函数

Sendfile函数说明
#include
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

sendfile()是作用于数据拷贝在两个文件描述符之间的操作函数.这个拷贝操作是内核中操作的,所以称为"零拷贝".sendfile函数比起read和write函数高效得多,因为read和write是要把数据拷贝到用户应用层操作.

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

返回值:

如果成功的拷贝,返回写操作到out_fd的字节数,错误返回-1,并相应的设置error信息.


EAGAIN 无阻塞I/O设置O_NONBLOCK时,写操作(write)阻塞了.
EBADF 输出或者输入的文件描述符没有打开.
EFAULT 错误的地址.
EINVAL 描述符不可用或者锁定了,或者用mmap()函数操作的in_fd不可用.
EIO 当读取(read)in_fd时发生未知错误.
ENOMEM 读(read)in_fd时内存不足.

由于想再提升原有系统中文件传输模块的速度,并减少系统资源占用,进行了一次sendfile()的性能测试,但失败了.不过还是将它用在了模块中.记录一下这次失改的微调测试.

运行平台: 客户机与服务器均为P4计算机,IDE硬盘; Fedora5发行版; 百兆局域网;

接收端程序如下:
FILE *fp = fopen(FILENAME,"wb");

while((len = recv(sockfd, buff, sizeof(buff), 0)) > 0)
{
fwrite(buffer, 1, len, fp);
}
fclose(fp);

A. 发送端传统方式代码段如下:
fd = open(FILENAME, O_RDONLY);
while((len =read(fd, buff, sizeof(buff))) >0)
{
send(sockfd, buff, len ,0);
}
close(fd);

由于我磁盘分区时指定的块大小为4096,为了最优读取磁盘数据,buff大小设为4096字节.但在测试中发现设为1024或8192不会对传输速度带来影响.

文件大小:9M; 耗时:0.71 - 0.76秒;
文件大小:32M; 耗时:2.64 - 2.68秒;
文件大小:64M; 耗时:5.36 - 5.43秒;

B. 使用sendfile()传输代码段.
off_t offset = 0;
stat(FILENAME, &filestat);

fd = open(FILENAME, O_RDONLY);
sendfile(sockfd, fd, &offset, filestat.st_size) );
close(fd);

文件大小:9M; 耗时:0.71 - 1.08秒;
文件大小:32M; 耗时:2.66 - 2.74秒;
文件大小:64M; 耗时:5.43 - 6.64秒;

似乎还略有下降.根据sendfile的man手册,我在使用该函数前调用了

int no = 1;
printf("%d\n", setsockopt(sockfd, IPPROTO_TCP, TCP_CORK, (char*)&no, sizeof(int)) );

文件大小:9M; 耗时:0.72 - 0.75秒;
文件大小:32M; 耗时:2.66 - 2.68秒;
文件大小:64M; 耗时:5.38 - 5.60秒;



服务器响应一个http请求的步骤如下:
1 把磁盘文件读入内核缓冲区
2 从内核缓冲区读到内存
3 处理(静态资源不需处理)
4 发送到网卡的内核缓冲区(发送缓存)
5 网卡发送数据

数据从第一步中的内核缓冲区到第四步的内核缓冲区白白绕了一个圈,没有任何变化浪费了时间

而sendfile系统调用就是来解决这个问题的。sendfile省略了上面的 2、3步,磁盘文件被直接发送到了网卡的内存缓冲区,减少了数据复制和内核态切换的开销 。

更详细的描述见 http://www.vpsee.com/2009/07/linux-sendfile-improve-performance/

现在流行的 web 服务器里面都提供 sendfile 选项用来提高服务器性能,那到底 sendfile 是什么,怎么影响性能的呢?sendfile 实际上是 Linux 2.0+ 以后的推出的一个系统调用,web 服务器可以通过调整自身的配置来决定是否利用 sendfile 这个系统调用。先来看一下不用 sendfile 的传统网络传输过程:

read(file, tmp_buf, len);
write(socket, tmp_buf, len);

硬盘 >> kernel buffer >> user buffer >> kernel socket buffer >> 协议栈

一般来说一个网络应用是通过读硬盘数据,然后写数据到 socket 来完成网络传输的。上面2行用代码解释了这一点,不过上面2行简单的代码掩盖了底层的很多操作。来看看底层是怎么执行上面2行代码的:

1、系统调用 read() 产生一个上下文切换:从 user mode 切换到 kernel mode,然后 DMA 执行拷贝,把文件数据从硬盘读到一个 kernel buffer 里。
2、数据从 kernel buffer 拷贝到 user buffer,然后系统调用 read() 返回,这时又产生一个上下文切换:从kernel mode 切换到 user mode。
3、系统调用 write() 产生一个上下文切换:从 user mode 切换到 kernel mode,然后把步骤2读到 user buffer 的数据拷贝到 kernel buffer(数据第2次拷贝到 kernel buffer),不过这次是个不同的 kernel buffer,这个 buffer 和 socket 相关联。
4、系统调用 write() 返回,产生一个上下文切换:从 kernel mode 切换到 user mode(第4次切换了),然后 DMA 从 kernel buffer 拷贝数据到协议栈(第4次拷贝了)。

上面4个步骤有4次上下文切换,有4次拷贝,我们发现如果能减少切换次数和拷贝次数将会有效提升性能。在kernel 2.0+ 版本中,系统调用 sendfile() 就是用来简化上面步骤提升性能的。sendfile() 不但能减少切换次数而且还能减少拷贝次数。

再来看一下用 sendfile() 来进行网络传输的过程:

sendfile(socket, file, len);

硬盘 >> kernel buffer (快速拷贝到kernel socket buffer) >> 协议栈

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 里。

步骤减少了,切换减少了,拷贝减少了,自然性能就提升了。这就是为什么说在 Nginx 配置文件里打开 sendfile on 选项能提高 web serve r性能的原因。




参考文献:

http://www.linuxdiyf.com/viewarticle.php?id=69189

http://blog.chinaunix.net/uid-20778583-id-37112.html

你可能感兴趣的:(NUTTX)