> For the complete documentation index, see [llms.txt](https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec21-networking-robert/21.7-ring-buffer.md).

# 21.7 Ring Buffer

对于今天的论文，了解packet的控制流程是如何工作的还是比较重要，这里的控制流程与前一节介绍的分层网络协议栈还不太一样。

有关网络协议栈，通常会有多个独立的actor会处理packet，解析packet并生成输出。出于各种各样的原因，这些不同的actor之间是解耦的，这样它们可以并发的运行，并且连接不同的packet队列。这对于今天的论文来说，是非常重要的前提。

现在我们有了一张网卡，有了一个系统内核。当网卡收到了一个packet，它会生成一个中断。系统内核中处理中断的程序会被触发，并从网卡中获取packet。因为我们不想现在就处理这个packet，中断处理程序通常会将packet挂在一个队列中并返回，packet稍后再由别的程序处理。所以中断处理程序这里只做了非常少的工作，也就是将packet从网卡中读出来，然后放置到队列中。

在一个传统的网络协议栈中，我们之所以想要快速的将packet从网卡中读出并存放于软件队列中，是因为通常来说网卡中用来存储packet的内存都非常小，而在计算机的RAM中，会有GB级别的内存，所以计算机的内存要大得多。如果有大量的packet发送到网卡，网卡可能会没有足够的内存来存储packet，所以我们需要尽快将packet拷贝到计算机的内存中。

![](/files/-MOZuep5mLHvaJeKTX7w)

之后，在一个独立的线程中，会有一个叫做IP processing thread的程序。它会读取内存中的packet队列，并决定如何处理每一个packet。其中一个可能是将packet向上传递给UDP，再向上传递给socket layer的某个队列中，最后等待某个应用程序来读取。通常来说，这里的向上传递实际上就是在同一个线程context下的函数调用。

![](/files/-MOZwVqlxtfTCWsO9qcN)

另一种可能就是，这个主机实际上是个路由器，packet从一个网卡进来，经过路由需要从另一个网卡出去。通过例如Linux操作系统构建路由器是非常常见的。如果你买一个wifi路由器，或者一个有线调制解调器，非常有可能里面运行的就是Linux系统，并且使用了Linux网络协议栈，因为Linux的协议栈实现了完整的路由协议。所以，如果IP process thread查看了packet的目的IP地址，并决定将packet从另一个网卡转发出去，它会将packet加入到针对发送网卡的发送队列中。

通常来说网卡会有发送中断程序，当网卡发送了一个packet，并且准备好处理更多packet的时候，会触发一个中断。所以网卡的发送中断也很重要。

![](/files/-MO_0GYkyi3KxBqCMUuw)

在这个结构中，有一点非常重要，这里存在一些并发的组件，它们以不同的方式调度。中断处理程序由网卡的发送或者接受中断触发。IP processing thread就是一个内核线程。在一个处理器上，IP processing thread不能与中断处理程序同时运行，因为中断处理程序的优先级最高，不过在多核处理器上，并发度可能会更高。最后，应用程序要能够读取socket layer中的packet，应用程序又是另一个独立调度的组件。所有这些组件都会参与到CPU的调度中。

缓存队列经常会被提到，在上图中，总共有3个队列。这里的队列的作用是，一个独立的组件会向队列中添加packet，其他的组件会从队列中读取packet。在网络系统中，这样的队列很常见，主要出于以下几个原因：

* 其中一个原因是可以应对短暂的大流量。比如，IP processing thread只能以特定的速度处理packet，但是网卡可能会以快得多的速度处理packet。对于短暂的大流量，我们想要在某个位置存储这些packet，同时等待IP processing来处理它们，这是网卡的接收方向。
* 在网卡的发送方向，我们可能需要在队列中存储大量的packet，这样网卡可以在空闲的时候一直发送packet。有的时候100%利用网卡的发送性能还是很重要的。
* 第三个原因是，队列缓存可以帮助组件之间解耦。我们不会想要IP processing thread或者应用程序知道中断处理程序的具体实现。在一个传统的操作系统中，IP processing thread并不必须知道中断是什么时候发生，或者应用程序怎么运行的。

> 学生提问：同一个网卡可以即是接收方又是发送方吗？
>
> Robert教授：可以的。比如说我的笔记本只有一个网卡连接到了wifi，packet会从一个网卡进入并发出。双网卡通常用在路由器中。比如说我家里的wifi路由器，它就有两张网卡，其中一个网卡连接到线缆并进一步连接到整个互联网，另一个网卡是wifi网卡。有很多服务器也有多个网卡，尤其是对于web服务器来说，会有一个网卡连接互联网，另一个网卡连接你的私有的敏感的数据库信息。两个网卡连接的是完全不同的网络。
>
> 学生提问：所以多网卡的场景在于想要连接不同的网络？
>
> Robert教授：是的。如果你想要连接不同的网络，那么你需要有多块网卡。

我想再讨论一下当packet送到网卡时，网卡会做什么操作？这与networking lab非常相关。对于一个网卡的结构，会有一根线缆连接到外面的世界。网卡会检查线缆上的电信号，并将电信号转换成packet。网卡会接入到一个主机上，主机会带有网卡的驱动软件。我们需要将网卡解码出来的packet传递给主机的内存，这样软件才能解析packet。

网卡内有许多内置的内存，当packet到达时，网卡会将packet存在自己的缓存中，并向主机发送中断，所以网卡内部会有一个队列。而主机的驱动包含了一个循环，它会与网卡交互，并询问当前是否缓存了packet。如果是的话，主机的循环会逐字节的拷贝packet到主机的内存中，再将内存中的packet加到一个队列中。这是我们今天要看的论文中网卡的工作方式：网卡驱动会负责拷贝网卡内存中的数据到主机内存。这在30年前还是有意义的，但是今天通过驱动中的循环来从硬件拷贝数据是非常慢的行为。即使是在同一个计算机上，外设到CPU之间的距离也非常的长，所以它们之间的交互需要的时间比较长。所以人们现在不会这么设计高速接口了。

接下来我将讨论一下E1000网卡的结构，这是你们在实验中要使用的网卡。E1000网卡会监听网线上的电信号，但是当收到packet的时候，网卡内部并没有太多的缓存，所以网卡会直接将packet拷贝到主机的内存中，而内存中的packet会等待驱动来读取自己。所以，网卡需要事先知道它应该将packet拷贝到主机内存中的哪个位置。E1000是这样工作的，主机上的软件会格式化好一个DMA ring，ring里面存储的是packet指针。所以，DMA ring就是一个数组，里面的每一个元素都是指向packet的指针。

![](/files/-MOd0yiW8bjGyY--gbxD)

当位于主机的驱动初始化网卡的时候，它会分配一定数量，例如16个1500字节长度的packet buffer，然后再创建一个16个指针的数组。为什么叫ring呢？因为在这个数组中，如果用到了最后一个buffer，下一次又会使用第一个buffer。主机上的驱动软件会告诉网卡DMA ring在内存中的地址，这样网卡就可以将packet拷贝到内存中的对应位置。

![](/files/-MOd2LVKIFr5VI2HQ1wa)

当网卡收到packet时，网卡还会记住当前应该在DMA ring的哪个位置并通过DMA将packet传输过去。

![](/files/-MOd2z1-dzep2BSuspqK)

传输完成之后，网卡会将内部的记录的指针指向DMA ring的下一个位置，这样就可以拷贝下一个packet。

![](/files/-MOd3Lz5898QQGBfxhlf)

刚才说的都是接收packet，对应的是RX ring。类似的，驱动还会设置好发送buffer，也就是TX ring。驱动会将需要网卡传输的packet存储在 TX ring中，网卡也需要知道TX ring的地址。

![](/files/-MOd3sJdQoxjV31LpKgD)

你们在networking lab中的主要工作就是写驱动来处理这些ring。

> 学生提问：E1000与生产环境的高性能场景使用的网卡有什么区别吗？
>
> Robert教授：E1000曾经是最优秀的网卡，没有之一，并且它也曾经使用在生产环境中，但这是很多年前的事了。现代的网卡更加的“智能”，但是我们这里介绍的DMA ring结构并没有太多的变化，现在你仍然可以发现网卡使用DMA来传输packet，内存中对应的位置是由ring buffer的位置决定。现代的网卡更加“智能”在以下几个方面：
>
> * E1000只能与一个RX ring传输数据，而现代网卡可以与多个RX ring同时传输数据。比如说你可以告诉一张现代的网卡，将接受到的packet分别传输给21个RX ring，网卡会根据packet的内容，决定将packet送到哪个RX ring。人们在很多地方都使用了这个特性，比如说在主机上运行了多个虚拟机，你可以使用这个特性将虚拟机对应的packet送到虚拟机对应的RX ring中，这样虚拟机可以直接读取相应的RX ring。（注，也就是网卡多队列）
> * 现代网卡更加“智能”的体现是，它们会完成一些TCP的处理，最常见的就是校验和计算。（注，各种TCP offload）
>
> 所以，现代的网卡有与E1000相似的地方，但是更加的“智能”。
>
> 学生提问：在接下来的networking lab中，IP层和驱动之间没有队列，是吗？
>
> Robert教授：是的，lab中的网络栈已经被剥离到了最小，它比实际的网络协议栈简单的多
>
> 学生提问：那这样的话，性能会不会很差？
>
> Robert教授：我不知道，我没有在实际环境中运行过这些代码。在写networking lab的代码时，我们没有关注过性能。大多数情况下，性能不是问题，lab中的代码可以完成一个网络协议栈95%的功能，例如处理多网卡，处理TCP。
>
> 学生提问：为了让网卡能支持DMA，需要对硬件做一些修改吗？在E1000之前的网卡中，所有的数据传输都是通过CPU进行传输。
>
> Robert教授：我们在介绍E1000之前的网卡时，网卡并不能访问内存。我认为这里最重要的问题是，当网卡想要使用主机内存中的某个地址时，虚拟内存地址是如何翻译的。我不知道这里是如何工作的。
>
> 网卡通过总线，并经过一些可编程芯片连接到了DRAM，我认为在现代的计算机中，你可以设置好地址翻译表，这样网卡可以使用虚拟内存地址，虚拟内存地址会由网卡和DRAM之间的硬件翻译，这对于一些场景还是很有价值的。
>
> 另一方面，如果网卡需要读写一些内存地址，而内存数据现在正在CPU的cache中，那么意味着内存对应的最新数据位于CPU cache中，而不是在RAM。这种情况下，当网卡执行DMA时，我们希望网卡能读取CPU的cache而不是RAM。在Intel的机器上，有一些精心设计的机制可以确保当网卡需要从内存读取数据而最新的内存数据在CPU cache中时，CPU cache而不是RAM会返回数据。一些软件基于这种机制来获得高性能。对于写数据同样的也适用，网卡可以直接将数据写到CPU cache中，这样CPU可以非常快的读到数据。
>
> 我们介绍的E1000的结构非常简单，但是实际中的网卡机制非常的复杂。
