networking

在本实验中,你将为网络接口卡 (NIC) 编写 xv6 设备驱动程序。

要求

前言

您将使用名为 E1000 的网络设备来处理网络通信。对于xv6(以及您编写的驱动程序),E1000看起来像连接到真实以太网局域网(LAN)的真实硬件。实际上,您的驱动程序将与之通信的 E1000 是 qemu 提供的模拟,连接到 LAN 也由 qemu 模拟。在此模拟 LAN 上,xv6(“客机”) 的 IP 地址为 10.0.2.15。Qemu 还安排运行 qemu 的计算机出现在 IP 地址为 10.0.2.2 的局域网上。当 xv6 使用 E1000 将数据包发送到 10.0.2.2 时,qemu 会将数据包传送到运行 qemu 的(真实)计算机上的相应应用程序(“主机”)。

您将使用 QEMU 的“用户模式网络堆栈”。QEMU 的文档在此处提供了有关用户模式堆栈的更多信息。我们更新了 Makefile 以启用 QEMU 的用户模式网络堆栈和 E1000 网卡。

Makefile 将 QEMU 配置为将所有传入和传出数据包记录到实验目录中的 packets.pcap 文件。查看这些记录以确认 xv6 正在传输和接收您期望的数据包可能会有所帮助。显示记录的数据包的命令:`tcpdump -XXnr packets.pcap
`

我们已将一些文件添加到此实验的 xv6 存储库中。文件 kernel/e1000.c 包含 E1000 的初始化代码以及用于发送和接收数据包的空函数,您将补充这些函数。kernel/e1000_dev.h 包含由 E1000 定义并在英特尔 E1000 软件开发人员手册中描述的寄存器和标志位的定义。kernel/net.ckernel/net.h 包含一个简单的网络堆栈,用于实现 IP、UDP 和 ARP 协议。这些文件还包含用于保存数据包的灵活数据结构的代码,称为 mbuf。最后,kernel/pci.c 包含当 xv6 启动时在 PCI 总线上搜索 E1000 卡的代码。

需求

你的工作是在 kernel/e1000.c 中完成 e1000_transmit()e1000_recv() ,以便驱动程序可以传输和接收数据包。

在编写代码时,您会发现自己参考了 E1000 软件开发人员手册。特别有帮助的可能是以下部分:

  • 第 2 部分是必不可少的,它概述了整个设备。
  • 第 3.2 节概述了数据包接收。
  • 第 3.3 节概述了数据包传输,以及第 3.4 节。
  • 第 13 节概述了 E1000 使用的寄存器。
  • 第 14 节可以帮助您了解我们提供的 init 代码。

浏览 E1000 软件开发人员手册。本手册介绍了几个密切相关的以太网控制器。QEMU 模拟 82540EM。现在浏览第 2 章以感受该设备。要编写驱动程序,您需要熟悉第 3 章和第 14 章以及 4.1(尽管不是 4.1 的小节)。您还需要使用第 13 章作为参考。其他章节主要介绍 E1000 的组件,您的驱动程序不必与之交互。一开始不要担心细节;只需了解文档的结构,以便后续查找。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_SIZETX_RING_SIZE

net.c 中的网络堆栈需要发送数据包时,它会用一个 mbuf 调用 e1000_transmit(),该 mbuf 保存要发送的数据包。你的传输代码必须在 TX(传输)环的描述符中放置指向数据包数据的指针。struct tx_desc描述描述符格式。您需要确保最终释放每个 mbuf,但仅在 E1000 完成数据包传输后(E1000 在描述符中设置E1000_TXD_STAT_DD位以指示这一点)。每当 E1000 从以太网接收到数据包时,它会使用DMA技术将数据包直接写入到下一个 RX(接收)环描述符中的 addr 指向的内存。如果 E1000 中断尚未挂起,则 E1000 会要求 PLIC 在启用中断后立即传递一个。您的 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 中运行 nettestsnettests 中的第一个测试尝试将 UDP 数据包发送到主机操作系统,该数据包发送到make server运行的程序。如果尚未完成实验,E1000 驱动程序实际上不会发送数据包,并且不会发生太多操作。

完成实验后,E1000 驱动程序将发送数据包,qemu 将其传送到您的主机,make server 将看到它,它将发送响应数据包,E1000 驱动程序和 nettests 将看到响应数据包。但是,在主机发送回复之前,它会向 xv6 发送“ARP”请求数据包以找出其 48 位以太网地址,并期望 xv6 使用 ARP 回复进行响应。kernel/net.c 将在您完成 E1000 驱动程序的工作后处理此问题。如果一切顺利,nettests 将打印 testing ping:okmake 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执行其他一些测试,最终通过(真实)互联网向Google的名称服务器之一发送DNS请求。应确保代码通过所有这些测试,之后应看到以下输出:

$ 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.

提示

首先将 print 语句添加到 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 的指针以供以后释放。
  • 最后,通过E100_TDT + 1再对TX_RING_SIZE取余来更新环位置
  • 如果 e1000_transmit() 成功地将 mbuf 添加到环中,则返回 0。失败时(例如,没有可用于传输 mbof 的描述符),返回 -1,以便调用方知道释放 mbuf。

对实现e1000_recv的提示:

  • 首先向 E1000 询问下一个等待接收的数据包(如果有)所在的环索引,方法是获取E1000_RDT控制寄存器, +1后对RX_RING_SIZE取余。
  • 然后,通过检查描述符状态部分中的E1000_RXD_STAT_DD位来检查新数据包是否可用。如果没有,请停止。
  • 否则,请将 mbuf 的 m->len 更新为描述符中报告的长度。使用 net_rx() 将 mbuf 传送到网络堆栈。
  • 然后使用 mbufalloc() 分配一个新的 mbuf 来替换刚刚给 net_rx() 的mbuf。将其数据指针(m->head)放到描述符中。将描述符的状态位清零。
  • 最后,将E1000_RDT寄存器更新为最后一个处理的描述符环的索引。
  • e1000_init() 使用 mbufs 初始化 RX 环,您需要查看它是如何做到这一点的,也许还可以借用代码。
  • 在某些时候,到达的数据包总数将超过环大小(16); 确保你的代码可以处理这个问题。

您需要锁来应对 xv6 可能从多个进程使用 E1000 的可能性,或者在中断到达时可能在内核线程中使用 E1000。

实现

e1000_transmit()的实现

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.
    //
    uint32 idx;
    struct tx_desc* desc;

    acquire(&e1000_lock);
    idx = regs[E1000_TDT];  // 获取寄存器中保存的TX环索引
    desc = tx_ring + idx;   // 得到当前要发送的描述符
    if (idx >= TX_RING_SIZE) {  // 索引溢出处理
        panic("transmit: 环溢出");
    }
    if (!(desc->status & E1000_TXD_STAT_DD)) { // 之前的传输请求未完成
        release(&e1000_lock);
        return -1;
    }
    // 将传入的mbuf信息填写到描述符
    desc->addr = (uint64)m->head;
    desc->length = (uint16)m->len;
    desc->cmd = E1000_TXD_CMD_EOP | E1000_TXD_CMD_RS;
    // 储存传入的mbuf指针以供之后释放
    if (tx_mbufs[idx]) { // 若有mbuf未释放
        mbuffree(tx_mbufs[idx]);
    }
    tx_mbufs[idx] = m;
    // 更新环指针
    regs[E1000_TDT] = (regs[E1000_TDT] + 1) % TX_RING_SIZE;
    release(&e1000_lock);
    return 0;
}

e1000_recv()的实现

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()).
    //
    uint32 idx;
    struct rx_desc* desc;
    
    while (1) {
        idx = (regs[E1000_RDT] + 1) % RX_RING_SIZE; // 获取RX环索引
        desc = rx_ring + idx;  // 获取描述符
        if (!(desc->status & E1000_RXD_STAT_DD)) {
            // 接收未完成
            break;
        }
        rx_mbufs[idx]->len = desc->length;  // 将对应mbuf更新为接收后的长度
        net_rx(rx_mbufs[idx]);  // 提交到网络协议栈
        // 将该位置的mbuf指针换成一个新的
        // 但是因为网络协议栈要用所以不能释放内存
        rx_mbufs[idx] = mbufalloc(0);  
        // 更新文件描述符,以对应新的空白mbuf           
        desc->addr = (uint64)rx_mbufs[idx]->head;
        desc->status = 0;
        // 更新RX环索引
        regs[E1000_RDT] = idx;
    }
}

遇到的问题

  1. 关于是否用锁的问题
    最开始我在e1000_recv()中也习惯性地使用了锁。然后就出现了如下错误:
    image.png
    但实际上只在中断处理时接收(只在e1000_intr()中被调用),并不是并发的,所以不需要锁。(而且就算是并发的,因为收和发的环不一样,应当是两把锁才对)

结果

  • 运行结果
    image.png
  • 测试结果
    image.png

个人收获

  1. 了解了网卡驱动中收发数据的基本工作原理。
  2. 通过两组(收、发)描述符和数据缓存环(本质上是数组)来记录接收和发送的数据包,然后通过寄存器来获取当前需要处理的数据在环中的位置。

Longfar
1 声望3 粉丝