实验概要
本次实验将涉及网络驱动程序的开发,特别是针对e1000网卡的数据包发送和接收功能。实验的核心是完成e1000_transmit()和e1000_recv()两个函数,这需要深入理解发送和接收描述符的结构体以及相关的命令和状态位。在e1000_transmit()函数中,我们将把mbuf中的以太网帧编程到TX描述符环中,以便E1000可以发送它,并在发送后释放指针。而在e1000_recv()函数中,我们将检查是否有从E1000接收到的数据包,并为每个数据包创建并传递一个mbuf。通过完成这两个函数,我们将能够实现网络驱动程序的基本数据包发送和接收功能。
实验思路
任务要求完成e1000_transmit()和e1000_recv()两个函数,使驱动程序能够进行数据包的发送和接收。e1000_transmit()函数需要将要发送的数据包放入发送队列中的描述符,并在E1000完成发送后释放相应的缓冲区。而e1000_recv()函数则需要扫描接收队列,将接收到的数据包传递给网络栈,并为下次接收分配新的缓冲区。此外,还需要通过内存映射控制寄存器与E1000进行通信,以检测接收到的数据包并通知E1000发送队列已经填充完毕。
了解发送和接收描述符
根据以上结构体,可以在the E1000 Software Developer's Manual
手册找到对应的结构
tx_desc
rx_desc
上面的代码定义了发送和接收描述符的结构体以及相关的命令和状态位。其中主要观察以下的定义:
tx_desc
结构体定义了发送描述符的格式,其中包含数据缓冲区地址、长度、校验和等信息。通过这些描述符,驱动程序可以告诉网卡应该如何发送数据包。E1000_TXD_CMD_EOP
和E1000_TXD_CMD_RS
是发送描述符命令的两个标志位,分别表示数据包结束和报告状态。这些标志位用于告诉网卡在发送数据包后应该执行哪些操作。E1000_TXD_STAT_DD
是发送描述符状态的一个标志位,表示描述符已经完成。rx_desc
结构体定义了接收描述符的格式,其中包括描述符数据缓冲区的地址、长度、校验和等信息。通过这些描述符,驱动程序可以告诉网卡应该如何接收数据包。E1000_RXD_STAT_DD
和E1000_RXD_STAT_EOP
是接收描述符状态的两个标志位,分别表示描述符已经完成和数据包结束。
了解初始化过程
tx_ring是一个存储发送数据帧描述符的数组,用于管理发送队列。
tx_mbufs是一个存储指向发送数据缓冲区(mbuf)的指针数组,用于保存待发送的数据。
tx_ring和tx_mbufs的初始化过程:
- 首先,通过memset函数将tx_ring数组的所有元素初始化为0,确保初始状态下不会存在脏数据。
- 接下来,使用for循环遍历tx_ring数组,并设置每个描述符的状态为E1000_TXD_STAT_DD(表示描述符可用)。
- 同时,将tx_mbufs数组的每个元素初始化为0,表示当前没有待发送的数据。
- 将tx_ring数组的内存地址赋值给寄存器regs的E1000_TDBAL字段,告诉硬件发送队列的起始地址。
- 设置寄存器regs的E1000_TDLEN字段为tx_ring数组的大小。
- 将寄存器regs的E1000_TDH和E1000_TDT字段都设置为0,以确保发送队列的头和尾指针都指向初始位置。
rx_ring是一个存储接收数据帧描述符的数组,用于管理接收队列。
rx_mbufs是一个存储指向接收数据缓冲区(mbuf)的指针数组,用于保存接收到的数据。
rx_ring和rx_mbufs的初始化过程:
- 首先,通过memset函数将rx_ring数组的所有元素初始化为0,确保初始状态下不会存在脏数据。
- 使用for循环遍历rx_ring数组,并为每个描述符分配一个mbuf,并将mbuf的头部地址赋值给对应的描述符的addr字段。
- 将rx_ring数组的内存地址赋值给寄存器regs的E1000_RDBAL字段,告诉硬件接收队列的起始地址。
- 设置寄存器regs的E1000_RDH字段为0,表示接收队列的头指针指向初始位置。
- 设置寄存器regs的E1000_RDT字段为RX_RING_SIZE - 1,表示接收队列的尾指针指向最后一个描述符。
- 设置寄存器regs的E1000_RDLEN字段为rx_ring数组的大小。
实验步骤
一些关于实现
e1000_transmit
的提示:首先,通过读取
E1000_TDT
控制寄存器,向E1000询问它期望下一个数据包的TX环索引。然后检查环是否溢出。如果在由
E1000_TDT
指定的描述符中未设置E1000_TXD_STAT_DD
位,则表示E1000尚未完成对应的上一个传输请求,因此返回错误。否则,使用
mbuffree()
释放最后一个从该描述符传输的mbuf(如果有的话)。然后填充描述符。
m->head
指向内存中数据包的内容,m->len
是数据包的长度。设置必要的命令标志(请参考E1000手册中的第3.3节),并保存mbuf的指针以便稍后释放。最后,通过将
E1000_TDT
加一取模TX_RING_SIZE
来更新环位置。如果
e1000_transmit()
成功将mbuf添加到环中,则返回0。如果失败(例如,没有可用的描述符来传输mbuf),则返回-1,以便调用者知道要释放mbuf。
根据给出的提示,逐步实现e1000_transmit
函数:
获取当前传输描述符的索引。通过读取
E1000_TDT
寄存器来获取当前传输描述符的索引,即tx_index = regs[E1000_TDT]
。检查当前传输描述符的状态。通过检查当前传输描述符的状态位,判断上一个传输请求是否已完成。如果状态位中的
E1000_TXD_STAT_DD
位为0,表示上一个传输请求尚未完成,此时应返回错误码-1。释放上一个传输描述符对应的mbuf。如果上一个传输描述符对应的mbuf不为空,则使用
mbuffree()
函数释放该mbuf。填充当前传输描述符。将传入的mbuf赋值给当前传输描述符对应的mbuf,即
tx_mbufs[tx_index] = m
。设置当前传输描述符的地址字段为mbuf的头部地址,即tx_ring[tx_index].addr = (uint64)m->head
。设置当前传输描述符的长度字段为mbuf的长度,即tx_ring[tx_index].length = m->len
。设置当前传输描述符的命令字段,指定结束包(EOP)和报告状态(RS),即tx_ring[tx_index].cmd = E1000_TXD_CMD_EOP | E1000_TXD_CMD_RS
。更新传输描述符环的TDT寄存器。通过将当前传输描述符的索引加一取模
TX_RING_SIZE
来更新TDT寄存器的值,即regs[E1000_TDT] = (tx_index + 1) % TX_RING_SIZE
。释放e1000锁。使用
release(&e1000_lock)
释放e1000锁。返回成功状态码。返回0表示成功将mbuf添加到环中。
int e1000_transmit(struct mbuf *m)
{
// 将mbuf中的以太网帧编程为TX描述符环,以便e1000发送它。储存一个指针,以便在发送后释放。
acquire(&e1000_lock); // 获得e1000锁,确保同一时间只有一个线程可以访问e1000
uint32 tx_index = regs[E1000_TDT]; // 获取当前传输描述符的索引
// 如果当前传输描述符的状态不是已完成(DD标志位为0),表示描述符还未准备好,则释放锁并返回错误码-1
if ((tx_ring[tx_index].status & E1000_TXD_STAT_DD) == 0){
release(&e1000_lock);
return -1;
}
// 如果当前传输描述符对应的mbuf不为空,则释放该mbuf
if (tx_mbufs[tx_index]){
mbuffree(tx_mbufs[tx_index]);
}
tx_mbufs[tx_index] = m; // 将传入的mbuf赋值给当前传输描述符对应的mbuf
tx_ring[tx_index].addr = (uint64)m->head; // 设置当前传输描述符的地址字段为mbuf头部地址
tx_ring[tx_index].length = m->len; // 设置当前传输描述符的长度字段为mbuf的长度
tx_ring[tx_index].cmd = E1000_TXD_CMD_EOP | E1000_TXD_CMD_RS; // 设置当前传输描述符的命令字段,指定结束包(EOP)和报告状态(RS)
regs[E1000_TDT] = (tx_index + 1) % TX_RING_SIZE; // 更新传输描述符环的TDT寄存器,指向下一个描述符的索引
release(&e1000_lock); // 释放e1000锁
return 0; // 返回成功状态码
}
一些关于实现
e1000_recv
的提示:首先,通过获取
E1000_RDT
控制寄存器并加一取模RX_RING_SIZE
,向E1000询问下一个等待接收到的数据包(如果有)所在的环索引。然后,通过检查描述符的状态部分中的
E1000_RXD_STAT_DD
位,检查是否有新的数据包可用。如果没有,停止。否则,将mbuf的
m->len
更新为描述符中报告的长度。使用net_rx()
将mbuf传递给网络堆栈。然后,使用
mbufalloc()
分配一个新的mbuf,以替换刚刚传递给net_rx()
的那个mbuf。将其数据指针(m->head
)编程到描述符中。将描述符的状态位清零。最后,更新
E1000_RDT
寄存器为已处理的最后一个环描述符的索引。
e1000_init()
函数使用mbufs初始化了RX环,你可以参考它的代码并借鉴一些实现方法。在某个时刻,已经到达的数据包总数将超过环的大小(16),请确保你的代码能够处理这种情况。
根据给出的提示,逐步实现e1000_recv
函数:
首先,需要计算出下一个待接收数据包的描述符索引。这个索引值可以通过获取
E1000_RDT
寄存器并加一取模RX_RING_SIZE
来得到。cuint32 rx_index = (regs[E1000_RDT] + 1) % RX_RING_SIZE;
接着,需要检查新接收到的数据包是否已经准备好。数据包的状态信息存放在描述符的status字段中。我们可以通过检查其中的
E1000_RXD_STAT_DD
位来判断数据包是否已经准备好。cif ((rx_ring[rx_index].status & E1000_RXD_STAT_DD) == 0) return;
如果数据包已经准备好,就可以将对应的mbuf传递给网络堆栈进行处理。此时,需要将mbuf的长度设置为描述符中报告的长度,并调用
net_rx()
函数来处理接收到的数据包。crx_mbufs[rx_index]->len = rx_ring[rx_index].length; net_rx(rx_mbufs[rx_index]);
处理完接收到的数据包后,需要为下一个数据包分配一个新的mbuf。可以使用
mbufalloc()
函数来进行分配。然后,需要将新mbuf的头指针编程到描述符中的地址字段,并清除描述符的状态字段。crx_mbufs[rx_index] = mbufalloc(0); rx_ring[rx_index].addr = (uint64)rx_mbufs[rx_index]->head; rx_ring[rx_index].status = 0;
最后,需要更新
E1000_RDT
寄存器为已处理的最后一个环描述符的索引。这样做可以通知E1000网卡下一次数据包接收的起始位置。cregs[E1000_RDT] = rx_index;
循环执行上述步骤,直到没有新的数据包可接收。
完整代码:
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()).
//
while (1) {
uint32 rx_index = (regs[E1000_RDT] + 1) % RX_RING_SIZE; // 计算下一个接收描述符的索引
if ((rx_ring[rx_index].status & E1000_RXD_STAT_DD) == 0) // 检查接收描述符是否已经准备好
return;
rx_mbufs[rx_index]->len = rx_ring[rx_index].length; // 设置接收到的mbuf的长度为接收描述符中的长度字段
net_rx(rx_mbufs[rx_index]); // 调用net_rx()函数,传递接收到的mbuf以处理接收到的数据包
rx_mbufs[rx_index] = mbufalloc(0); // 为下一个接收描述符分配一个新的mbuf
rx_ring[rx_index].addr = (uint64)rx_mbufs[rx_index]->head; // 设置下一个接收描述符的地址字段为新mbuf的头部地址
rx_ring[rx_index].status = 0; // 清除下一个接收描述符的状态字段
regs[E1000_RDT] = rx_index; // 更新接收描述符环的RDT寄存器,指向下一个描述符的索引
}
}
测试结果
在 xv6 目录下执行 make server
命令启动服务端。
在另一个终端执行 make qemu
命令启动 xv6 操作系统。然后执行 nettests
命令进行网络测试。
如果想查看网络报文,可以在终端执行 tcpdump -XXnr packets.pcap
命令来捕获报文。
可以使用 make grade
命令来对 xv6 的网络功能进行测试和评分。
实验总结
在本实验中,我们实现了e1000网卡驱动中的e1000_transmit()
和e1000_recv()
函数,分别用于发送和接收以太网帧。
在实现e1000_transmit()
函数时,我们首先需要将mbuf中的以太网帧编程为TX描述符环,以便E1000发送它,并储存一个指针,在发送后释放。这个函数的核心逻辑是将mbuf结构体嵌入至发送环中的某个空闲的tx_desc结构体中。由于发送操作可能会被多个线程同时调用,因此我们需要在函数中考虑多线程环境下的同步与互斥,正确使用锁机制确保多个线程对共享资源的安全访问,这有助于提高代码的稳定性和可靠性。
而在实现e1000_recv()
函数时,我们需要检查是否有从E1000接收到的数据包,并为每个数据包创建并传递一个mbuf。这个函数的核心逻辑是不断地检查是否有新的数据包到达,然后为每个数据包创建相应的mbuf并进行处理。由于接收操作只会在中断处理函数中被调用,不会出现并发的情况,且接收和发送数据结构是独立的,不存在共享,因此在接收数据时无需加锁。
通过本实验的实现,我们了解了E1000网卡驱动的工作原理,掌握了如何在多线程环境下实现发送和接收以太网帧的操作,并学会了如何正确使用锁机制来保证代码的稳定性和可靠性。