MIT6.S081 Lab 7 Network driver
Lab 7 network driver
这个实验将使用一个名为 E1000 的网络设备来处理网络通信。 对于 xv6来说,E1000 看起来就像一个连接到真实以太网局域网的真实硬件。 事实上,E1000 是由 qemu 模拟的,它连接的局域网也是由 qemu 模拟的。 在这个模拟局域网中,xv6的 IP 地址为 10.0.2.15, 运行 qemu 的计算机的 IP 地址为10.0.2.2。当 xv6 使用 E1000 向 10.0.2.2 发送数据包时,qemu 会将数据包发送到运行 qemu 的主机上的相应应用程序。
Makefile 会配置 QEMU 将所有进出数据包记录到实验室目录下的 packets.pcap 文件中。 查看这些记录可能有助于确认 xv6 发送和接收的数据包是否符合预期。 要显示记录的数据包,请执行:tcpdump -XXnr packets.pcap
我们为本实验在 xv6 资源库中添加了一些文件。
-
kernel/e1000.c 文件包含 E1000 的初始化代码,以及用于发送和接收数据包的空函数,您将填写这些代码。
-
kernel/e1000_dev.h 文件包含 E1000 定义的寄存器和标志位的定义,这些定义在《英特尔 E1000 软件开发人员手册》中有所描述。 这些文件还包含用于保存数据包的灵活数据结构(称为 mbuf)的代码。
-
kernel/pci.c 包含在 xv6 启动时在 PCI 总线上搜索 E1000 卡的代码。
Your Job (hard)
您的任务是完成 kernel/e1000.c 中的 e1000_transmit() 和 e1000_recv(),以便驱动程序能够发送和接收数据包。
在编写代码时,您会发现自己经常参考 E1000 软件开发人员手册。 以下章节可能对您有特别帮助:
- 第 2 节非常重要,概述了整个设备。
- 第 3.2 节概述数据包接收。
- 第 3.3 节和第 3.4 节概述了数据包传输。
- 第 13 节概述 E1000 使用的寄存器。
- 第 14 节可以帮助您理解我们提供的启动代码。
浏览 E1000 软件开发人员手册。 本手册涵盖多个密切相关的以太网控制器。现在请浏览第 2 章,以便对该设备有所了解。 要编写驱动程序,您需要熟悉第 3 章、第 14 章和第 4.1 章(但不包括第 4.1 章的小节)。 您还需要使用第 13 章作为参考。 其他章节主要涉及 E1000 的组件,您的驱动程序无需与之交互。 一开始不必担心细节,只需了解文件的结构,以便日后查找。 E1000 有许多高级功能,其中大部分可以忽略。 完成本实验只需要一小部分基本功能。
我们在 e1000.c 中提供的 e1000_init() 函数可配置 E1000 使用DMA直接将数据包写入/读出 RAM。
由于突发数据包的到达速度可能快于驱动程序的处理速度,e1000_init() 为 E1000 提供了多个缓冲区,E1000 可以将数据包写入这些缓冲区。 E1000 要求这些缓冲区由 RAM 中的 "descriptors "数组来描述;每个descriptor包含 RAM 中的一个地址,E1000 可将接收到的数据包写入其中。 struct rx_desc 描述了descriptor格式。 descriptors数组称为“接收环”或“接收队列”。
e1000_init() 使用 mbufalloc() 为 E1000 分配 mbuf 数据包缓冲区,以便 DMA 进入。 e1000_init() 将这两个环配置为 RX_RING_SIZE 和 TX_RING_SIZE 大小。
#define RX_RING_SIZE 16
static struct rx_desc rx_ring[RX_RING_SIZE] __attribute__((aligned(16)));
static struct mbuf *rx_mbufs[RX_RING_SIZE];
struct rx_desc
{
uint64 addr; /* Address of the descriptor's data buffer */
uint16 length; /* Length of data DMAed into data buffer */
uint16 csum; /* Packet checksum */
uint8 status; /* Descriptor status */
uint8 errors; /* Descriptor Errors */
uint16 special;
};
struct mbuf {
struct mbuf *next; // the next mbuf in the chain
char *head; // the current start position of the buffer
unsigned int len; // the length of the buffer
char buf[MBUF_SIZE]; // the backing store
};
全局变量 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 回应。 如果一切顺利,nettests 将打印 ping 测试结果: OK,make server 将打印来自 xv6 的信息
tcpdump -XXnr packets.pcap 的输出开头应该是这样的:

您的输出结果看起来会有些不同,但应该包含字符串 "ARP、请求"、"ARP、回复"、"UDP"、"a.message.from.xv6 "和 "this.is.the.host"。
nettests 还进行了其他一些测试,最后通过(真实)互联网向 Google 的一个域名服务器发送了 DNS 请求。 您应确保您的代码通过了所有这些测试,然后您将看到以下输出:

首先在 e1000_transmit() 和 e1000_recv() 中添加打印语句,并运行 make server 和(在 xv6 中)nettests。 您应该能从打印语句中看到 nettests 会生成对 e1000_transmit 的调用。
e1000_transmit
当 net.c 中的网络堆需要发送数据包时,它会调用 e1000_transmit(),参数为 mbuf ,其保存着要发送的数据包。 你的transmit代码必须在 TX ring 的描述符中放置 mbuf 指针。 您需要确保每个 mbuf 最终都被释放,但只有在 E1000 完成数据包发送后才会释放(E1000 会设置描述符中的 E1000_TXD_STAT_DD 位来表明这一点)。

e1000_transmit(struct mbuf m)的作用为将mbuf m添加到发送环中,*rx_mbufs[]数组保存指向mbuf的指针,如果之前index对应的mbuf还未释放则对其进行释放。随后将m添加到rx_ring中,在rx_mbufs[]中记录m的指针
提示
- 获取TX ring索引:通过读取 E1000_TDT 控制寄存器,询问 E1000 下一个期望传输的数据包的 TX ring 索引。
- 检查 ring 是否溢出: 如果 E1000_TDT 索引的 descriptor 中未设置 E1000_TXD_STAT_DD,则表明 E1000 尚未完成相应的前一个传输请求,因此返回错误。
- 释放已传输的mbuf:若未溢出,使用 mbuffree() 释放上一个已传输 mbuf(如果有的话)。
- 填充描述符:然后将 m->head 和 m->len 填充进 descriptor中。m->head 指向内存中的数据包内容,m->len 是数据包长度。 设置必要的 cmd 标志(请参阅 E1000 手册第 3.3 节),并保存一个指向 mbuf 的指针,以便日后释放。
- 更新TX ring索引:( E1000_TDT + 1 ) mod TX_RING_SIZE
- 返回:如果 e1000_transmit() 成功地将 mbuf 添加到 ring 上,则返回 0。 如果失败(例如,没有可用的描述符来传输 mbuf),则返回 -1 以便调用者知道如何释放 mbuf。
完成
int
e1000_transmit(struct mbuf *m)
{
acquire(&e1000_lock);
uint64 index = regs[E1000_TDT]; // get index
if(!(tx_ring[index].status & E1000_TXD_STAT_DD)){ // check overflow
release(&e1000_lock);
return -1;
}
if(tx_mbufs[index]) // free the mbuf which has transmitted (if there was one)
mbuffree(tx_mbufs[index]);
tx_ring[index].addr = (uint64)m->head; // fill in the descriptor
tx_ring[index].length = m->len;
tx_ring[index].cmd = 0;
tx_ring[index].cmd |= E1000_TXD_CMD_EOP | E1000_TXD_CMD_RS;
tx_mbufs[index] = m; // save a pointer to m
regs[E1000_TDT] = (regs[E1000_TDT] + 1) % TX_RING_SIZE; // update index
release(&e1000_lock);
return 0;
}
结果


e1000_recv
E1000 从以太网接收每个数据包时,首先将数据包 DMA 到下一个 RX ring descriptor指向的 mbuf,然后产生中断。 您的 e1000_recv() 代码必须扫描 RX ring,并通过调用 net_rx()将每个新数据包的 mbuf 传递到网络堆。 然后,您需要分配一个新的 mbuf 并将其放入descriptor中,这样当 E1000 再次到达 RX ring 中的该点时,就可以找到一个新的缓冲区,将新数据包 DMA 到其中。

将旧mbuf发送,分配新的mbuf顶替,更新rx_ring[index]内容和rx_mbufs[index]记录的指针
提示
- 获取RX ring的索引:E1000_RDT +1 mod RX_RING_SIZE ,得到下一个等待接收的数据包(如果有)所在的 ring 索引。
- 检查新的数据包:通过检查描述符 status 部分的 E1000_RXD_STAT_DD 位来查看是否有新数据包。 如果没有,则停止。
- 更新mbuf:将 mbuf 的 m->len 更新为descriptor中描述的长度。 使用 net_rx() 将 mbuf 传输到网络栈。
- 分配新的mbuf:使用 mbufalloc() 分配一个新的 mbuf,以替换刚刚给 net_rx() 分配的 mbuf。 将其数据指针 (m->head) 编入descriptor。 将descriptor的状态位清零。
- 更新RX ring索引:更新 E1000_RDT 寄存器,使其成为最后处理的 ring descriptor的索引。
- e1000_init() 使用 mbufs 初始化 RX ring,你需要了解它是如何做到这一点的,或许还可以借用代码。
- 在某些时候,到达的数据包总数会超过 ring 的大小(16);请确保您的代码能够处理这些数据包。
如果 xv6 可能在多个进程中使用 E1000,或者在中断发生时在内核线程中使用 E1000,那么就需要加锁。
完成
static void
e1000_recv(void)
{
struct mbuf *m;
//acquire(&e1000_lock);
uint64 index = (regs[E1000_RDT] + 1) % RX_RING_SIZE; // get index
while(rx_ring[index].status & E1000_RXD_STAT_DD){ // check if a new packect is available
rx_mbufs[index]->len = rx_ring[index].length; // update mbuf->len
net_rx(rx_mbufs[index]); // deliver the mbuf to the network stack
m = mbufalloc(0); // allocate a new mbuf
if(!m){
printf("allocate new mbuf fail\n");
//release(&e1000_lock);
return;
}
rx_mbufs[index] = m; // replace the one just given to net_rx()
rx_ring[index].addr = (uint64)m->head; // program its data pointer into the descriptor
rx_ring[index].status = 0; // clear the descriptor's status bits to zero
index = (index + 1) % RX_RING_SIZE; // next index
}
regs[E1000_RDT] = (index + RX_RING_SIZE - 1) % RX_RING_SIZE; // update the register to be the index of the last ring descriptor processed
//release(&e1000_lock);
return;
}
因为每次循环结束后都会更新下一个index:index = (index + 1) % RX_RING_SIZE
,因此如果之后没有需要接收的数据,需要将index回退一格:regs[E1000_RDT] = (index + RX_RING_SIZE - 1) % RX_RING_SIZE;
recv不知道为什么加锁反而有问题,不加锁却能通过测试,暂时还没想清楚
结果


整个实验其实并不算难,按照着提示一步一步来甚至十分简单,但大篇的文档介绍很容易吓退一些人,慢慢读一点一点推进就行