如下翻译自:Zero Copy I: User-Mode Perspectivehtml
零拷贝是什么?linux
为了更好地理解问题的解决方案,咱们首先须要理解问题自己。让咱们来看看什么是参与网络服务器的简单过程dæmon服务数据存储在一个文件经过网络客户端。下面是一些示例代码:缓存
read(file, tmp_buf, len); write(socket, tmp_buf, len);
看起来很简单;您会认为只有这两个系统调用不会带来太多开销。事实上,这与事实相去甚远。在这两个调用以后,数据至少复制了四次,而且几乎执行了相同数量的用户/内核上下文切换。(实际上这个过程要复杂得多,但我想让它保持简单)。为了更好地了解所涉及的流程,请看图1。顶部显示上下文切换,底部显示复制操做。服务器
图1。复制两个示例系统调用网络
第一步:read系统调用致使上下文从用户模式切换到内核模式。第一个副本由DMA引擎执行,它从磁盘读取文件内容并将其存储到内核地址空间缓冲区中。异步
第二步:将数据从内核缓冲区复制到用户缓冲区,read系统调用返回。调用的返回致使上下文从内核切换回用户模式。如今数据存储在用户地址空间缓冲区中,它能够再次开始向下移动。socket
第三步:write系统调用致使上下文从用户模式切换到内核模式。执行第三次复制,再次将数据放入内核地址空间缓冲区。不过,这一次,数据被放入一个不一样的缓冲区,一个专门与套接字关联的缓冲区。性能
第四步:write系统调用返回,建立咱们的第四个上下文切换。当DMA引擎将数据从内核缓冲区传递到协议引擎时,会独立地、异步地进行第四次复制。你可能会问本身,“独立和异步是什么意思?”在呼叫返回以前,数据没有传输吗?“呼叫返回,实际上并不保证传输;它甚至不能保证传输的开始。它只是意味着以太网驱动程序在它的队列中有空闲的描述符,而且已经接受咱们的数据进行传输。可能有许多包在咱们的前面排队。除非驱动程序/硬件实现优先级环或队列,不然数据是在先进先出的基础上传输的。(图1中分叉的DMA副本演示了最后一个副本能够延迟的事实)。操作系统
正如您所看到的,不少数据复制实际上并非必要的。能够消除一些重复,以减小开销并提升性能。做为一名驱动程序开发人员,我使用的硬件具备一些很是高级的特性。一些硬件能够彻底绕过主存,直接将数据传输到另外一个设备。这个特性消除了系统内存中的副本,这是一个很好的特性,可是并非全部的硬件都支持它。还有一个问题是来自磁盘的数据必须为网络从新打包,这带来了一些复杂性。为了消除开销,咱们能够从消除内核和用户缓冲区之间的一些复制开始。翻译
消除副本的一种方法是跳过调用read,而是调用mmap。例如:
tmp_buf = mmap(file, len); write(socket, tmp_buf, len);
为了更好地了解所涉及的流程,请看图2。上下文切换保持不变。
图2。调用mmap
第一步:mmap系统调用致使DMA引擎将文件内容复制到内核缓冲区。而后与用户进程共享缓冲区,而不须要在内核和用户内存空间之间执行任何复制。
第二步:write系统调用致使内核将原始内核缓冲区中的数据复制到与套接字相关的内核缓冲区中。
第三步:当DMA引擎将数据从内核套接字缓冲区传递到协议引擎时,发生第三次复制。
经过使用mmap而不是read,咱们减小了内核必须复制的数据量的一半。当传输大量数据时,这将产生至关好的结果。然而,这种改善不是没有代价的;在使用mmap write方法时存在一些隐藏的陷阱。当您在内存中映射一个文件,而后调用write,而另外一个进程截断相同的文件时,您将陷入其中之一。您的写系统调用将被总线错误信号SIGBUS中断,由于您执行了错误的内存访问。该信号的默认行为是终止进程并转储内核——这对于网络服务器来讲不是最理想的操做。有两种方法能够解决这个问题。
第一种方法是为SIGBUS信号安装一个信号处理程序,而后在处理程序中简单地调用return。经过这样作,write系统调用将返回它在被中断以前写入的字节数,并将errno设置为成功。让我指出,这将是一个糟糕的解决方案,只解决症状,而不是问题的根源。由于SIGBUS信号代表进程出现了严重错误,因此我不建议使用它做为解决方案。
第二种解决方案涉及从内核租用文件(在Microsoft Windows中称为“机会锁定”)。这是解决这个问题的正确方法。经过在文件描述符上使用租借,您能够对特定文件的内核进行租借。而后能够从内核请求读/写租约。当另外一个进程试图截断您正在传输的文件时,内核会向您发送实时信号,即RT_SIGNAL_LEASE信号。它告诉您内核正在破坏您对该文件的读或写租约。在程序访问无效地址并被SIGBUS信号终止以前,写调用被中断。write调用的返回值是在中断以前写入的字节数,errno将被设置为成功。下面是一些示例代码,展现了如何从内核得到租约:
if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) { perror("kernel lease set signal"); return -1; } /* l_type can be F_RDLCK F_WRLCK */ if(fcntl(fd, F_SETLEASE, l_type)){ perror("kernel lease set type"); return -1; }
您应该在映射文件以前得到您的租约,并在完成以后取消您的租约。这是经过使用F_UNLCK的租赁类型调用fcntl F_SETLEASE实现的。
Sendfile
在内核版本2.1中,引入了sendfile系统调用,以简化经过网络和两个本地文件之间的数据传输。sendfile的引入不只减小了数据复制,还减小了上下文切换。像这样使用它:
sendfile(socket, file, len);
为了更好地了解所涉及的流程,请看图3。
图3。用Sendfile替换读和写
第一步:sendfile系统调用致使DMA引擎将文件内容复制到内核缓冲区。而后,内核将数据复制到与套接字关联的内核缓冲区中。
步骤2:当DMA引擎将数据从内核套接字缓冲区传递到协议引擎时,发生第三次复制。
您可能想知道若是另外一个进程截断了咱们使用sendfile系统调用传输的文件,会发生什么状况。若是咱们不注册任何信号处理程序,sendfile调用只返回它在中断以前传输的字节数,errno将被设置为成功。
可是,若是咱们在调用sendfile以前从内核得到文件的租约,则行为和返回状态是彻底相同的。咱们还将在sendfile调用返回以前得到RT_SIGNAL_LEASE信号。
到目前为止,咱们已经可以避免让内核复制几个副本,可是仍然只剩下一个副本。这也能避免吗?固然,在硬件的帮助下。为了消除内核所作的全部数据重复,咱们须要一个支持收集操做的网络接口。这仅仅意味着等待传输的数据不须要在连续的内存中;它能够分散在不一样的内存位置。在内核版本2.4中,修改了套接字缓冲区描述符以适应那些需求——在Linux下称为零拷贝。这种方法不只减小了多个上下文切换,还消除了处理器形成的数据重复。对于用户级应用程序,一切都没有改变,因此代码仍然是这样的:
sendfile(socket, file, len);
为了更好地了解所涉及的流程,请看图4。
图4。支持收集的硬件能够从多个内存位置收集数据,从而消除了另外一个副本。
第一步:sendfile系统调用致使DMA引擎将文件内容复制到内核缓冲区。
第二步:没有数据被复制到套接字缓冲区。相反,只有包含关于数据位置和长度信息的描述符才会被附加到套接字缓冲区中。DMA引擎直接将数据从内核缓冲区传递到协议引擎,从而消除了剩余的最终副本。
由于数据实际上仍然是从磁盘复制到内存,从内存复制到链接,因此有些人可能会认为这不是真正的零拷贝。可是,从操做系统的角度来看,这是零拷贝,由于数据不是在内核缓冲区之间复制的。在使用零拷贝时,除了避免拷贝以外,还能够得到其余性能优点,好比更少的上下文切换、更少的CPU数据缓存污染和更少的CPU校验和计算。
下面是两篇很是好的文章,收藏: