networking
编写一个xv6设备驱动程序,驱动网卡
Background
参考书Chapter5 中断和设备驱动
你会使用一个叫作e1000的网络设备来处理网络通信。qemu虚拟化e1000,e1000就相当于现实中真正链接网络的网卡设备。
xv6的ip地址为10.0.2.15。qemu会让宿主机(the computer running qemu)的ip地址为10.0.2.2。qemu会将网络数据包传递给宿主机。
你将使用qemu的”user-mode network stack”。具体可查看qemu的文档。makefile已经起用了e1000网卡和”user-mode network stack”。
makefile配置qemu,让他记录所有网络包输入输出到packets.pcap文件中。可以通过查看此文件来调试。
1 | tcpdump -XXnr packets.pcap |
kernel/e1000.c包含了e1000的初始化代码和传送网络包的空函数,你将要实现这些空函数。kernel/e1000_dev.h定义了e1000的寄存器和标志位,具体根据e1000手册。kernel/net.c和kernel/net.h包含了实现了IP, UDP和ARP的简单的网络协议栈。这些文件同样使用固定大小的数据结构来保存网络包(mbuf)。最后kernel/pci.c包含xv6启动时在pci总线上寻找e1000设备的代码。
Your Job
完成kernel/e1000.c中的e1000_transmit()和e1000_recv()让驱动可以传输网络包
具体问题可以查看e1000手册。下面一些章节可能会比较有用
- Section2 是必须的,会给overview
- Section3.2 包接收
- Section3.3和Section3.4 包传输
- Section13 e1000使用的寄存器
- Section14 帮助理解初始化代码
阅读e1000手册,你需要熟悉Chapter 3, 4.1和14(可以先跳过2)。还会需要参考Chapter13。不需要关注太多细节,只需要实现基础功能。
e1000.c:e1000_init()函数配置e1000设备从ram中读写网络包。因为用了DMA。
因为数据包的达到速度可能会大于处理速度,e1000_init()供多个缓存让e1000设备使用。
The E1000 requires these buffers to be described by an array of “descriptors” in RAM; each descriptor contains an address in RAM where the E1000 can write a received packet.
e1000将缓冲区组织成”descriptor数组”,e1000将向descriptor中的地址收发数据。
struct rx_desc描述了descriptor的格式,descriptor数组是环形组织的(循环数组)。e1000_init()会创建mbuf数据包缓冲区。另外还有一个传输环(transmit ring),驱动将想要e1000发送的数据包放入。e1000_init()配置这两个环的大小分别文RX_RING_SIZE, TX_RING_SIZE
- 接收环
- 发送环
网络协议栈位于net.c,发包使用e1000_transmit()和mbuf中的数据包。你的实现必须使用一个数据包指针,指向传输环(transmit ring)的descriptor中的数据。struct tx_desc定义了结构。你需要保证每个mbuf当e1000完成数据包传输后(e1000会将descriptor的E1000_TXD_STAT_DD位置位)最终会被释放。
- 传输完成
E1000_TXD_STAT_DD置位
当e1000接收到网络数据包,它首先会用DMA将数据包传输到mbuf的”next RX(receive) ring descriptor”,然后产生中断。你的e1000_recv()实现需要扫描RX ring然后将mbuf中新的数据包使用net.c:net_rx()发送给网络协议栈。然后你需要申请新的mbuf然后放入descriptor,为下次dma做准备。
net.c:net_rx()发数据包给协议栈
除了要读写descriptor ring外,你的驱动还需要通过e1000映射到内存的控制器与e1000交互。以便检测是否由新数据包和通知e1000发送。全局变量regs保存了指向e1000第一个控制器的指针(数组),你的驱动可以索引该数组访问到其他控制器。你将使用E1000_RDT和E1000_TDT
- 与内存中的控制器交互
E1000_RDT,E1000_TDT
测试: 一个窗口中make server,另一个窗口make qemu和$ nettests。nettests的第一个测试会尝试发送UDP数据包给宿主机。如果实验完成,make server会发送响应。
hints
在e1000_transmit()和e1000_recv()中添加打印
实现e1000_transmit
TODO整理整个流程
- 读取
E1000_TDT寄存器,向e1000寻找它希望的next packet的TX ring索引 - 检查ring是否溢出。如果
E1000_TDT索引的descriptor status的E1000_TXD_STAT_DD没有置位,说明e1000没有完成响应传输请求,返回错误 - 否则,使用
mbuffree()释放从descriptor传输来的最后一个mbuf(如果存在的话) - 之后填充descriptor
m->head指向内存中的数据包m->len表示数据包的长度- 设置必要的cmd标志位(查看手册Section 3.3)
- 然后stash away a pointer to the mbuf for later freeing。保存一个
mbuf指针,方便之后释放
- 最后更新ring的位置使用
(1 + E1000_TDT) % TX_RING_SIZE - 如果
e1000_transmit()成功向ring添加mbuf则返回0。失败返回-1,然后调用者释放mbuf
实现e1000_recv的提示
- 找到ring中待接收的包(the next waiting received)的位置:使用
(E1000_RDT + 1)%TX_RING_SIZE, 注意是加1取模 - 查看对应descriptor的状态: 检测
status成员的E1000_RXD_STAT_DD位检查新数据包是否就绪,如果没就绪则停止 - 否则更新
mbuf的m->len为descriptor的记录的长度。使用net_rx()传递mbuf给网络协议栈处理 - 之后使用
mbufalloc()申请新的mbuf来替换刚刚传递给net_rx()的旧mbuf- 初始化新
mbuf: 将新mbuf的数据(m->head)指向descriptor记录的的addr,然后清除descriptor的状态位为0
- 初始化新
- 最后更新
E1000_RDT寄存器为ring中最后处理的descriptor e1000_init()使用mbuf初始化RX ring,你可以参考它是怎么做的- 会出现到达的包的数量超过ring的大小的情况,需要注意处理
因为xv6会存在多个线程使用e1000的情况,需要注意加锁。或者也可以在内核线程中处理使用e1000并处理中断。
result
机制小论文
- 需要熟悉基本框架
- 了解DMA工作
- 描述符(
descriptor)和具体接收数据的地方
- 描述符(
- 这个实验需要了解的东西比较多,但是跟着hint做就行
- mbuf index and descriptor index
- ring buffer的巧妙地方
- 如
transmit中用完当前mbuf后,指向下一个mbuf。当获取的mbuf非空时,说明就是上一次使用的mbuf,这时应该页过了一段时间了,缓存也够久了,再释放就能用了 - 即头遇到尾了就释放过期数据。
E1000_TDTTX Descripotr Tail
- 如
- 机制
- 使用ring buffer
- 设备的缓冲区, 如果
tx_mbufs,设备接收到的数据放入其中 - descriptor和mbuf一一对应,使用同样的index,难道设计就是”descriptor of mbuf?”
- 即
desc对应全局的tx_mbufs和rx_mbufs怎么用
- 即
- data are stored in buffers pointed by descriptor
- DNS测试可能需要科学上网,不过可以修改
nettests的dst