光拿乌云揉成团
像鲸鱼吻着浪
叫我和你去飞翔
——飞云之下
完整代码见:6.s081/kernel at net · SnowLegend-star/6.s081 (github.com)
下面节选了部分实验说明的内容:
由于数据包突发到达的速度可能快于驱动程序处理数据包的速度,因此e1000_init()为E1000提供了多个缓冲区,E1000可以将数据包写入其中。E1000要求这些缓冲区由RAM中的“描述符”数组描述;每个描述符在RAM中都包含一个地址,E1000可以在其中写入接收到的数据包。struct rx_desc描述描述符格式。描述符数组称为接收环或接收队列。它是一个圆环,在这个意义上,当网卡或驱动程序到达队列的末尾时,它会绕回到数组的开头。e1000_init()使用mbufalloc()为要进行DMA的E1000分配mbuf数据包缓冲区。此外还有一个传输环,驱动程序将需要E1000发送的数据包放入其中。e1000_init()将两个环的大小配置为RX_RING_SIZE和TX_RING_SIZE。
当net.c中的网络栈需要发送数据包时,它会调用e1000_transmit(),并使用一个保存要发送的数据包的mbuf作为参数。传输代码必须在TX(传输)环的描述符中放置指向数据包数据的指针。struct tx_desc描述了描述符的格式。您需要确保每个mbuf最终被释放,但只能在E1000完成数据包传输之后(E1000在描述符中设置E1000_TXD_STAT_DD位以指示此情况)。
当E1000从以太网接收到每个包时,它首先将包DMA到下一个RX(接收)环描述符指向的mbuf,然后产生一个中断。e1000_recv()代码必须扫描RX环,并通过调用net_rx()将每个新数据包的mbuf发送到网络栈(在net.c中)。然后,您需要分配一个新的mbuf并将其放入描述符中,以便当E1000再次到达RX环中的该点时,它会找到一个新的缓冲区,以便DMA新数据包。
除了在RAM中读取和写入描述符环外,您的驱动程序还需要通过其内存映射控制寄存器与E1000交互,以检测接收到数据包何时可用,并通知E1000驱动程序已经用要发送的数据包填充了一些TX描述符。全局变量regs包含指向E1000第一个控制寄存器的指针;您的驱动程序可以通过将regs索引为数组来获取其他寄存器。您需要特别使用索引E1000_RDT和E1000_TDT。
由于笔者对于计网实在是不甚感兴趣,故本次分析以仅以完成lab为导向。
其实弄懂了tx的各种数据结构后,跟踪hints一步步来就可以完成transmit()。
Hints:
首先,将打印语句添加到e1000_transmit()和e1000_recv(),然后运行make server和(在xv6中)nettests。您应该从打印语句中看到,nettests生成对e1000_transmit的调用。
实现e1000_transmit的一些提示:
1、首先,通过读取E1000_TDT控制寄存器,向E1000询问等待下一个数据包的TX环索引。
2、然后检查环是否溢出。如果E1000_TXD_STAT_DD未在E1000_TDT索引的描述符中设置,则E1000尚未完成先前相应的传输请求,因此返回错误。
这里倒是比较新颖,用STATUS的DD位来判断是否溢出。
3、否则,使用mbuffree()释放从该描述符传输的最后一个mbuf(如果有)。
释放上一个传输的mbuf(如果有的话),重新使用这个mbuf数据结构。
4、然后填写描述符。m->head指向内存中数据包的内容,m->len是数据包的长度。设置必要的cmd标志(请参阅E1000手册的第3.3节),并保存指向mbuf的指针,以便稍后释放。
应该设置那些标志呢?RS位和(还有EOP?)和DD位,DD位是STATUS中的。RS是重中之重,EOP和DD都可以自行选择设置。同时,当第 i 个 tx_desc 处理完后,对应的 mbuf 就要被释放。
5、最后,通过将一加到E1000_TDT再对TX_RING_SIZE取模来更新环位置。
6、如果e1000_transmit()成功地将mbuf添加到环中,则返回0。如果失败(例如,没有可用的描述符来传输mbuf),则返回-1,以便调用方知道应该释放mbuf。
具体的实现代码如下:
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.
//
// printf("This is transmit.\n");
// acquire(&e1000_lock); //不可以加锁,真是奇怪
//获取下一个可以发生数据包的位置
int tail=regs[E1000_TDT];
//检查环是否溢出
if(!(tx_ring[tail].status & E1000_TXD_STAT_DD)){
//如果描述符中的DD未设置,则表示环溢出
release(&e1000_lock);
return -1;
}
//释放上一个传输的mbuf(如果有的话)
if(tx_mbufs[tail]!=0){
mbuffree(tx_mbufs[tail]);
}
//填充描述符
tx_ring[tail].addr=(uint64)m->head;
tx_ring[tail].length=m->len;
tx_ring[tail].cmd = E1000_TXD_CMD_RS | E1000_TXD_CMD_EOP; //必须加上EOP啊?意思是每条报文的长度都不超过MTU是吗
tx_ring[tail].status=E1000_TXD_STAT_DD;
tx_mbufs[tail]=m;
// __sync_synchronize(); //加上会更好,不加也可以
//更新环的位置
tail=(tail+1)%TX_RING_SIZE;
regs[E1000_TDT]=tail;
// release(&e1000_lock);
return 0;
}
实现e1000_recv的一些提示:
1、首先通过提取E1000_RDT控制寄存器并加一对RX_RING_SIZE取模,向E1000询问下一个等待接收数据包(如果有)所在的环索引。
不同于发送模型中 mbuf 的空间需要我们自己分配,在接收模型中,mbufs 的每一个元素对应空间都已经被分配好了,用于存放接收到的报文。mbufs 中何时被放入了数据包我们不管,只需要按照 rx_ring 的顺序依次处理这些 mbuf 并将其递交给上层即可。
我们需要取得 rx_ring 中首个未处理(未递交)的报文,通过 (regs[E1000_RDT]+1)%RX_RING_SIZE 来确定(tail + 1)。注意,这里不是队列尾,而是队列尾 +1。对于发送模型而言,队列尾是下一个待处理的报文,而对于接收模型而言,队列尾是当前在处理的,而队列尾 +1才是下一个待处理的报文们。
2、然后通过检查描述符status部分中的E1000_RXD_STAT_DD位来检查新数据包是否可用。如果不可用,请停止。
3、否则,将mbuf的m->len更新为描述符中报告的长度。使用net_rx()将mbuf传送到网络栈。
看前半句话就行,怎么recv没有传入的m,是用rx_mbufs中的元素吗——对的。使用net_rx()不会出现自动补全函数的功能,在defs.h中使用ifdef来供编译器识别net.c中的函数。
4、然后使用mbufalloc()分配一个新的mbuf,以替换刚刚给net_rx()的mbuf。将其数据指针(m->head)编程到描述符中。将描述符的状态位清除为零。
这里也能看出与发送缓冲区队列不同的地方, 发送缓冲区队列初始时全为空指针, 而缓冲区实际由 sockwrite() 分配, 在最后时绑定到缓冲区队列中, 主要是为了方便后续释放缓冲区。而接收缓冲区队列在初始化时全部都已分配, 由内核解封装后释放内存。而此处由于缓冲区已经交由网络栈去解封装, 因此需要替换成一个新的缓冲区用于下一次硬件接收数据。
5、最后,将E1000_RDT寄存器更新为最后处理的环描述符的索引。
6、e1000_init()使用mbufs初始化RX环,您需要通过浏览代码来了解它是如何做到这一点的。
这句话才是一个纯粹的hint,不涉及代码实现。
7、在某刻,曾经到达的数据包总数将超过环大小(16);确保你的代码可以处理这个问题。
如何处理呢?处理包的速度比接收包的速度慢,导致 tail + 1 之后还有一些包未处理。这个要求应该是本次实验最困难的点,我是束手无策。这里我参考了其他人的做法,在 e1000_recv() 添加循环,tail 一直往后推。如果不处理这个问题就会出现bug
汗流浃背了~
8、您将需要锁来应对xv6可能从多个进程使用E1000,或者在中断到达时在内核线程中使用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()).
//
// printf("Here we should receive something.\n");
acquire(&e1000_lock);
//获取下一个等待接收数据包所在的环索引
int tail=(regs[E1000_RDT]+1)%RX_RING_SIZE;
//检查数据包是否可用
// if(!(rx_ring[tail].status && E1000_RXD_STAT_DD) ){
// release(&e1000_lock);
// return ;
// }
while(rx_ring[tail].status && E1000_RXD_STAT_DD ){ //循环是为了解决到达的数据包超过16的问题
//更新mbuf元素
rx_mbufs[tail]->len=rx_ring[tail].length;
//将mbuf传送到网络栈
net_rx(rx_mbufs[tail]);
rx_mbufs[tail]=mbufalloc(0);
if (!rx_mbufs[tail])
panic("e1000");
rx_ring[tail].addr=(uint64)rx_mbufs[tail]->head;
rx_ring[tail].status=0;
tail=(tail+1)%RX_RING_SIZE;
}
regs[E1000_RDT]=tail-1;
release(&e1000_lock);
}
当然在完成lab过程中还碰到了些bug。
在transmit()中加锁直接导致卡在testing ping了,debug也没发现原因到底是什么。
在recv()中没调用net_rx()也会卡住。
最后一个bug最为隐晦
这里应该是Google的DNS不能访问,把“8.8.8.8”这个地址改成国内运营商的DNS地址即可。我参考的是“114.114.114.114”这个中国电信的DNS。
nettests.c中的dns()
static void
dns()
{
#define N 1000
uint8 obuf[N];
uint8 ibuf[N];
uint32 dst;
int fd;
int len;
memset(obuf, 0, N);
memset(ibuf, 0, N);
// 8.8.8.8: google's name server
// dst = (8 << 24) | (8 << 16) | (8 << 8) | (8 << 0);
dst=(114 << 24) | (114 << 16) | (114 << 8) | (114 << 0);
if((fd = connect(dst, 10000, 53)) < 0){
fprintf(2, "ping: connect() failed\n");
exit(1);
}
最后贴个通过测试的截图吧:
说实话要完全理解这个lab还真得把《E100》那本手册的五节内容看完,我只看了两节就是一知半解的。照着hints完成lab还凑合,但是对于数据包的收发过程还是没有吃透。不过我向来对计网的内容不甚感兴趣,所以目标明确——完成lab就行。CSAPP的proxy还有点意思,相比之下这个lab的难度主要体现在手册文档的阅读上,代码实现就给个easy难度吧,就是上百页的英文手册属实是让我望而却步。