MIT_6.s081_Lab7:Xv6 and Networking

2022-12-08 14:50:49 浏览数 (2)

MIT_6.s081_Lab7:Xv6 and Networking

于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。这可能对于检查接收发送的数据包是有用的。展现记录的数据包:

代码语言:javascript复制
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。

代码语言:javascript复制
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];
代码语言:javascript复制
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。

代码语言:javascript复制
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;
}

0 人点赞