于2022年3月7日2022年3月7日由Sukuna发布
您将使用称为 E1000 的网络设备来处理网络通信。 对于 xv6(以及您编写的驱动程序),E1000 看起来像是连接到真实以太网局域网 (LAN) 的真实硬件。 实际上,您的驱动程序将与之通信的 E1000 是由 qemu 提供的仿真,连接到同样由 qemu 仿真的 LAN。 在这个模拟 LAN 上,xv6(“guest”)的 IP 地址为 10.0.2.15。 Qemu 还安排运行 qemu 的计算机出现在 IP 地址为 10.0.2.2 的 LAN 上。 当 xv6 使用 E1000 向 10.0.2.2 发送数据包时,qemu 会将数据包传送到您正在运行 qemu(“主机”)的(真实)计算机上的适当应用程序。(就是qemu模拟器传递数据到真实的计算机中)
你将会用到QEMU的 “用户态网络栈”。QEMU的文档中由很多关于用户态栈的描述。我们已经更新了 Makefile,打开了QEMU的用户态网络栈以及E1000网卡。
Makefile 设置了 QEMU记录所有的进出数据包到文件 packets.pcap。这可能对于检查接收发送的数据包是有用的。展现记录的数据包:
tcpdump -XXnr packets.pcap
我们已经添加了一些文件到xv6上了。文件kernel/e1000.c包含了E1000的初始化代码以及空的传输和接收数据包的函数,这些是需要你去完成的。kernel/e1000_dev.h包含了寄存器和标志位的定义,这些在Intel E1000的文档有描述。kernel/net.c和kernel/net.h包含了一个简单的包含IP、UDP、ARP协议的网络栈。这些文件页包含了一个灵活的数据结构来持有数据包,叫做mbuf。最后,kernel/pci.c包含了在xv6启动时,在PCI总线上查找一个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() 将 E1000 的 mbuf 数据包缓冲区分配给 DMA 。还有一个传输环,驱动程序将它希望 E1000 发送的数据包放入其中。 e1000_init() 将两个环配置为具有大小 RX_RING_SIZE 和 TX_RING_SIZE。
struct tx_desc
{
uint64 addr;
uint16 length;
uint8 cso;
uint8 cmd;
uint8 status;
uint8 css;
uint16 special;
};
#define TX_RING_SIZE 16
static struct tx_desc tx_ring[TX_RING_SIZE] __attribute__((aligned(16)));
static struct mbuf *tx_mbufs[TX_RING_SIZE];
// [E1000 3.2.3]
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;
};
#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 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
};
其中,tx_ring和tx_mbufs是一一对应的.
当 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 数据包,地址是使服务器运行的程序。如果您还没有完成实验,E1000 驱动程序实际上不会发送数据包,也不会发生任何事情。
完成实验后,E1000 驱动程序会发送数据包,qemu 会将数据包传送到您的主机,make server 会看到它,它会发送响应数据包,然后 E1000 驱动程序和 nettests 会看到响应数据包.然而,在主机发送回复之前,它会向 xv6 发送一个“ARP”请求包以查找其 48 位以太网地址,并期望 xv6 以 ARP 回复进行响应。一旦你完成了 E1000 驱动程序的工作,kernel/net.c 就会处理这个问题。如果一切顺利,nettests 将打印 testing ping: OK,并且 make server 将打印一条来自 xv6! 的消息。
首先将打印语句添加到 e1000_transmit() 和 e1000_recv(),然后运行 make server 和(在 xv6 中)nettests。您应该从您的打印语句中看到 nettests 生成了对 e1000_transmit 的调用。
实现 e1000_transmit 的一些提示:
首先通过读取 E1000_TDT 控制寄存器向 E1000 询问它期待下一个数据包的 TX 环索引。 然后检查环是否溢出。如果 E1000_TDT 索引的描述符中没有设置 E1000_TXD_STAT_DD,则说明 E1000 还没有完成对应的上一个传输请求,因此返回错误。 否则,使用 mbuffree() 释放从该描述符传输的最后一个 mbuf(如果有的话)。 然后填写描述符。 m->head 指向包在内存中的内容,m->len 是包的长度。设置必要的 cmd 标志(查看 E1000 手册中的第 3.3 节)并隐藏指向 mbuf 的指针以供以后释放。 最后,通过将 E1000_TDT 模 TX_RING_SIZE 加一来更新环位置。 如果 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() 用 mbufs 初始化 RX 环,你会想看看它是如何做到的,也许还需要借用代码。 在某些时候,已经到达的数据包总数将超过环大小(16);确保您的代码可以处理。 您将需要锁来应对 xv6 可能从多个进程使用 E1000 的可能性,或者当中断到达时可能在内核线程中使用 E1000。
int
e1000_transmit(struct mbuf *m)
{
//
// Your code here.
//
// the mbuf contains an ethernet frame; program it into
// the TX descriptor ring so that the e1000 sends it. Stash
// a pointer so that it can be freed after sending.
//
acquire(&e1000_lock);
//首先通过读取 E1000_TDT 控制寄存器向 E1000 询问它期待下一个数据包的 TX 环索引。
uint reg_tdt = regs[E1000_TDT];
//然后检查环是否溢出。如果 E1000_TDT 索引的描述符中没有设置 E1000_TXD_STAT_DD,则说明 E1000 还没有完成对应的上一个传输请求,因此返回错误。
if((tx_ring[reg_tdt].status & E1000_TXD_STAT_DD) == 0){
return -1;
}
//否则,使用 mbuffree() 释放从该描述符传输的最后一个 mbuf(如果有的话)。
if(tx_mbufs[reg_tdt] != 0)
mbuffree(tx_mbufs[reg_tdt]);
//然后填写描述符。 m->head 指向包在内存中的内容,m->len 是包的长度。设置必要的 cmd 标志
tx_mbufs[reg_tdt] = m;
tx_ring[reg_tdt].length = m->len;
tx_ring[reg_tdt].addr = (uint64)(m->head);
tx_ring[reg_tdt].cmd = 9;
//最后,通过将 E1000_TDT 模 TX_RING_SIZE 加一来更新环位置。
regs[E1000_TDT] = (regs[E1000_TDT] + 1) % TX_RING_SIZE;
release(&e1000_lock);
return 0;
}
static void
e1000_recv(void)
{
//
// Your code here.
//
// Check for packets that have arrived from the e1000
// Create and deliver an mbuf for each packet (using net_rx()).
//首先通过获取 E1000_RDT 控制寄存器并加一个模 RX_RING_SIZE,向 E1000 询问下一个等待接收的数据包(如果有)所在的环索引。
uint reg_rdt = regs[E1000_RDT];
int i = (reg_rdt + 1)%RX_RING_SIZE;
//然后通过检查描述符状态部分中的 E1000_RXD_STAT_DD 位来检查新数据包是否可用。如果没有,请停止。
//否则,将 mbuf 的 m->len 更新为描述符中报告的长度。使用 net_rx() 将 mbuf 传送到网络堆栈。
while(rx_ring[i].status & E1000_RXD_STAT_DD){
rx_mbufs[i]->len = rx_ring[i].length;
net_rx(rx_mbufs[i]);
//然后使用 mbufalloc() 分配一个新的 mbuf 来替换刚刚给 net_rx() 的那个。将其数据指针(m->head)编程到描述符中。将描述符的状态位清零。
if((rx_mbufs[i] = mbufalloc(0)) == 0)
panic("e1000");
rx_ring[i].addr = (uint64)rx_mbufs[i]->head;
rx_ring[i].status = 0;
i = (i + 1) % RX_RING_SIZE;
}
//最后,将 E1000_RDT 寄存器更新为最后处理的环描述符的索引。
regs[E1000_RDT] = (i - 1) % RX_RING_SIZE;
}