实验:网络
在本实验中,您将为网络接口卡 (NIC) 编写 xv6 设备驱动程序。
获取实验室的 xv6 源代码并查看分支net:
$ git fetch
$ git checkout net
$ make clean
背景
在编写代码之前,您可能会发现查看xv6书的“第 5 章:中断和设备驱动程序”很有帮助。
您将使用名为 E1000 的网络设备来处理网络通信。对于 xv6(以及您编写的驱动程序)来说,E1000 看起来就像一个连接到真实以太网局域网 (LAN) 的真实硬件。事实上,您的驱动程序将与之通信的 E1000 是 qemu 提供的仿真,连接到也由 qemu 仿真的 LAN。在此模拟 LAN 上,xv6(“来宾”)的 IP 地址为 10.0.2.15。 Qemu 还安排运行 qemu 的计算机出现在 IP 地址为 10.0.2.2 的 LAN 上。当 xv6 使用 E1000 将数据包发送到 10.0.2.2 时,qemu 会将数据包传送到运行 qemu(“主机”)的(真实)计算机上的相应应用程序。
您将使用 QEMU 的“用户模式网络堆栈”。 QEMU 的文档在此处提供了有关用户模式堆栈的更多信息。我们更新了 Makefile 以启用 QEMU 的用户模式网络堆栈和 E1000 网卡。
Makefile 将 QEMU 配置为将所有传入和传出数据包记录到packets.pcap实验室目录中的文件中。查看这些记录可能会有助于确认 xv6 正在传输和接收您期望的数据包。显示记录的数据包:
tcpdump -XXnr packets.pcap
我们已为本实验的 xv6 存储库添加了一些文件。该文件kernel/e1000.c包含 E1000 的初始化代码以及用于发送和接收数据包的空函数(您将填写这些函数)。包含由 E1000 定义并在《英特尔 E1000软件开发人员手册》kernel/e1000_dev.h中描述的寄存器和标志位的定义 。并包含一个实现IP、UDP和ARP协议的简单网络堆栈。这些文件还包含用于保存数据包的灵活数据结构的代码,称为.最后,包含当 xv6 启动时在 PCI 总线上搜索 E1000 卡的代码。kernel/net.c``kernel/net.h``mbuf``kernel/pci.c
你的工作(困难)
你的工作是完成中的e1000_transmit()和,以便驱动程序可以发送和接收数据包。当您的解决方案通过所有测试时,您就完成了。e1000_recv()``kernel/e1000.c``make grade
在编写代码时,您会发现自己正在参考 E1000软件开发人员手册。以下部分可能特别有帮助:
- 第 2 节很重要,概述了整个器件。
- 3.2 节给出了数据包接收的概述。
- 3.3 节与 3.4 节一起概述了数据包传输。
- 第 13 节概述了 E1000 使用的寄存器。
- 第 14 节可以帮助您理解我们提供的初始化代码。
浏览 E1000软件开发人员手册。本手册涵盖了几个密切相关的以太网控制器。 QEMU 模拟 82540EM。现在浏览第 2 章来感受一下该设备。要编写驱动程序,您需要熟悉第 3 章和第 14 章以及 4.1(尽管不是 4.1 的小节)。您还需要使用第 13 章作为参考。其他章节主要介绍驱动程序无需与之交互的 E1000 组件。一开始不要担心细节;只需感受一下文档的结构,以便稍后查找。 E1000 有许多高级功能,其中大部分您可以忽略。完成本实验只需要一小组基本功能。
e1000_init()我们为您提供的功能将E1000e1000.c配置为从 RAM 中读取要发送的数据包,并将接收到的数据包写入 RAM。这种技术称为 DMA,即直接内存访问,指的是 E1000 硬件直接向 RAM 写入数据包或从 RAM 读取数据包的事实。
由于数据包突发的到达速度可能比驱动程序处理它们的速度快,因此e1000_init()为 E1000 提供了多个缓冲区,E1000 可以将数据包写入其中。 E1000 要求这些缓冲区由 RAM 中的“描述符”数组来描述;每个描述符都包含 RAM 中的一个地址,E1000 可以将接收到的数据包写入其中。struct rx_desc描述描述符格式。描述符数组称为接收环或接收队列。从某种意义上说,它是一个圆环,当卡或驱动程序到达阵列的末尾时,它会回绕到开头。使用 为 E1000 分配数据包e1000_init()缓冲区到 DMA 。还有一个传输环,驱动程序应将其希望 E1000 发送的数据包放入其中。将两个环配置为大小和。mbuf``mbufalloc()``e1000_init()``RX_RING_SIZE``TX_RING_SIZE
当网络堆栈net.c需要发送数据包时,它会调用e1000_transmit()保存要发送的数据包的 mbuf。您的发送代码必须在 TX(发送)环的描述符中放置一个指向数据包数据的指针。struct tx_desc描述描述符格式。您需要确保每个 mbuf 最终都被释放,但只有在 E1000 完成数据包传输之后(E1000E1000_TXD_STAT_DD在描述符中设置位来指示这一点)。
当 E1000 从以太网接收到每个数据包时,它会将数据包 DMA 到下addr一个 RX(接收)环描述符中指向的内存。如果 E1000 中断尚未挂起,则一旦启用中断,E1000 就会要求 PLIC 立即传送中断。您的e1000_recv()代码必须扫描 RX 环并net.c通过调用将每个新数据包的 mbuf 传递到网络堆栈(在 中)net_rx()。然后,您需要分配一个新的 mbuf 并将其放入描述符中,以便当 E1000 再次到达 RX 环中的该点时,它会找到一个新的缓冲区来将新数据包 DMA 到其中。
除了在 RAM 中读写描述符环之外,您的驱动程序还需要通过其内存映射控制寄存器与 E1000 进行交互,以检测接收到的数据包何时可用,并通知 E1000 驱动程序已填充一些 TX 描述符与要发送的数据包。全局变量保存regs指向E1000的第一个控制寄存器的指针;您的驱动程序可以通过索引regs为数组来获取其他寄存器。您需要特别使用E1000_RDT索引。E1000_TDT
要测试您的驱动程序,请make server在一个窗口中运行,在另一个窗口中运行make qemu,然后nettests在 xv6 中运行。第一个测试nettests尝试将 UDP 数据包发送到主机操作系统,寻址到正在make server运行的程序。如果您尚未完成实验,E1000 驱动程序实际上不会发送数据包,并且不会发生任何事情。
完成实验后,E1000 驱动程序将发送数据包,qemu 将其传送到您的主机,您make server将看到它,它将发送一个响应数据包,然后 E1000 驱动程序nettests将看到响应数据包。然而,在主机发送回复之前,它会向 xv6 发送一个“ARP”请求数据包以找出其 48 位以太网地址,并期望 xv6 以 ARP 回复进行响应。kernel/net.c一旦您完成了 E1000 驱动程序的工作,我们就会处理这个问题。如果一切顺利,nettests将打印testing ping: OK,并将make server打印a message from xv6!。
tcpdump -XXnr packet.pcap 应该产生如下所示的输出:
reading from file packets.pcap, link-type EN10MB (Ethernet)
15:27:40.861988 IP 10.0.2.15.2000 > 10.0.2.2.25603: UDP, length 19
0x0000: ffff ffff ffff 5254 0012 3456 0800 4500 ......RT..4V..E.
0x0010: 002f 0000 0000 6411 3eae 0a00 020f 0a00 ./....d.>.......
0x0020: 0202 07d0 6403 001b 0000 6120 6d65 7373 ....d.....a.mess
0x0030: 6167 6520 6672 6f6d 2078 7636 21 age.from.xv6!
15:27:40.862370 ARP, Request who-has 10.0.2.15 tell 10.0.2.2, length 28
0x0000: ffff ffff ffff 5255 0a00 0202 0806 0001 ......RU........
0x0010: 0800 0604 0001 5255 0a00 0202 0a00 0202 ......RU........
0x0020: 0000 0000 0000 0a00 020f ..........
15:27:40.862844 ARP, Reply 10.0.2.15 is-at 52:54:00:12:34:56, length 28
0x0000: ffff ffff ffff 5254 0012 3456 0806 0001 ......RT..4V....
0x0010: 0800 0604 0002 5254 0012 3456 0a00 020f ......RT..4V....
0x0020: 5255 0a00 0202 0a00 0202 RU........
15:27:40.863036 IP 10.0.2.2.25603 > 10.0.2.15.2000: UDP, length 17
0x0000: 5254 0012 3456 5255 0a00 0202 0800 4500 RT..4VRU......E.
0x0010: 002d 0000 0000 4011 62b0 0a00 0202 0a00 .-....@.b.......
0x0020: 020f 6403 07d0 0019 3406 7468 6973 2069 ..d.....4.this.i
0x0030: 7320 7468 6520 686f 7374 21 s.the.host!
您的输出看起来会有所不同,但它应该包含字符串“ARP,Request”,“ARP,Reply”,“UDP”,“a.message.from.xv6”和“this.is.the.host”。
nettests执行一些其他测试,最终通过(真实)互联网向 Google 的一台名称服务器发送 DNS 请求。您应该确保您的代码通过所有这些测试,之后您应该看到以下输出:
$ nettests
nettests running on port 25603
testing ping: OK
testing single-process pings: OK
testing multi-process pings: OK
testing DNS
DNS arecord for pdos.csail.mit.edu. is 128.52.129.126
DNS OK
all tests passed.
您应该确保make grade您的解决方案获得通过。
提示
首先将 print 语句添加到e1000_transmit()and e1000_recv(),然后运行make serverand (在 xv6 中)nettests。您应该从打印语句中看到nettests生成对e1000_transmit.
实施的一些提示e1000_transmit:
- 首先,通过读取控制寄存器,向 E1000 询问其期望下一个数据包的 TX 环索引
E1000_TDT。 - 然后检查环是否溢出。如果
E1000_TXD_STAT_DD索引的描述符中没有设置E1000_TDT,则 E1000 还没有完成相应的上一次传输请求,因此返回错误。 - 否则,用于
mbuffree()释放从该描述符传输的最后一个 mbuf(如果有)。 - 然后填写描述符。
m->head指向内存中数据包的内容,m->len是数据包长度。设置必要的 cmd 标志(请参阅 E1000 手册中的第 3.3 节)并隐藏指向 mbuf 的指针以供以后释放。 E1000_TDT最后,通过对modulo加 1 来更新环位置TX_RING_SIZE。- 如果
e1000_transmit()成功将 mbuf 添加到环中,则返回 0。失败时(例如,没有可用于传输 mbuf 的描述符),则返回 -1,以便调用者知道要释放 mbuf。
实施的一些提示e1000_recv:
E1000_RDT首先通过获取控制寄存器并加一模来向 E1000 询问下一个等待接收的数据包(如果有)所在的环索引RX_RING_SIZE。- 然后通过检查描述符部分
E1000_RXD_STAT_DD中的位来检查新数据包是否可用。status如果没有,就停下来。 - 否则,将 mbuf 更新
m->len为描述符中报告的长度。使用 将 mbuf 传递到网络堆栈net_rx()。 - 然后使用 分配一个新的 mbuf
mbufalloc()来替换刚刚给 的 mbufnet_rx()。将其数据指针 (m->head) 编程到描述符中。将描述符的状态位清零。 - 最后,将
E1000_RDT寄存器更新为最后处理的环描述符的索引。 e1000_init()使用 mbufs 初始化 RX 环,您会想看看它是如何做到这一点的,也许还想借用代码。- 在某个时刻,到达的数据包总数将超过环大小 (16);确保你的代码可以处理这个问题。
您需要锁来应对 xv6 可能从多个进程使用 E1000,或者当中断到达时可能在内核线程中使用 E1000 的可能性。
可选挑战:
下面的挑战练习的一些好处只能在真实的高性能硬件(这意味着基于 x86 的计算机)上进行测量/测试。
- 在本实验中,网络堆栈使用中断来处理入口数据包处理,但不处理出口数据包处理。一种更复杂的策略是在软件中对出口数据包进行排队,并且在任何时候仅向 NIC 提供有限数量的数据包。然后,您可以依靠 TX 中断来重新填充传输环。使用这种技术,可以对不同类型的出口流量进行优先级排序。 (简单的)
- 提供的网络代码仅部分支持 ARP。实现完整的ARP 缓存并将其连接到
net_tx_eth(). (缓和) - E1000支持多个RX和TX环。配置 E1000 为每个核心提供一个环对,并修改网络堆栈以支持多个环。这样做有可能增加网络堆栈可支持的吞吐量并减少锁争用。 (中等),但难以测试/测量
sockrecvudp()使用单链表来查找目标套接字,效率较低。尝试使用哈希表和 RCU 来提高性能。 (简单),但是认真的实施将很难测试/测量- ICMP可以提供失败的网络流的通知。检测这些通知并通过套接字系统调用接口将它们作为错误传播。
- E1000支持多种无状态硬件卸载,包括校验和计算、RSC和GRO。使用其中一项或多项卸载来提高网络堆栈的吞吐量。 (中等),但难以测试/测量
- 本实验室中的网络堆栈很容易受到活锁的影响。使用讲座中的材料和阅读作业,设计并实施解决方案来解决这个问题。 (中等),但很难测试。
- 为 xv6 实现 UDP 服务器。 (缓和)
- 实现一个最小的 TCP 堆栈并下载网页。 (难的)
如果您追求挑战性问题,无论是否与网络相关,请告知课程工作人员!