您好,欢迎来到九壹网。
搜索
您的当前位置:首页零拷贝方案

零拷贝方案

来源:九壹网


直接 I/O:对于这种数据传输方式来说,应用程序可以直接访问硬件存储,操作系统内核只是辅助数据传输:这类零拷贝技术针对的是操作系统内核并不需要对数据进行直接处理的情况,数据可以在应用程序地址空间的缓冲区和磁盘之间直接进行传输,完全不需要 Linux 操作系统内核提供的页缓存的支持。

在数据传输的过程中,避免数据在操作系统内核地址空间的缓冲区和用户应用程序地址空间的缓冲区之间进行拷贝。有的时候,应用程序在数据进行传输的过程中不需要对数据进行访问,那么,将数据从 Linux 的页缓存拷贝到用户进程的缓冲区中就可以完全避免,传输的数据在页缓存中就可以得到处理。在某些特殊的情况下,这种零拷贝技术可以获得较好的性能。Linux 中提供类似的系统调用主要有 mmap(),sendfile() 以及 splice()。

对数据在 Linux 的页缓存和用户进程的缓冲区之间的传输过程进行优化。该零拷贝技术侧重于灵活地处理数据在用户进程的缓冲区和操作系统的页缓存之间的拷贝操作。这种方法延续了传统的通信方式,但是更加灵活。在 Linux 中,该方法主要利用了写时复制技术。

以标准的方式对文件进行读写

利用 mmap 代替 read

直接IO方式

sendfile ()

sendfile(socket, file, len);

零拷贝

带有 DMA 收集拷贝功能的 sendfile()

上小节介绍的 sendfile() 技术在进行数据传输仍然还需要一次多余的数据拷贝操作,通过引入一点硬件上的帮助,这仅有的一次数据拷贝操作也可以避免。为了避免操作系统内核造成的数据副本,需要用到一个支持收集操作的网络接口,这也就是说,待传输的数据可以分散在存储的不同位置上,而不需要在连续存储中存放。这样一来,从文件中读出的数据就根本不需要被拷贝到 socket 缓冲区中去,而只是需要将缓冲区描述符传到网络协议栈中去,之后其在缓冲区中建立起数据包的相关结构,然后通过 DMA 收集拷贝功能将所有的数据结合成一个网络数据包。网卡的 DMA 引擎会在一次操作中从多个位置读取包头和数据。Linux 2.4 版本中的 socket 缓冲区就可以满足这种条件,这也就是用于 Linux 中的众所周知的零拷贝技术,这种方法不但减少了因为多次上下文切换所带来开销,同时也减少了处理器造成的数据副本的个数。对于用户应用程序来说,代码没有任何改变。首先,sendfile() 系统调用利用 DMA 引擎将文件内容拷贝到内核缓冲区去;然后,将带有文件位置和长度信息的缓冲区描述符添加到 socket 缓冲区中去,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,DMA 引擎会将数据直接从内核缓冲区拷贝到协议引擎中去,这样就避免了最后一次数据拷贝。 性能对比:传统方法与零拷贝

文件大小 7MB

正常文件传输(ms) 156

transferTo(ms) 45

21MB 63MB 98MB 200MB 350MB 700MB 1GB 337 843 1320 2124 3631 13498 18399 128 387 617 1150 1762 4422 8537

splice()

splice() 是 Linux 中与 mmap() 和 sendfile() 类似的一种方法。它也可以用于用户应用程序地址空间和操作系统地址空间之间的数据传输。splice() 适用于可以确定数据传输路径的用户应用程序,它不需要利用用户地址空间的缓冲区进行显式的数据传输操作。那么,当数据只是从一个地方传送到另一个地方,过程中所传输的数据不需要经过用户应用程序的处理的时候,spice() 就成为了一种比较好的选择。splice() 可以在操作系统地址空间中整块地移动数据,从而减少大多数数据拷贝操作。而且,splice() 进行数据传输可以通过异步的方式来进行,用户应用程序可以先从系统调用返回,而操作系统内核进程会控制数据传输过程继续进行下去。splice() 可以被看成是类似于基于流的管道的实现,管道可以使得两个文件描述符相互连接,splice 的调用者则可以控制两个设备(或者协议栈)在操作系统内核中的相互连接。

splice() 系统调用和 sendfile() 非常类似,用户应用程序必须拥有两个已经打开的文件描述符,一个用于表示输入设备,一个用于表示输出设备。与 sendfile() 不同的是,splice() 允许任意两个文件之间互相连接,而并不只是文件到 socket 进行数据传输。对于从一个文件描述符发送数据到 socket 这种特例来说,一直都是使用 sendfile() 这个系统调用,而 splice 一直以来就只是一种机制,它并不仅限于 sendfile() 的功能。也就是说,sendfile() 只是 splice() 的一个子集,在 Linux 2.6.23 中,sendfile() 这种机制的实现已经没有了,但是这个 API 以及相应的功能还存在,只不过 API 以及相应的功能是利用了 splice() 这种机制来实现的。

在数据传输的过程中,splice() 机制交替地发送相关的文件描述符的读写操作,并且可以将读缓冲区重新用于写操作。它也利用了一种简单的流控制,通过预先定义的水印

( watermark )来阻塞写请求。有实验表明,利用这种方法将数据从一个磁盘传输到另一个磁盘会增加 30% 到 70% 的吞吐量,数据传输的过程中, CPU 的负载也会减少一半。 Linux 2.6.17 内核引入了 splice() 系统调用,但是,这个概念在此之前 ] 其实已经存在了很长一段时间了。1988 年,Larry McVoy 提出了这个概念,它被看成是一种改进服务器端系统的 I/O 性能的一种技术,尽管在之后的若干年中经常被提及,但是 splice 系统调用从来没有在主流的 Linux 操作系统内核中实现过,一直到 Linux 2.6.17 版本的出现。splice 系统调用需要用到四个参数,其中两个是文件描述符,一个表示文件长度,还有一个用于控制如何进行数据拷贝。splice 系统调用可以同步实现,也可以使用异步方式来实现。在使用异步方式的时候,用户应用程序会通过信号 SIGIO 来获知数据传输已经终止。splice() 系统调用的接口如下所示:

long splice(int fdin, int fdout, size_t len, unsigned int flags);

调用 splice() 系统调用会导致操作系统内核从数据源 fdin 移动最多 len 个字节的数据到 fdout 中去,这个数据的移动过程只是经过操作系统内核空间,需要最少的拷贝次数。使用 splice() 系统调用需要这两个文件描述符中的一个必须是用来表示一个管道设备的。不难看出,这种设计具有局限性,Linux 的后续版本针对这一问题将会有所改进。参数 flags 用于表示拷贝操作的执行方法,当前的 flags 有如下这些取值:

  

SPLICE_F_NONBLOCK:splice 操作不会被阻塞。然而,如果文件描述符没有被设置为不可被阻塞方式的 I/O ,那么调用 splice 有可能仍然被阻塞。

SPLICE_F_MORE:告知操作系统内核下一个 splice 系统调用将会有更多的数据传来。

SPLICE_F_MOVE:如果输出是文件,这个值则会使得操作系统内核尝试从输入管道缓冲区直接将数据读入到输出地址空间,这个数据传输过程没有任何数据拷贝操作发生。

Splice() 系统调用利用了 Linux 提出的管道缓冲区( pipe buffer )机制,这就是为什么这个系统调用的两个文件描述符参数中至少有一个必须要指代管道设备的原因。为了支持 splice 这种机制,Linux 在用于设备和文件系统的 file_operations 结构中增加了下边这两个定义:

ssize_t (*splice_write)(struct inode *pipe, strucuct file *out, size_t len, unsigned int flags); ssize_t (*splice_read)(struct inode *in, strucuct file *pipe, size_t len, unsigned int flags);

这两个新的操作可以根据 flags 的设定在 pipe 和 in 或者 out 之间移动 len 个字节。Linux 文件系统已经实现了具有上述功能并且可以使用的操作,而且还实现了一个 generic_splice_sendpage() 函数用于和 socket 之间的接合。

快速缓冲区( Fast Buffers )

tee

long tee(int fd_in, int fd_out, size_t len, unsigned int flags);

vmsplice

与他们一同引入的还有vmsplice: #define _GNU_SOURCE #include #include

long vmsplice(int fd, const struct iovec *iov, unsigned long nr_segs, unsigned int flags); 这个系统调用将用户空间的内存映射到内核空间,从而避免了实际的内存写操作,提高了系统效率。

socket使用mmap

如果你的内核提供了CONFIG_PACKET_MMAP选项,那么恭喜你,对网络包你可以采用mmap了,用了mmap,你就节省了内核从内核内存区拷贝到用户内存区的这一步,效率提高很多。

下面我们说说怎么用这么强的功能。

int fd;

fd= socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))

socket函数的protocal参数采用了ETH_P_ALL,表示抓取所有以太帧。SOCK_RAW表示抓取到的包的数据是IP包。

setsockopt(fd, SOL_PACKET, PACKET_RX_RING, (void *) &req, sizeof(req)) 其中req参数是个结构,如下所示:

struct tpacket_req {

unsigned int tp_block_size; /* Minimal size of contiguous block */ unsigned int tp_block_nr; /* Number of blocks */ unsigned int tp_frame_size; /* Size of frame */

unsigned int tp_frame_nr; /* Total number of frames */ };

该结构是用来在内核中创建循环缓冲区,这个缓冲区随后会被映射到调用进程的用户空间。

这块内存在内核中用block来组织,每个块是连续的物理内存区,每 个块的大小是tp_block_size个字节,每个块最多可以装tp_block_size/tp_frame_size个帧。 这几个参数是由用户程序设置的,但有以下一些 tp_block_size 必须是 PAGE_SIZE 的倍数

tp_frame_size 必须大于 TPACKET_HDRLEN ,每个帧都有帧头结构用来描述meta信息,如时间戳

tp_frame_size 必须是 TPACKET_ALIGNMENT的倍数,TPACKET_ALIGNMENT的值是15。 tp_frame_nr 必须是frames_per_block*tp_block_nr 每个帧由以下的部件组成: - struct tpacket_hdr 帧头

- pad 填充物,起对齐到16字节边界的作用 - struct sockaddr_ll

- Gap 填充物,起对齐到16字节边界的作用 - Start+tp_mac: 可选mac地址

- Start+tp_net: 包数据,16字节对齐 - Pad to align to TPACKET_ALIGNMENT=16 如果用以下参数:

tp_block_size= 4096 tp_frame_size= 2048 tp_block_nr = 4 tp_frame_nr = 8

我们就得到了,如下的数据结构: 代码:

block #1 block #2 block #3 block #4

+---------+---------+ +---------+---------+ +---------+---------+ +---------+---------+

| frame 1 | frame 2 | | frame 3 | frame 4 | | frame 5 | frame 6 | | frame 7 | frame 8 |

+---------+---------+ +---------+---------+ +---------+---------+ +---------+---------+

接着用户程序使用

mmap(0, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); 映射后得到的是一个block数组,每个block中包含有若干帧。 程序直接就可以读这些内容了。

linux下零拷贝技术的实现(zero-copy)

2009-11-19 09:55

作者:jk.li (jk.li@foxmail.com)

零拷贝(zero-copy)是只使用内存映射技术实现在内核与应用层之间的数据传递,由于内核与应用层使用的是同一块内存,取消了内核向用户空间的拷贝过程,会很大程度上提高系统效率。

实际应用,有一数据采集设备A,产生大量数据流,应用层读取通过该设备驱动获得数据,处理后,在通过网卡分发出去。在实际应用的过程中,数据要由A驱动copy到应用层,再由应用层处理后再次走网卡驱动,cpu除了要进行大量的应用处理(计算)还要copy,效率很低,接收端会出现数据卡的现象。采用zero-copy技术就可以解决这个问题。 具体实现: 内核端

#define SH_MEM_SIZE (1024*1024*2) int order;

unsigned long shmem_virt_addr; unsigned long shmem_phy_addr; order = get_order(SH_MEM_SIZE);

shmem_virt_addr = __get_free_pages(GFP_KERNEL, order); SetPageReserved(virt_to_page(shmem_virt_addr)); shmem_phy_addr=virt_to_phys(shmem_virt_addr); printk(\"[shmem_phy_addr= %x]\\n\copy_to_user(arg,&shmem_phy_addr,sizeof(long)); //shmem_virt_addr 内核使用,可以直接写入数据 //shmem_phy_addr 需要提供给应用层

应用

shmem_phy_addr = get_phy_addr(); //获得内核传过来的物理地址

printf(\"[shmem_phy_addr= %x]\\n\fd=open(\"/dev/mem\if(fd == -1){

perror(\"open mem\"); }

buf=mmap(0,SH_MEM_SIZE,PROT_READ|PROT_WRITE,MAP_SHARED,fd,shmem_phy_addr);

if(buf == MAP_FAILED){

perror(\"mmbuf error!\\n\"); }

//buf存放共享后的内存指针,应用层可以直接读取buf中的内容 代码:

原理介绍:

http://www.ibm.com/developerworks/cn/linux/l-cn-zerocopy1/index.html http://www.chineselinuxuniversity.net/articles/42547.shtml http://www.ibm.com/developerworks/cn/java/j-zerocopy/ http://www.linuxjournal.com/article/6345?page=0,0

实现:

http://blog.csdn.net/drizztzou/article/details/18293

http://hi.baidu.com/ljk0209/blog/item/95b025cbfb060cf7536f37.html

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- 91gzw.com 版权所有 湘ICP备2023023988号-2

违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务