当前位置: 首页 >> 程序设计 >> Linux中的"零拷贝"剖析
 

Linux中的"零拷贝"剖析

作者:      来源:http://blog.csdn.net/lovekatherine     发表时间:2007-03-25     浏览次数:      字号:    

到此为止,我们已经能够避免内核进行多次复制,然而我们还存在一分多余的副本。这份副本也可以消除吗?当然,在硬件提供的一些帮助下是可以的。为了消除内核产生的素有数据冗余,需要网络适配器支持聚合操作特性。该特性意味着待发送的数据不要求存放在地址连续的内存空间中;相反,可以是分散在各个内存位置。在内核版本2.4中,socket缓冲区描述符结构发生了改动,以适应聚合操作的要求——这就是Linux中所谓的"零拷贝“。这种方式不仅减少了多个上下文切换,而且消除了数据冗余。从用户层应用程序的角度来开,没有发生任何改动,所有代码仍然是类似下面的形式:

 

sendfile(socket, file, len);

 

为了更好的理解所涉及的操作,请看图4

Figure 4. Hardware that supports gather can assemble data from multiple memory locations, eliminating another copy.

 

 

 

步骤一:sendfile系统调用导致文件内容通过DMA模块被复制到内核缓冲区中。

 

步骤二:数据并未被复制到socket关联的缓冲区内。取而代之的是,只有记录数据位置和长度的描述符被加入到socket缓冲区中。DMA模块将数据直接从内核缓冲区传递给协议引擎,从而消除了遗留的最后一次复制。

 

由于数据实际上仍然由磁盘复制到内存,再由内存复制到发送设备,有人可能会声称这并不是真正的"零拷贝"。然而,从操作系统的角度来看,这就是"零拷贝",因为内核空间内不存在冗余数据。应用"零拷贝"特性,出了避免复制之外,还能获得其他性能优势,例如更少的上下文切换,更少的CPU cache污染以及没有CPU必要计算校验和。

 

现在我们明白了什么是"零拷贝",让我们将理论付诸实践,编写一些代码。你可以从www.xalien.org/articles/source/sfl-src.tgz处下载完整的源码。执行"tar -zxvf sfl-src.tgz"将源码解压。运行make命令,编译源码,并创建随机数据文件data.bin

 

从头文件开始介绍代码:

 

/* sfl.c sendfile example program

Dragan Stancevic <

header name                 function / variable

-------------------------------------------------*/

#include <stdio.h>          /* printf, perror */

#include <fcntl.h>          /* open */

#include <unistd.h>         /* close */

#include <errno.h>          /* errno */

#include <string.h>         /* memset */

#include <sys/socket.h>     /* socket */

#include <netinet/in.h>     /* sockaddr_in */

#include <sys/sendfile.h>   /* sendfile */

#include <arpa/inet.h>      /* inet_addr */

#define BUFF_SIZE (10*1024) /* size of the tmp

                               buffer */

 

除了基本socket操作所需要的 <sys/socket.h> <netinet/in.h>头文件外,我们还需要包含sendfile系统调用的原型定义,这可以在<sys/sendfile.h>头文件中找到。

 

服务器标志:

 

/* are we sending or receiving */

if(argv[1][0] == 's') is_server++;

/* open descriptors */

sd = socket(PF_INET, SOCK_STREAM, 0);

if(is_server) fd = open("data.bin", O_RDONLY);

 

 

该程序既能以服务端/发送方,也能以客户端/接收方的身份运行。我们需要检查命令行参数中的一项,然后相应的设置is_server标志。程序中大开了一个地址族为PF_INET的流套接字;作为服务端运行时需要向客户发送数据,因此要打开某个数据文件。由于程序中是用sendfile系统调用来发送数据,因此不需要读取文件内容并存储在程序的缓冲区内。

 

接下来是服务器地址:

 

/* clear the memory */

memset(&sa, 0, sizeof(struct sockaddr_in));

/* initialize structure */

sa.sin_family = PF_INET;

sa.sin_port = htons(1033);

sa.sin_addr.s_addr = inet_addr(argv[2]);

 

将服务端地址结构清零后设置协议族、端口和IP地址。服务端的IP地址作为命令行参数传递给程序。端口号硬编码为1033,选择该端口是因为它在要求root权限的端口范围之上。

 

下面是服务端的分支代码:

 

if(is_server){

    int client; /* new client socket */

    printf("Server binding to [%s]\n", argv[2]);

    if(bind(sd, (struct sockaddr *)&sa,

                      sizeof(sa)) < 0){

        perror("bind");

        exit(errno);

    }

 

作为服务端,需要为socket描述符分配一个地址,这是通过系统调用bind完成的,它将服务器地址(sa)分配给socket描述符(sd).

 

if(listen(sd,1) < 0){

    perror("listen");

    exit(errno);

}

 

由于使用流套接字,必须对内核声明接受外来连接请求的意愿,并设置连接队列的尺寸。此处将队列长度设为1,但是通常会将该值设的高一些,用于接受已建立的连接。在老版本的内核中,该队列被用于阻止SYN flood攻击。由于listen系统调用之改为设定已建立连接的数量,该特性已被listen调用遗弃。内核参数tcp_max_syn_backlog承担了保护系统不受SYN flood攻击的功能。

 

if((client = accept(sd, NULL, NULL)) < 0){

    perror("accept");

    exit(errno);

}

 

accept系统调用从待处理的已连接队列中选取第一个连接请求,为之建立一个新的socketaccept调用的返回值是新建立连接的描述符;新的socket可以用于readwritepoll/select系统调用。

 

if((cnt = sendfile(client,fd,&off,

                          BUFF_SIZE)) < 0){

    perror("sendfile");

    exit(errno);

}

printf("Server sent %d bytes.\n", cnt);

close(client);

 

在客户socket描述符上已经建立好连接,因此可以开始将数据传输至远端系统——这时通过调用sendfile系统调用来完成。该调用在Linux中的原型为如下形式:

 

extern ssize_t

sendfile (int __out_fd, int __in_fd, off_t *offset,

          size_t __count) __THROW;

 

前两个参数为文件描述符,第三个参数表示sendfile开始传输数据的偏移量。第四个参数是打算传输的字节数。为了sendfile可以使用"零拷贝“特性,网卡需要支持聚合操作,此外还应具备校验和计算能力。如果你的NIC不具备这些特性,仍可以是用sendfile来发送数据,区别是内核在传输前会将所有缓冲区的内容合并。

 

移植性问题

 

sendfile系统调用的问题之一,总体上来看,是缺少标准化的实现,这与open系统调用类些。sendfileLinuxSolarisHP-UX中的实现有很大的不同。这给希望在网络传输代码中利用"零拷贝"的开发者带来了问题。

 

这些实现差异中的一点在于Linux提供的sendfile,是定义为用于两个文件描述符之间和文件到socket之间的传输接口。另一方面,HP-UXSolaris中,sendfile只能用于文件到socket的传输。

 

第二点差异,是Linux没有实现向量化传输。SolarisHP-UX 中的sendfile系统调用包含额外的参数,用于消除为待传输数据添加头部的开销。

 

展望

 

Linux中“零拷贝”的实现还远未结束,并很可能在不久的未来发生变化。更多的功能将会被添加,例如,现在的sendfile不支持向量化传输,而诸如SambaApache这样的服务器不得不是用TCP_COKR标志来执行多个sendfile调用。该标志告知系统还有数据要在下一个sendfile调用中到达。TCP_CORKTCP_NODELAY不兼容,后者在我们希望为数据添加头部时使用。这也正是一个完美的例子,用于说明支持向量化的sendfile将在那些情况下,消除目前实现所强制产生的多个sendfile调用和延迟。

 

当前sendfile一个相当令人不愉快的限制是它无法用户传输大于2GB的文件。如此尺寸大小的文件,在今天并非十分罕见,不得不复制数据是十分令人失望的。由于这种情况下sendfilemmap都是不可用的,在未来内核版本中提供sendfile64,将会提供很大的帮助。

 

 

结论

 

尽管有一些缺点,"零拷贝"sendfile是一个很有用的特性。我希望读者认为本文提供了足够的信息以开始在程序中使用sendfile。如果你对这个主题有更深层次的兴趣,敬请期待我的第二篇文章——"Zero Copy II: Kernel Perspective",在其中将更深一步的讲述"零拷贝"的内核内部实现。

[1] [2]

编辑 webmaster

 
 
 
评论更多>>
 
 
发表
 
姓名: QQ:
性别: MSN:
E-mail: 主页:
评分: 1 2 3 4 5
评论内容:
验证码:
  
  • 请遵守《互联网电子公告服务管理规定》及中华人民共和国其他各项有关法律法规。
  • 严禁发表危害国家安全、损害国家利益、破坏民族团结、破坏国家宗教政策、破坏社会稳定、侮辱、诽谤、教唆、淫秽等内容的评论 。
  • 用户需对自己在使用本站服务过程中的行为承担法律责任(直接或间接导致的)。
  • 本站管理员有权保留或删除评论内容。
  • 评论内容只代表网友个人观点,与本网站立场无关。
  •