[更新] 如果你观看2020的课程,并且做了2023的实验,那么请先观看2020课程的lec21再开始本实验。我之前是在没有看这节课的基础上做的实验,所以感觉无比煎熬。以下是原文章:
本实验的最大难点在于看懂它让你干什么,为此需要阅读一堆手册文档。
除此之外意义不大,而且这个实验太打击人了。。。建议跳过
top 为上层提供接口,bottom 与下层交互
存在缓冲区、读写指针、锁等
通常几个几个字符写入,然后休眠,设备发送完成后引发中断唤醒进程继续写入
基本流程可参考 uart、pipe 实现:
一堆的条件检查别忘了
缓冲和中断 实现了解耦
写入内存映射地址即可与设备交互,注意相应寄存器的行为
中断处理程序通常执行相对较少的工作,因为它们没有特定的上下文
由QEMU模拟,IP:10.0.2.2
Documentation/Networking - QEMU
QEMU 模拟一个计算机和其网络设备,其 IP 为 10.0.2.15,网关 IP 为 10.0.2.2,DNS…,SMB…
SLIRP
通过 QEMU 内置的 TCP/IP 栈实现网络功能,并提供了一些基本的网络服务,例如网关和 DNS
kernel/e1000.c
包含 E1000 的初始化代码以及用于发送和接收数据包
的空函数(您将填写这些代码)
kernel/e1000_dev.h
包含由 E1000 定义并在 Intel E1000软件开发人员手册中描述的寄存器和标志位的定义
- kernel/net.c
和 kernel/net.h
包含一个实现IP 、 UDP和ARP协议的简单网络堆栈。
这些文件还包含用于保存数据包的灵活数据结构的代码,称为 mbuf
kernel/pci.c
包含当 xv6 引导时在 PCI 总线上搜索 E1000 卡的代码。
本节描述了用于 PCI/PCI-X 家族千兆以太网控制器的:数据包接收、数据包传输、传输描述符环结构、TCP 分段以及传输校验和卸载功能。
基本流程
:识别数据包存在–地址过滤–存储数据包到 接收数据FIFO中–发送它到RAM的接收缓冲区–更新接收描述符状态
缓冲区大小由 RCTL.BSIZE & RCTL.BSEX 控制,支持以下几种 buffer 大小:
详见第 13.4.22 节
硬件对缓冲区地址没有对齐限制,但强烈建议软件在可能的情况下在至少缓存线边界上分配接收缓冲区。
接收描述符是一种数据结构,包含接收数据缓冲区地址和用于硬件存储数据包信息的字段。在接收到数据包后,会将其写入RAM中指定的buffer内。
其格式如下:
其中阴影区域表示数据包接收时硬件字段修改的字段。
Checksum:似乎没用?见3.2.3
status:见3.2.3.1。我们会用到 DD
error:似乎没用?见3.2.3.2
special:似乎没用?见3.2.3.3
这是一个循环缓冲区,头指针 RDH
指向下一个将需要写回RAM的描述符,尾指针 RDT
标识硬件可以放数据包的最后一个的buffer 之后的位置,即第一个未被软件处理的数据包
头指针和尾指针引用 16 字节的内存块,即这个结构体:
软件可以读取RAM中的buffer,检测 status
是否非0,即可判定这个buffer是否可以使用。将 status 清0
表示这个 buffer 软件已经用完了,硬件可以继续往里头写收到的包
详细解释见3.2.7
以太网控制器可以生成四种与接收相关的中断:
接收中断延迟定时器 RDTR
:每次完成写入RAM一个数据包后都会重新启动,经过预设的时间后,生成中断。
接收中断绝对定时器: E1000_RADV
,写入RAM一个包后启动,但新的包并不会重置该定时器,经过预设时间生成中断
小型接收包检测:E1000_RSRPD
接收描述符最小阈值
接收FIFO溢出
地址过滤: 如果接收 FIFO 中没有足够的空间,硬件会丢弃数据包并在相应的统计寄存器中标示丢失的数据包。
描述符获取策略:3.2.4。描述符获取策略旨在支持跨 PCI 总线的大量突发传输。
接收描述符写回:3.2.5
不同版本(82544GC/EI)的接收中断:3.2.8
校验和 Offloading:3.2.9
常规(非 TCP 分段)数据包的传输过程包括:
输出数据包由指针-长度对组成,构成一个描述符链(即描述符基础传输)。
软件通过组装指针-长度对的列表来形成传输数据包,将这些信息存储在传输描述符中,然后将芯片上的传输尾指针更新到该描述符。
传输描述符和缓冲区存储在主机内存中。硬件通常在完全从主机内存中获取所有数据包数据并将其存入芯片上的传输 FIFO 之后才开始传输数据包。这允许计算 TCP 或 UDP 校验和,并避免 PCI 下溢的问题。
数据存储在由描述符指向的缓冲区中…一个数据包通常由两个(或更多)描述符组成,一个(或多个)用于头部,一个用于实际数据。
一些软件实现将头部和数据包数据复制到一个缓冲区中,并且每个传输的数据包只使用一个描述符。
以太网控制器提供三种类型的传输描述符格式:
后两个统称为扩展描述符。扩展描述符类型通过将 TDESC.DEXT 位设置为 1b 来访问。如果设置了此位,则检查 TDESC.DTYP
字段以控制对描述符其余位的解释。
3.3.3
需将 DEXT 即 bit 29 设为0
环形缓冲区。尾指针指向最后一个由硬件拥有的描述符之后的一个条目,即下一个软件写入的位置。头指针指向正在发送的描述符。当head=tail,数据全部发送完毕,缓冲区为空
rs
位,要求硬件在发送完成后,设置 status 中的 DD
位。之后硬件再递增头指针通常,硬件在传输之前预取数据包数据。硬件通常在将数据存储在发送 FIFO1 中后更新头指针的值。
检查已完成数据包的过程包括以下方法之一:
e1000_init 配置E1000直接从RAM读写数据包,这被称为 DMA
e1000_init() 使用 mbufalloc()
为 E1000 分配了用于 DMA 的 mbuf 数据包缓冲区。
你的发送代码必须将指向数据包数据的指针放入发送环(TX 环)中的描述符中。struct tx_desc
描述了描述符的格式
你需要确保每个 mbuf 最终都被释放,但只能在 E1000 完成发送数据包后释放(E1000_TXD_STAT_DD
)
查看E1000内存映射控制寄存器
的可用状态,以检测接收数据包的可用性,并告知 E1000 驱动程序已填充了一些带有要发送数据包的 TX 描述符
全局变量 regs
持有一个指向 E1000 第一个控制寄存器的指针;你的驱动程序可以通过将 regs
作为数组来访问其他寄存器。你特别需要使用 E1000_RDT
和 E1000_TDT
索引。
一些有用的数据结构:
循环缓冲区,大小 由 TDLEN 寄存器指定,该寄存器必须是128字节对齐的。
基地址:64位 Base = TDBAH + TDBAL = 32+32
头指针:E1000_TDH,正在处理的描述符
尾指针:E1000_TDT
,指向下一个可以添加数据包的地址。
队列状态:
队列为空:head == tail
阴影部分:硬件已发送,但软件未释放其缓冲区
一个描述符传输完毕后,会自动更新 E1000_TXD_STAT_DD
位为1:
当 E1000 从以太网上接收到每个数据包时,它会通过 DMA 将数据包写入到下一个接收环(RX 环)描述符的 addr
指向的内存中。
你的 e1000_recv()
代码必须扫描 RX 环,并通过调用 net_rx()
将每个新数据包的 mbuf 交给网络栈(在 net.c
中)
然后你需要分配一个新的 mbuf 并将其放入描述符中,以便当 E1000 再次到达 RX 环的这一点时,它能找到一个新的缓冲区来 DMA 新的数据包。
数据包的传输速率可能比处理速率快, e1000_init() 为 E1000 提供了多个缓冲区
。这些缓冲区用一个数组描述,这些数组是一个循环接收队列:
每个缓冲区存放在RAM中,用 rx_desc
描述它的信息:
如果尚未有 E1000 中断挂起,则 E1000 会请求 PLIC 在启用中断后立即发送一个中断
显示历史输入输出数据:
1 | tcpdump -XXnr packets.pcap |
要测试你的驱动程序,在一个窗口中运行 make server
,在另一个窗口中运行 make qemu
,然后在 xv6 中运行 nettests
。nettests
中的第一个测试尝试将一个 UDP 数据包发送到主机操作系统,该数据包的目标是由 make server
运行的程序。如果你尚未完成实验,E1000 驱动程序将不会实际发送数据包,基本上什么也不会发生。
完成实验后,E1000 驱动程序将发送数据包,qemu 将其传递给你的主机计算机,make server
会看到它,并发送一个响应数据包,E1000 驱动程序和 nettests
将看到响应数据包。然而,在主机发送回复之前,它会向 xv6 发送一个“ARP”请求数据包,以查找其 48 位以太网地址,并期望 xv6 响应一个 ARP 回复。一旦你完成了 E1000 驱动程序的工作,kernel/net.c
将处理这一点。如果一切顺利,nettests
将打印 testing ping: OK
,而 make server
将打印一条来自 xv6 的消息。