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_SIZE
和 TX_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_RDT
和E1000_TDT
。
为了测试实验的驱动,在一个窗口运行 make server
,在另一个窗口运行 make qemu
,然后在 xv6 中运行 nettests
。nettests
第一个测试用例尝试发送一个 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;
}
}