2020 6.s081——Lab11:Networking踩坑之旅

光拿乌云揉成团

像鲸鱼吻着浪

叫我和你去飞翔

——飞云之下

 完整代码见: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()为要进行DMAE1000分配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、否则,将mbufm->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难度吧,就是上百页的英文手册属实是让我望而却步。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值