Linux零拷贝原理

源文链接

到目前为止,几乎所有人都听说过Linux下所谓的零拷贝功能,但我经常遇到对这个主题没有完全了解的人。 正因为如此,我决定写几篇文章,深入探讨这个问题,希望能解开这个有用的特性。在这篇文章中,我们从一个用户的角度看零拷贝,血淋淋的内核级细节被有意省略。

什么是零拷贝?

为了更好地理解问题的解决方案,我们首先需要了解问题本身。让我们来看看网络服务器处理的简单过程中所涉及到的内容,它将存储在文件中的数据存储到网络上的客户端中。这里有一些示例代码:

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

看起来很简单;你会认为只有这两个系统调用不会有太多的开销. 事实上,这与事实并无太大的距离。在这两个调用的后面,数据至少被复制了4次,并且几乎已经执行了许多用户/内核上下文切换(实际上这个过程要复杂得多,但我想让它保持简单)。 为了更好地了解所涉及的过程,请看图1。顶部显示了上下文切换,而底部显示了复制操作。

Linux零拷贝原理

图1。在两个示例系统调用中复制

步骤一:读系统调用会导致从用户模式到内核模式的上下文切换。第一个复制由DMA引擎执行,它读取磁盘中的文件内容并将其存储到内核地址空间缓冲区中。

第二步:将数据从内核缓冲区复制到用户缓冲区,read系统调用返回。调用的返回导致了从内核返回到用户模式的上下文切换,现在,数据存储在用户地址空间缓冲区中,它可以再次开始向下移动。

第三步:write系统调用导致从用户模式到内核模式的上下文切换,执行第三个复制,将数据再次放入内核地址空间缓冲区中。但是这一次,数据被放入一个不同的缓冲区,这个缓冲区是与套接字相关联的。

第四步:写系统调用返回,创建第四个上下文切换。DMA引擎将数据从内核缓冲区传递到协议engin时,第四个复制发生了独立和异步的情况。你可能会问自己,“你说的独立和异步是什么意思?”在调用返回之前,数据不是传输的吗?”  实际上,调用返回并不能保证传输;它甚至不能保证传输的开始。它只是意味着以太网驱动程序在其队列中有空闲的描述符并接受了我们的传输数据 ,在我们的之前可能会有很多的数据包在排队。除非驱动/硬件实现了优先级环或队列,否则数据将以先入先出的方式传输。(图1中派生的DMA copy表明了最后一个复制可以被延迟的事实)。

正如您所看到的,大量的数据复制并不是真正需要的。可以消除一些重复,以减少开销并提高性能。 作为一名驱动开发人员,我使用的硬件具有一些非常高级的特性。一些硬件可以完全绕过主存,直接将数据传输到另一个设备上。 该特性消除了系统内存中的复制,这是一件很好的事情,但并不是所有的硬件都支持它。还有一个问题是,磁盘上的数据必须重新打包以供网络使用,这带来了一些复杂的问题。 为了消除开销,我们可以从消除内核和用户缓冲区之间的一些复制开始。

消除复制的一种方法是跳过调用read和调用mmap。例如:

tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);

为了更好地了解过程,请查看图2。上下文切换保持不变。

Linux零拷贝原理

图2。调用mmap

第一步:mmap系统调用将文件内容复制到DMA引擎的内核缓冲区中。然后在用户进程中共享缓冲区,而不需要在内核和用户内存空间之间执行任何复制。

第二步:write系统调用导致内核将数据从原始内核缓冲区复制到与套接字关联的内核缓冲区中。

第三步:当DMA引擎将数据从内核套接字缓冲区传递到协议引擎时,第三次复制发生。

通过使用mmap而不是读取,我们将内核必须复制的数据量减少了一半。当大量数据被传输时,这将产生相当好的结果。然而,这种改进并不是没有代价的;使用mmap+write方法时存在一些隐藏的缺陷。当您的内存映射一个文件,然后调用write,而另一个进程截断相同的文件时,您将陷入其中之一。 您的write系统调用将被总线错误信号SIGBUS中断,因为您执行了一个糟糕的内存访问。该信号的默认行为是杀死进程并转储内核——而不是网络服务器最理想的操作。有两种方法可以解决这个问题。

第一种方法是为SIGBUS信号安装一个信号处理程序,然后在处理程序中简单地调用return。通过这样做,write系统调用将返回它在被中断之前所写的字节数,以及errno设置为成功。我必须指出,这将是一个糟糕的解决方案,一个治疗症状,而不是病根的解决方案。因为SIGBUS信号表明这个过程出现了严重的问题,所以我不建议使用这个作为解决方案。

第二个解决方案涉及文件租赁(在Microsoft Windows中称为“机会锁定”)。这是解决这个问题的正确方法。通过使用文件描述符上的租赁,你将在内核上租赁获取一个特定的文件。通过在文件描述符上使用租赁,可以在特定文件上使用内核进行租约。然后可以从内核请求读/写租约。 当另一个进程试图截断正在传输的文件时,内核会向您发送一个实时信号,即RT_SIGNAL_LEASE信号。它告诉您内核正在破坏您在该文件上的写或读租约。在程序访问一个无效的地址并被SIGBUS信号杀死之前,您的write调用会被中断。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;
}

你应该在mmaping文件之前得到你的租约,并且在你完成之后将租约撕毁。这是通过使用F_UNLCK的租约类型调用fcntl F_SETLEASE实现的。

 

Sendfile

在内核版本2.1中,引入了sendfile系统调用,以简化网络和两个本地文件之间的数据传输。sendfile的引入不仅减少了数据复制,还减少了上下文切换。使用它是这样的:

sendfile(socket, file, len);

为了更好地了解过程,请查看图3。

Linux零拷贝原理

图3。用Sendfile替换读和写。

第一步:sendfile系统调用将把文件内容复制到DMA引擎的内核缓冲区中。然后将数据复制到与套接字相关联的内核缓冲区中。

步骤二:当DMA引擎将数据从内核套接字缓冲区传递到协议引擎时,第三次复制发生。

您可能想知道,如果另一个进程截断了我们用sendfile系统调用发送的文件,会发生什么。如果我们不注册任何信号处理程序,sendfile调用只需返回它在被中断之前传输的字节数,而errno将被设置为成功。

如果我们在调用sendfile之前从文件的内核获得一个租约,但是,行为和返回状态完全相同。在sendfile调用返回之前,我们还获得了RT_SIGNAL_LEASE信号。

到目前为止,我们已经能够避免内核生成几个复制,但是我们仍然只剩下一个复制。这个可以避免吗? 当然,在硬件的帮助下。为了消除内核所做的所有数据复制,我们需要一个支持收集操作的网络接口。  这仅仅意味着等待传输的数据不需要在连续的内存中它可以分散在不同的内存位置。在内核版本2.4中,修改了套接字缓冲区描述符以适应这些需求——在Linux下称为零拷贝。这种方法不仅减少了多个上下文切换,还消除了处理器的数据复制。对于用户级应用程序,没有任何更改,因此代码仍然是这样:

sendfile(socket, file, len);

为了更好地了解过程,请查看图4。

Linux零拷贝原理

图4。支持集合的硬件可以从多个内存位置组装数据,从而消除另一个复制。

第一步:sendfile系统调用将把文件内容复制到DMA引擎的内核缓冲区中。

第二步:没有将数据复制到套接字缓冲区中。相反,只有带有关于数据的位置和长度的信息的描述符被追加到套接字缓冲区。DMA引擎直接将数据从内核缓冲区传递到协议引擎,从而消除剩余的最终复制。

因为数据实际上仍然是从磁盘复制到内存和从存储器到导线,有些人可能会认为这不是一个真正的零拷贝。但是,这是从操作系统的角度来看是零拷贝,因为数据不是在内核缓冲区之间复制的。当使用零拷贝时,除了复制避免之外,还可以使用其他性能优势,例如更少的上下文切换、更少的CPU数据缓存污染和没有CPU校验和计算。

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/93945.html

(0)
上一篇 2021年8月16日 11:46
下一篇 2021年8月16日 11:57

相关推荐

发表回复

登录后才能评论