在本实验中,你将为网络接口卡 (NIC)编写一个 xv6 设备驱动程序。
获取实验的 xv6 源代码并切换到 net 分支:
$ git fetch $ git checkout net $ make clean
在编写代码之前,你可能会发现回顾 xv6 书籍中的"第5章:中断和 设备驱动程序"有所帮助。
你将使用一个名为 E1000 的网络设备来处理网络 通信。对 xv6(以及你编写的驱动程序)来说,E1000 看起来像是一块真实的 硬件,连接到一个真实的以太网局域网(LAN)。实际上, 你的驱动程序将与 QEMU 提供的仿真 E1000 通信, 连接到同样由 QEMU 仿真的局域网。在这个仿真的局域网上,xv6("客户机") 的 IP 地址为 10.0.2.15。 QEMU 还安排运行 QEMU 的计算机以 IP 地址 10.0.2.2 出现在局域网上。 当 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 的初始化 代码以及用于发送和接收数据包的 空函数,你需要填写这些函数。 kernel/e1000_dev.h 包含 E1000 定义的 寄存器和标志位的定义, 在 Intel E1000 软件开发人员手册中有描述。 kernel/net.c 和 kernel/net.h 包含一个简单的网络栈,实现了 IP、UDP 和 ARP 协议。 这些文件还包含一个灵活的数据结构代码,用于保存数据包, 称为 mbuf。 最后,kernel/pci.c 包含在 xv6 启动时 搜索 PCI 总线上 E1000 卡的代码。
你的任务是完成 e1000_transmit() 和 e1000_recv(), 两者都在 kernel/e1000.c 中, 使驱动程序能够发送和接收数据包。 当 make grade 显示你的 解决方案通过所有测试时,你就完成了。
在编写代码时,你会发现需要参考 E1000 软件开发人员手册。以下章节可能特别有帮助:
浏览 E1000 软件开发人员手册。 该手册涵盖了几个密切相关的以太网控制器。 QEMU 仿真的是 82540EM。现在浏览第2章以了解 该设备。要编写驱动程序,你需要熟悉第3章 和第14章,以及4.1(但不包括4.1的子节)。 你还需要将第13章作为参考。其他章节主要涵盖 你的驱动程序无需与之交互的 E1000 组件。 先不要担心细节;只需了解文档的结构,以便以后 查找内容。E1000 有许多高级功能, 其中大部分你可以忽略。完成本实验只需要一小组基本功能。
我们在 e1000.c 中提供的 e1000_init() 函数 配置 E1000 从 RAM 读取要发送的数据包, 并将接收到的数据包写入 RAM。这种技术称为 DMA(直接内存访问), 指的是 E1000 硬件 直接对 RAM 进行数据包的写入和读取。
由于数据包可能成批到达,速度超过驱动程序的处理能力, e1000_init() 为 E1000 提供了多个缓冲区, E1000 可以将数据包写入其中。E1000 要求这些 缓冲区由 RAM 中的"描述符"数组来描述;每个 描述符包含 RAM 中的一个地址,E1000 可以在该地址 写入接收到的数据包。 struct rx_desc 描述了描述符格式。 描述符数组称为 接收环或接收队列。它是一个环形结构,当 网卡或驱动程序到达数组末尾时,会回到 开头。e1000_init() 使用 mbufalloc() 分配 mbuf 数据包缓冲区 供 E1000 进行 DMA 写入。 还有一个发送环, 驱动程序将希望 E1000 发送的数据包放入其中。 e1000_init() 将两个环的大小配置为 RX_RING_SIZE 和 TX_RING_SIZE。
当 net.c 中的网络栈需要发送数据包时, 它调用 e1000_transmit(),传入一个 包含待发送数据包的 mbuf。 你的发送代码必须将指向数据包数据的 指针放入 TX(发送)环中的描述符中。 struct tx_desc 描述了描述符格式。你需要确保每个 mbuf 最终被释放,但只有在 E1000 完成 发送数据包之后(E1000 在描述符中设置 E1000_TXD_STAT_DD 位来指示这一点)。
当 E1000 从以太网接收到每个数据包时,它首先通过 DMA 将数据包写入下一个 RX(接收)环描述符指向的 mbuf,然后 产生中断。你的 e1000_recv() 代码必须扫描 RX 环并 通过调用 net_rx() 将每个新数据包的 mbuf 传递给网络栈(net.c 中)。 然后你需要分配一个新的 mbuf 并将其放入描述符中,以便当 E1000 再次到达 RX 环中的该位置时,它能找到一个新鲜的缓冲区来 DMA 写入新的 数据包。
除了在 RAM 中读写描述符环之外, 你的驱动程序还需要 通过内存映射控制寄存器与 E1000 交互, 以检测何时有接收到的数据包可用,并 通知 E1000 驱动程序已在某些 TX 描述符中填入了待发送的数据包。 全局变量 regs 保存指向 E1000 第一个控制寄存器的 指针;你的驱动程序可以通过将 regs 作为数组索引来访问其他寄存器。 你特别需要使用 E1000_RDT 和 E1000_TDT 索引。
要测试你的驱动程序, 在一个窗口中运行 make server, 在另一个窗口中运行 make qemu, 然后在 xv6 中运行 nettests。 nettests 中的第一个测试尝试向主机 操作系统发送一个 UDP 数据包,目标地址为 make server 运行的程序。 如果你还没有完成实验,E1000 驱动程序 不会实际发送数据包,也不会发生什么。
完成实验后, E1000 驱动程序将发送数据包, QEMU 会将其传送到你的主机计算机,make server 会看到它,然后发送一个响应数据包, E1000 驱动程序和 nettests 将看到响应数据包。 但在主机发送回复之前,它会向 xv6 发送一个 "ARP"请求数据包以获取其 48位以太网地址,并期望 xv6 用 ARP 回复进行响应。一旦你完成了 E1000 驱动程序的工作, kernel/net.c 就会处理这个问题。 如果一切顺利,nettests 将打印 testing ping: OK, make server 将打印 a message from xv6!。
tcpdump -XXnr packets.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 还执行其他一些测试,最终发送一个 DNS 请求到 Google 的一个名称服务器 (通过真实的互联网)。你应确保你的代码通过这些 测试,之后你应该看到以下输出:
$ 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 也同意你的解决方案通过了。
实现 e1000_transmit 的一些提示:
实现 e1000_recv 的一些提示:
你需要锁来处理 xv6 可能 从多个进程使用 E1000,或者在中断到达时 可能在内核线程中使用 E1000 的情况。
>
实验到此完成。确保你通过了所有 make
grade 测试。如果本实验有问题,别忘了在 answers-lab-name.txt 中写下你的
答案。提交你的更改
(包括添加 answers-lab-name.txt)并在实验
目录中输入 make handin 来提交你的实验。
创建一个新文件 time.txt,在其中放入一个整数,即
你在本实验上花费的小时数。别忘了 git add 和
git commit 该文件。
提交实验
花费时间
提交
你将使用提交
网站提交你的作业。在提交任何作业或实验之前,
你需要从提交网站请求一次 API 密钥。
在提交你对实验的最终更改后,输入 make handin 来提交你的实验。
$ git commit -am "ready to submit my lab"
[util c2e3c8b] ready to submit my lab
2 files changed, 18 insertions(+), 2 deletions(-)
$ make handin
tar: Removing leading `/' from member names
Get an API key for yourself by visiting https://6828.scripts.mit.edu/2020/handin.py/
Please enter your API key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 79258 100 239 100 79019 853 275k --:--:-- --:--:-- --:--:-- 276k
$
make handin 会将你的 API 密钥存储在 myapi.key 中。如果你需要
更改 API 密钥,只需删除此文件并让 make handin
重新生成(myapi.key 不能包含换行符)。
如果你运行 make handin 时有未提交的更改或 未跟踪的文件,你将看到类似于以下的输出:
M hello.c ?? bar.c ?? foo.pyc Untracked files will not be handed in. Continue? [y/N]检查上述行并确保你的实验解决方案所需的 所有文件都被跟踪,即不在以 ?? 开头的行中列出。 你可以使用 git add filename 让 git 跟踪你创建的新文件。
如果 make handin 无法正常工作, 请尝试修复 curl 或 Git 命令的问题。 或者你可以运行 make tarball。 这会为你制作一个 tar 文件,然后你可以通过我们的 网页界面上传。
以下挑战练习的一些好处只能在真实的 高性能硬件(即基于 x86 的计算机)上 测量/测试。
如果你尝试了挑战问题,无论是否与网络相关, 请告诉课程工作人员!