2.Lab-7 Networking
学习流程
QEMU模仿的是82540EM
一、LAB说明
1. 背景
将使用一个名为 E1000 的网卡来处理网络通信。xv6是qemu模拟的硬件,连接到一个同样由 QEMU 模拟的 LAN。在这个仿真LAN上,xv6的IP地址是10.0.2.15。同时,Qemu将运行qemu的宿主的ip地址设置为 10.0.2.2。当xv6向10.0.2.2发送数据包时,Qemu会将数据包传递给宿主上的对应端口的程序。
将使用 QEMU 的“用户模式网络栈”。QEMU 的文档在这里有更多关于用户模式栈的信息。我们已经更新了 Makefile 以启用 QEMU 的用户模式网络栈和 E1000 网卡。
Makefile 配置 QEMU 将所有传入和传出的数据包记录到目录中的 packets.pcap 文件。查看这些记录有助于确认 xv6 正在传输和接收的数据包。要显示记录的数据包:
tcpdump -XXnr packets.pcap
文件 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 网卡的代码。
2.要做的事
LAB的任务是完成 kernel/e1000.c 中的 e1000_transmit() 和 e1000_recv() 函数,以传输和接收数据包。当 make grade 显示您的解决方案通过了所有测试时,LAB就完成了。
在编写代码时,可能需要参考 E1000 手册。以下部分有帮助:
- 第 2 章是必不可少的,提供了整个设备的概述。
- 第 3.2 章提供了数据包接收的概述。
- 第 3.3 章提供了数据包传输的概述,以及第 3.4 章。
- 第 13 章提供了 E1000 使用的寄存器的概述。
- 第 14 章可能有助于您理解我们提供的 init 代码。
-
浏览E1000手册。
QEMU 模拟的是
82540EM
-
快速浏览第 2 章,以了解设备的情况。
-
要编写驱动程序,需要熟悉3 、14 章,以及第 4.1 章。
还需要使用第 13 章作为参考。
-
其他章节不需要看。
-
-
e1000.c
中的e1000_init()
配置 DMA,网卡可直接从 RAM 中读取要传输的数据包,或将接收到的数据包写入 RAM。数据包的传输速度可能比驱动程序更快,
e1000_init()
提供了多个缓冲区。每个缓冲区包含一个 RAM 地址,可以在缓冲区内写入一个接收到的数据包。struct rx_desc
描述了接收缓冲区的格式。缓冲区数组是环形结构,当驱动程序到达数组末尾时,会回到开头。e1000_init()
使用mbufalloc()
为mbuf数据包缓冲区分配空间。还有一个传输环,驱动程序应将希望发送的数据包放置其中。两个环的大小为 RX_RING_SIZE 和 TX_RING_SIZE。 -
当 net.c 中的网络堆栈需要发送一个数据包时,会调用
e1000_transmit()
,同时将要发送数据包mbuf作为参数传入。您的传输代码必须将数据包的指针放入 TX(传输)环中的描述符中。struct tx_desc
描述了描述符的格式。您需要确保在 E1000 完成传输数据包后,释放mbuf(E1000 在描述符中设置 E1000_TXD_STAT_DD 位以指示这一点)。 -
E1000接收到数据包时,会将数据包 DMA 到下一个
struct rx_desc
中 addr 指向的内存中。如果 E1000 无法响应,E1000 会请求 PLIC 一旦中断启用就尽快传递。您的 e1000_recv() 代码必须扫描 RX 环,并通过调用 net_rx() 将每个新数据包的 mbuf 传递给网络堆栈(在 net.c 中)。然后需要分配一个新的 mbuf 并将其放入描述符中,以便当 E1000 再次到达 RX 环中的该点时,它找到一个新鲜的缓冲区,以便 DMA 一个新的数据包。除了在 RAM 中读写描述符环之外,您的驱动程序还需要通过控制寄存器与 E1000 交互,检测接收到的数据包是否可用,并通知 E1000 驱动程序已填充了一些 TX 描述符以发送数据包。可通过全局变量 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 将打印测试 ping: OK,make server 将打印来自 xv6 的消息!
3. 提示
首先,在 e1000_transmit()
和 e1000_recv()
中添加打印语句,然后运行 make server 和 (在 xv6 中) nettests。可以从打印语句中看到 nettests 生成了对 e1000_transmit 的调用。
e1000_transmit
的一些提示:
-
首先读取
E1000_TDT
控制寄存器,得到下一个TX ring index
,通过index
获取对应的struct tx_desc tx_ring
,以下简称ring
。 -
然后检查环是否溢出。如果获取到的
ring
的status
不是E1000_TXD_STAT_DD
,表明传输尚未完成,返回错误。 -
否则,使用
mbuffree()
释放从该描述符传输的最后一个 mbuf(如果有)。 -
然后填充
ring
。m->head
指向内存中数据包的地址,m->len
是数据包长度。设置必要的 cmd 标志(请参阅 E1000 手册中的第 3.3 节),并存储 mbuf 指针以供后续释放。 -
最后,通过对
E1000_TDT
模TX_RING_SIZE
加 1 来更新环位置。 -
如果
e1000_transmit()
成功地将mbuf添加到环中,则返回 0。如果失败(例如没有可用的描述符来传输 mbuf),则返回 -1,以便调用者知道要释放 mbuf。
e1000_recv
的一些提示:
-
获取 E1000_RDT 控制寄存器并加一模
RX_RING_SIZE
,询问 E1000 下一个等待接收的数据包(如果有的话)位于哪个环索引。 -
然后通过检查描述符状态部分中的 E1000_RXD_STAT_DD 位来检查是否有新数据包可用。如果没有,停止。
否则,将 mbuf 的 m->len 更新为描述符中报告的长度。使用 net_rx() 将 mbuf 传递给网络堆栈。
-
使用 mbufalloc() 分配一个新的 mbuf 来替换刚刚给 net_rx() 的那个。将其数据指针(m->head)编程到描述符中。清除描述符的状态位为零。
-
更新 E1000_RDT 注册表为处理的最后一个环描述符的索引。
-
e1000_init() 使用 mbuf 初始化 RX 环,您可能希望看看它是如何做到的,并可能借用代码。
-
在某个时候,已经到达的总数据包数将超过环大小(16);确保您的代码可以处理这种情况。
您需要锁来应对 xv6 可能从多个进程使用 E1000 的可能性,或者在中断到达时在内核线程中使用 E1000。
二、结果
https://github.com/INnoVationv2/xv6-labs-2023/commit/4d5ca4d1114f4907d39a04fab838858fb4357eb8
![image-20240327233356259](http://pic-save-fury.oss-cn-shanghai.aliyuncs.com/uPic/image-20240327233356259.png)
Q&A
1. 初始化
vm.c中,以下代码将e1000的寄存器映射到页表中
// pci.c maps the e1000's registers here.
kvmmap(kpgtbl, 0x40000000L, 0x40000000L, 0x20000, PTE_R | PTE_W);
为什么e1000寄存器的地址是0x40000000?谁规定的?
发送
- 发送的初始化,数据接收到哪里?如果是直接写入内存,在哪里指定的内存地址?
- 接收到数据后,会发生什么中断?
- 接收到数据后,数据放在哪里?处理流程是什么?
- mbuf是干嘛的
接收
- 怎么初始化的