xv6实验记录(五)
- 任务:为 xv6 的 E1000 网卡添加发送/接收网络包的功能。
- 官方实验指南:https://pdos.csail.mit.edu/6.828/2022/labs/net.html
- 官方参考书:https://pdos.csail.mit.edu/6.828/2022/xv6/book-riscv-rev3.pdf
一、实验前准备
# 实验前先切换分支
$ git fetch
$ git checkout net
$ make clean
在kernel/e1000.c
文件里,设备 E1000 的初始化代码已经给出。但是发送包和接受包的两个对应函数体暂时为空:e1000_transmit()
和 e1000_recv()
,因此本次实验的任务就是实现这两个函数,让xv6能够发送以及接收包
二、实验任务(transmit)
2.1、实验前提示
根据官方实验文档可以得到下述提示信息:
- 通过读取控制寄存器
E1000_TDT
的值(用regs[E1000_TDT]
的方式来读取),来获取“发送环(TX ring)”中下一个可以存放新 mbuf 的 tx_desc 结构体的位置,记为 idx - 通过判断 idx 所对应的 tx_desc 结构体的
E1000_TXD_STAT_DD
标志位是否为零,来判断“发送环”是否存在溢出的情况——如果为零,则 idx 指向的tx_desc 结构体存在尚未发送的 mbuf。该情况表明该“发送环”没有空的位置来存放新 mbuf(也即溢出),e1000_transmit()返回错误 - 如果存在一个已经发送 mbuf 的
tx_desc
结构体,则需要执行mbuffree()
函数,来将已经发送的 mbuf 数据进行释放以腾出空间(如果已经是空闲 tx_desc 的话,则没有必要执行)。这里注意,释放的是 mbuf,而不是外部包裹它的 tx_desc,因为后者需要被重复利用而没有必要反复释放和创建,以免在该 OS 的网络路径上造成不必要的 CPU 开销 - 执行从新 mbuf 到该 tx_desc 结构体的“封装”:需要将m->head 和 m->len 赋予至 idx 所对应的 tx_desc 结构体的合适字段,并且,需要在该 tx_desc 的 cmd 字段里设置合适的标志位,来指明该 tx_desc 所对应的mbuf 需要在将来被 DMA 传输至网卡后发送
- 将这个新 mbuf 的指针保存起来,用于之后的释放(与第三步的操作对应)
- 更新“发送环” 的 E1000_TDT,让其指向下一个可以封装新 mbuf 的tx_desc 位 置 ( 递 增 操 作 ), 然 后 不 要 忘 记 取 模 ( E1000_TDT modulo TX_RING_SIZE),以免越界
- 最后,如果 e1000_transmit()成功地向发送环添加了一个 mbuf 的话,则返回0;如果失败,则返回-1,将此信息告诉 e1000_transmit()的调用者
网卡E1000交互方法:
- E1000 使用了 DMA(direct memory access)技术,可以直接把接收到的数据包写入计算机的内存,这在数据量大的时候非常有用,可以当作缓存。
- 在发送时也可以把描述符写入内存的特定位置,这样 E1000 就会自己去找到待发送的数据,然后发送。
- 不管是接收还是发送,数据包都是以描述符数组描述的。在下面的接收和发送部分,会分别介绍接收描述符和发送描述符的格式。
2.2、实验过程
- 不管是接收还是发送,数据包都是以描述符数组描述。在xv6中发送数据包的描述符如下:
//在kernel/e1000_dev.h文件中有定义
struct tx_desc
{
uint64 addr; //数据包发送到地址
uint16 length; //数据包长度
uint8 cso; //checksum offset
uint8 cmd; //command field
uint8 status;
uint8 css;
uint16 special;
};
- 根据提示可以发现发送包的时候需要使用到一个信息保存信息的数据结构–
mbuf
因此首先来研究下这个结构体:
struct mbuf {
struct mbuf *next; // the next mbuf in the chain[下一个数据块的指针]
char *head; // the current start position of the buffer[头指针]
unsigned int len; // the length of the buffer[长度]
char buf[MBUF_SIZE]; // the backing store[数据保存]
};
e1000_transmit()
函数接收一个mbuf类型的网络数据,并写进相应的tx_desc内存地址,让网卡能够发现这个数据
有了上述知识铺垫并且根据实验提示可以实现如下代码:
//kernel/e1000.c文件:实现代码如下
int e1000_transmit(struct mbuf *m){
acquire(&e1000_lock);
uint idx = regs[E1000_TDT];
struct tx_desc *desc = &tx_ring[idx];
if(!(desc->status & E1000_TXD_STAT_DD)){
release(&e1000_lock); //标志位判断
return -1;
}
if(tx_mbufs[idx] != NULL){
mbuffree(tx_mbufs[idx]);
tx_mbufs[idx] = NULL;
}
desc->addr = (uint64)m->head;
desc->length = m->len;
desc->cmd = E1000_TXD_CMD_RS | E1000_TXD_CMD_EOP;
tx_mbufs[idx] = m;
regs[E1000_TDT] = (idx + 1) % TX_RING_SIZE;
release(&e1000_lock);
return 0;
}
三、实验任务(recv)
- 接收网络数据包方向:从“网卡”到“内存”
3.1、实验前提示
根据官方实验文档可以得到下述提示信息:
- 获取“接收环”中下一个要被“接收处理”的 rx_desc 结构体的位置 idx;获取的方法是查询其控制寄存器 E1000_RDT 的值,先加 1 再取模(modulo
RX_RING_SIZE
),然后再赋值给 idx - 此时 idx 的值指向了一个
rx_desc
结构体。需要通过查询其 E1000_RXD_STAT_DD 状态位来判断其是否为一个真正待“接收处理”的rx_desc 结构体;如果不是,则表明接收环中没有需要待接收处理的数据,e1000_recv()处理逻辑终止 - 如果 idx 指向的是一个待处理的 rx_desc,则开启一个循环,用来处理“接收环”中所有尚未处理的 rx_desc。这是因为接收环中可能存在多个尚未处理的 rx_desc,而 idx 此时指向的只是这些数据中最早到达接收环的那一个
- (第三步的循环内操作),“接收处理”一个 rx_desc 的大致操作是:获取 idx 指向的 rx_desc 中被嵌入的 mbuf 结构体指针;更新其大小为 rx_desc结构体中成员变量 length 的值(这是因为每次接收到的数据长度可能不一致,而 length 成员变量的值为当前接收到的数据长度);然后再调用
net_rx()
,将此 mbuf 向上层传递 - 第三步的循环内操作),每向上传递一次 mbuf,都需要在 idx 指向的位置分配一个新的 mbuf 结构体(之前的 mbuf 已经交给上层协议处理且其结束时间不确定),用于接收新的网络包。将这个新 mbuf 和 idx 指向的 rx_desc 结构体绑定的方式是:将 mbuf->head 赋予 rx_desc->addr,并且需要将该rx_desc 结构体的 status 成员变量状态位置零
3.2、实验过程
-
在
e1000_recv()
中,需要一次性读出所有的待读取数据包。也就是加一个循环,然后一直读取tail
位置的描述符,直到描述符的状态为未完成接收。 -
首先查看一下接受数据包的描述符
//在kernel/e1000_dev.h文件中有定义
struct rx_desc
{
uint64 addr; /* Address of the descriptor's data buffer */
uint16 length; /* Length of data DMAed into data buffer */
uint16 csum; /* Packet checksum */
uint8 status; /* Descriptor status */
uint8 errors; /* Descriptor Errors */
uint16 special;
};
比较重要 status
和 length
属性。网卡在写入的时候就会设置这些属性。
length
表示写入 addr
的数据包长度。status
则可以代表状态[TCPCS
、RSV
、VP
、IXSM
、EOP
、DD
]
- 需要用到的主要是 DD (Descriptor Done) 这个标志位。其表示网卡已经接收好了这个包。
根据上述提示以及铺垫的知识可以实现下述代码:
//kernel/e1000.c文件:实现代码如下
static void
e1000_recv(void)
{
while(1){
uint idx = (regs[E1000_RDT] + 1) % RX_RING_SIZE;
struct rx_desc *desc = &rx_ring[idx];
if(!(desc->status & E1000_RXD_STAT_DD)){
return; //标志位判断到DD
}
rx_mbufs[idx]->len = desc->length;
net_rx(rx_mbufs[idx]);
rx_mbufs[idx] = mbufalloc(0);
desc->addr = (uint64)rx_mbufs[idx]->head;
desc->status = 0;
regs[E1000_RDT] = idx;
}
}
四、结果测试
- 开启两个终端窗口,一个在 xv6 源码根目录下,敲
make server
- 另外一个在 xv6 的源码根目录下,敲
make qemu
,顺利进入 xv6 的内部终端后,敲nettests
。 - 在此场景下,xv6 扮演客户端,主机系统中的一个程序扮演服务端。客户端则会向服务端发送 UDP 数据包。后者接收后,则会向客户端发送响应信息
- 上述过程全部成功后,客户端会在终端输出:
testing ping: OK
而服务端会在终端输出:a message from xv6!
Make Grade测试
在根目录下敲入make grade
命令就可以测试整个程序