Loading

MIT6.S081 ---- Lab networking

Lab: networking

Background

使用 E1000 网络设备处理网络通信。对于 xv6 和 开发者所写驱动,E1000 像一个真正的硬件,连接到真正的以太网 LAN (local area network)。实际,和驱动通信的 E1000 是由 qemu 仿真的,E1000 连接的 LAN 也是由 qemu 仿真的。在仿真的 LAN 中,xv6 ("guest") 的 IP 地址是 10.0.2.15,qemu 安排运行 qemu 的计算机的 IP 地址是 10.0.2.2。当 xv6 使用 E1000 向 10.0.2.2 发一个 packet,qemu 把这个 packet 传给运行 qemu 的真实计算机("host")上的合适的应用。

使用 QEMU 的 "user-mode network stack" (文档)。

Makefile 配置 QEMU 记录所有的传入和传出 packet 到 lab 目录下的文件 packets.pcap。有助于检查 xv6 发送和接收的 packet 是否符合期待。显示记录的 packet:

tcpdump -XXnr packets.pcap

文件 kernel/e1000.c 有 E1000 的初始化代码和发送/接收 packet 的空的函数,空函数需要在本实验完成。
kernel/e1000_dev.h 有寄存器的定义, E1000 定义的和 Intel E1000 软件工程师手册描述的标志位。
kernel/net.c 和 kernel/net.h 有一个简单的网络栈,实现了 IP, UDP, ARP 协议。这些文件有灵活的数据结构代码用于保存 packet,称为 mbuf
kernel/pci.c 有代码用于在 xv6 启动时,在 PCI 总线上查找 E1000 卡。

Your Job

任务是完成 kernel/e1000.c 中的 e1000_transmit()e1000_recv(),所以驱动能发送和接收 packet。make grade 测试。

E1000 Software Developer's Manual 对于实验很有帮助。特别是以下章节:

  • Section 2 很重要,提供整个设备的概述。
  • Section 3.2 提供 packet 接收的概述。
  • Section 3.3 3.4 提供 packet 发送的概述。
  • Section 13 提供 E1000 使用的寄存器的概述。
  • Section 14 帮你理解提供的初始化代码。

该手册有与以太网相关的控制器。QEMU 模拟 82540EM。浏览 Ch2 了解设备。为写驱动,需要熟悉 Ch3 Ch14 Ch4.1。也需要参考 Ch13。其他章节都是与实验驱动无关的 E1000 的组件。
开始不用关心细节,只需了解手册如何组织,便于之后查阅。E1000 有许多高级特性,大多可以忽略,完成本实验只需小部分基本特性。

e1000.c 中的 e1000_init() 函数配置 E1000 从 RAM 中读取要发送的 packets,将要接收的 packets 写入 RAM。这个技术称为 DMA(direct memory access),表明了 E1000 硬件直接向/从 RAM 写/读 packets。

因为 packets 突发传输可能超过了驱动的处理速度,e1000_init() 为 E1000 提供了多个 buffers 以便 E1000 能写 packets。E1000 需要的 buffers 被 RAM 中的一组“描述符”定义;每个描述符在 RAM 中有一个地址,E1000 在该地址能写一个接收的 packet。struct rx_desc 定义了描述符的格式。描述符数组被称为 receive ring 或者 receive queue,它是环形,当网卡或驱动到达数组的末尾时,又返回了开始。e1000_init() 为 E1000 分配 mbuf packet buffers,用于 DMA。也有一个 transmit ring,驱动把想要 E1000 发送的 packet 放在这里。e1000_init() 配置两个 rings,大小分别为 RX_RING_SIZETX_RING_SIZE

当 net.c 中的网络栈需要发送一个 packet,它调用 e1000_transmit(),参数为 mbuf,含有将要发送的 packet。发送代码必须将一个指向 packet 数据的指针放在 TX(transmit) ring 中的描述符中。struct tx_desc 定义了描述符的格式。只有在 E1000 完成发送 packet 之后,确保每个 mbuf 最终都被释放。(E1000 在描述符中设置 E1000_TXD_STAT_DD 位表示)

当 E1000 从以太网中接收到一个 packet,首先通过 DMA 将 packet 传输到下一个 RX(receive) ring 描述符指向的 mbuf,然后生成一个中断。e1000_recv() 代码必须扫描 RX ring,调用 net_rx() 逐个发送新的 packet 的 mbuf 给网络栈(在 net.c)。然后需要分配一个新的 mbuf,将 mbuf 放在描述符中,因此,当 E1000 再次到达 RX ring 中的那个位置,将看到一个新的 buffer,可以通过 DMA 传输一个新的 packet。

除了读写 RAM 中的描述符 rings 外,实验的驱动需要通过内存映射控制寄存器 和 E1000 交互,检测什么时候接收的 packets 可用以及通知 E1000 驱动已经将要发送的 pakcets 填充到一些 TX 描述符。全局变量 regs 保存指向 E1000 的第一个寄存器的指针;实验的驱动通过检索 regs 数组获取其他寄存器。实验特别需要使用索引 E1000_RDTE1000_TDT

为了测试实验的驱动,在一个窗口运行 make server ,在另一个窗口运行 make qemu,然后在 xv6 中运行 nettestsnettests 第一个测试用例尝试发送一个 UDP packet 给 host OS,发送给 host OS 上 make server 运行的程序。如果没有完成 lab,E1000 驱动不会真正的发送 packet,并且什么都不会发生。

完成 lab 之后,E1000 驱动将发送 packet,qemu 将 packet 传给 host 计算机,make server 能收到这个 packet,且会发送一个响应 packet,然后 E1000 驱动和 nettests 将收到响应 packet。然而,在 host 发送响应之前,它先发送一个 “ARP” 请求 packet 给 xv6,查找 \(48\) 位的以太网地址,期待 xv6 发回一个 ARP 响应。实验不需要考虑这个问题,kernel/net.c 负责处理。如果一切 OK,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 的一个 name server。确保代码通过所有测试用例,之后应该看到如下输出:

$ 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 通过实验代码。

Hints

通过向 e1000_transmit()e1000_recv() 添加 print 语句,并且运行 make server 和 (xv6 中)nettests。应该从 print 语句看到 nettests 产生一个 e1000_transmit 调用。

实现 e1000_transmit 的一些提示:

  • 首先请求 E1000 的 TX ring 的索引,在该索引等待下一个 packet,通过读取 E1000_TDT
  • 然后检查 ring 是否溢出了。如果 E1000_TXD_STAT_DD 没有在 E1000_TDT 索引的描述符中被设置,则 E1000 还没有结束之前的传输请求,所以返回一个 error。
  • 否则,使用 mbuffree() 释放最后的(索引到的描述符指向的)已经被发送(但还没有释放)的 mbuf。(如果有一个)
  • 设置描述符。m->head 指向内存中 packet 的内容,m->len 的长度。设置必要的 cmd 标志(参考 E1000 手册的 Section 3.3),保存指向 mbuf 的指针,方便之后释放。
  • 最后,更新 ring 位置:E1000_TDT\(1\)TX_RING_SIZE
  • 如果 e1000_transmit() 成功添加 mbuf 到 ring 中,则返回 \(0\)。失败的话(如:没有描述符可用于发送 mbuf)返回 \(-1\),便于调用者释放 mbuf。

实现 e1000_recv 的一些提示:

  • 首先,向 E1000 请求 ring 索引,索引指向了下个等待接收的 packet。获取索引的方法:读取 E1000_RDT 控制寄存器,然后加 \(1\),然后模 RX_RING_SIZE
  • 通过检查描述符中 status 域的 E1000_RXD_STAT_DD 位判断新的 packet 是否可用。如果不可用,停止。
  • 否则,更新 mbuf 的 m->len 为描述符的 length 域。使用 net_rx() 将 mbuf 发给网络栈(上层释放 mbuf)。
  • 然后,使用 mbufalloc() 分配一个新的 mbuf 替换刚刚 net_rx() 发送的 mbuf。新的 mbuf 的数据指针(m->head)放在描述符中,将描述符的 status 置为 \(0\)
  • 最后,更新 E1000_RDT 寄存器指向被处理的最后一个 ring 描述符的索引。
  • e1000_init() 用 mbuf 初始化 RX ring,你将看到如何实现,或许需要参考代码。
  • 有时,到达的 packets 的数量超过了 ring 大小(\(16\));确保实验的代码可以处理。

需要用锁处理一些情况:xv6 可能有多个进程使用 E1000 ;kernel thread 正在使用 E1000 时,发生中断。

Implement

结合 DMA 机制,理解 ring 和 mbuf 等结构体,熟悉实验相关寄存器,按照提示步骤完成。

int
e1000_transmit(struct mbuf *m)
{
  acquire(&e1000_lock);
  uint32 tx_index = regs[E1000_TDT];
  struct tx_desc *desc = &tx_ring[tx_index];

  // check if the the ring is overflowing.
  if (!(desc->status && E1000_TXD_STAT_DD)) {
    release(&e1000_lock);
    return -1;
  }

  // free the last mbuf that was transmitted from desc
  if (tx_mbufs[tx_index]) {
    mbuffree(tx_mbufs[tx_index]);
  }

  // fill in the descriptor
  desc->addr = (uint64) m->head;
  desc->length = m->len;
  // Report Status: When set, the Ethernet controller needs to report the status information.
  // End Of Packet: When set, indicates the last descriptor making up the packet. One or many descriptors can be used to form a packet.
  desc->cmd = E1000_TXD_CMD_RS | E1000_TXD_CMD_EOP;
  // stash away a pointer to the mbuf of freeing.
  tx_mbufs[tx_index] = 0;

  // update the ring position
  regs[E1000_TDT] = (tx_index + 1) % TX_RING_SIZE;

  release(&e1000_lock);
  return 0;
}
static void
e1000_recv(void)
{
  while (1)
  {
    uint32 rx_index = (regs[E1000_RDT] + 1) % RX_RING_SIZE;
    struct rx_desc *desc = &rx_ring[rx_index];

    // check if a new packet is available
    if (!(desc->status & E1000_RXD_STAT_DD)) {
      break;
    }

    // update the mbuf
    rx_mbufs[rx_index]->len = desc->length;
    // deliver the mbuf to the network stack
    net_rx(rx_mbufs[rx_index]);

    // allocate a new mbuf
    rx_mbufs[rx_index] = mbufalloc(0);
    if (!rx_mbufs[rx_index])
      panic("e1000");
    // program its data pointer (m->head) into the descriptor
    desc->addr = (uint64) rx_mbufs[rx_index]->head;
    // clear the descriptor's status bits to zero.
    desc->status = 0;

    // update the E1000_RDT register to be the index of
    // the last ring descriptor processed.
    regs[E1000_RDT] = rx_index;
  }
}

Code

Code: Lab cow

posted @ 2022-02-24 22:33  seaupnice  阅读(436)  评论(0编辑  收藏  举报