MIT-6.S081 | Networking(2021)

265 阅读6分钟

Lab7: Networking

最终的任务其实很简单,就是实现 E1000 网卡驱动中的 transmit()recv() 函数。

E1000 的交互方法

接收

如果网卡收到了数据,会产生一个中断,然后调用对应的中断处理程序去处理这个新到达的数据。

描述符

接收描述符的格式如下:

lab7_recv_desc.png

// [E1000 3.2.3]
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;
};

​ 我们会在内存中放一个数组的描述符,然后这个数组会被解读成一个环形队列。

​ 如果网卡接收到了一个新的数据包会检查环形队列 head 位置的描述符。然后把数据写入 head 描述符的缓冲区,也就是 addr 记录的地址。

​ 这里比较重要的还有 statuslength 属性。网卡在写入的时候就会设置这些属性。

​ 其中,length 表示写入 addr 的数据包长度。status 则可以代表下列状态:

image-20230520160703209.png

其中,我们需要用到的主要是 DD (Descriptor Done) 这个标志位。其表示网卡已经接收好了这个包。

环形队列

​ 上面我们提到了,如果网卡收到了新的数据,会往环形队列 head 位置描述符的缓冲区写入数据,下面来讨论网卡和驱动程序是如何具体管理这个缓冲区的。

​ 下图展示了接收描述符环形队列的结构:

image-20230520160751492.png

​ 初始化时,head 为 0,tail 为队列缓冲区减一。

​ 其中,headtail 的这段浅色的区域是空闲的(图有点问题, tail 指向的位置也时空闲的)。也就是说,这个区域内的数据包都已经被软件处理好了,那么如果有新的数据包到达,网卡会把数据写入这个区域的开始,也就是 head,把老的数据覆盖掉。网卡把老的数据覆盖掉后会把 head 的值加一。

​ 而软件会按照顺序处理深色的区域。读取环形队列时,读取的是 tail + 1 位置描述符缓冲区的数据(这个位置是所有未处理数据中等待时间最长的),处理完这个缓冲区后会把 tail 增加一。

发送

描述符

发送描述符的格式如下:

image-20230520160936064.png

image-20230520160936064.png

// [E1000 3.3.3]
struct tx_desc
{
  uint64 addr;
  uint16 length;
  uint8 cso;
  uint8 cmd;
  uint8 status;
  uint8 css;
  uint16 special;
};

​ 其中 addrlength 的作用和接收描述符的作用相同。除了这两个,我们主要还需要用到 cmdstatus 这两个属性。和接收标志位一样,在 status 中我们需要用到 DD 标志位,表示当前标志位指向的数据是否发送完成。而 cmd 描述了传输这个数据包时的一些设置,或者说对于网卡的命令。

image-20230520161142024.png

这里需要用到的命令有如下几个:

  • RPS (Report Packet Sent):设置之后,网卡会报告数据包发送的状态。比如,在描述符指向的数据发送完成后,网卡会设置描述符的 DD 标志位。
  • EOP (End of Packet):表明这个描述符是数据包的结尾。如果要发送的数据包特别大,我们可能会用很多个描述符的缓存空间来储存一个包。那么可以给这个数据包的最后一个描述符设置 EOP 命令。只有这样才能给这个描述符加上一些别的功能,如 IC,即加入和校验。

环形队列

​ 和接收描述符的环形队列略有不同,发送描述符的 headtail 这段区域(途中浅色区域)表示我们希望发送,但是网卡还没发送出去的数据

image-20230520161254894.png

​ 其中 head 指向等待时间最长的待发送数据,网卡会从这里开始发送。完成后会把 tail 加一而如果我们要新加入一个描述符,是从 tail 这个方向加入的,也会把 tail 加一。

lab

发包:

​ 当network stack需要发送一个packet的时候,会先将这个packet存放到发送环形缓冲区tx_ring,最后通过网卡将这个packet发送出去。(每次发送packet前都需要检查一下上一次的packet发送完没,如果发送完了,要将其的释放掉)

收包:

​ 当网卡需要接收packet的时候,网卡会直接访问内存(DMA),先将接受到的RAM的数据(即packet的内容)写入到接收环形缓冲区rx_ring中。接着,网卡会向cpu发出一个硬件中断,当cpu接受到硬件中断后,cpu就可以从接收环形缓冲区rx_ring中读取packet传递到network stack中了(net_rx())。(网卡会一次性接收全部的packets,即接收到rx_ring溢出为止)

e1000_transmit

/* Transmit Descriptor command definitions [E1000 3.3.3.1] */
#define E1000_TXD_CMD_EOP    0x01 /* End of Packet */
#define E1000_TXD_CMD_RS     0x08 /* Report Status */

​ 只给了这两个描述符,所以可以推测出只用这两个。

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.
  //
  acquire(&e1000_lock);
  //hint 1 cur-index
  uint32 tx_index = regs[E1000_TDT];
  //hint 2 检查溢出
  if((tx_ring[tx_index].status & E1000_TXD_STAT_DD) == 0)
  {
    release(&e1000_lock);
    return -1;
  }
  //hint 3 用mbuffree()释放mbuf
  if(tx_mbufs[tx_index])
  {
    mbuffree(tx_mbufs[tx_index]);
  }
  //hint 4 添加描述符
  tx_mbufs[tx_index] = m;
  tx_ring[tx_index].addr = (uint64)m->head;
  tx_ring[tx_index].length = m->len;
  tx_ring[tx_index].cmd = E1000_TXD_CMD_RS | E1000_TXD_CMD_EOP;

  //hint 5 更新地址
  regs[E1000_TDT] = (tx_index+1)%TX_RING_SIZE;
  
  release(&e1000_lock);

  return 0;
}

​ 这里有一个问题为什么要用锁?

  • 因为会有多线程测试,可能会出现多个线程会访问同一个mbuf的情况,导致竞争出错。
  • 获取锁之后,如果该索引能被访问,就要将原有的(上一次的)mbuf其释放掉,以便装入新的mbuf

e1000_recv

​ 为什么不用锁?

因为recv过程发生在OSI协议的数据链路层,而在这个层次的packet接收是不区分进程。既然不区分进程接收,自然就可以一直接收,直到不能再接收为止(溢出)。

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)
  {
    //hint 1 get next index
    uint32 rx_index = (regs[E1000_RDT] + 1) % RX_RING_SIZE;
    //hint 2 check status
    if((rx_ring[rx_index].status & E1000_RXD_STAT_DD) == 0)
      return;
    //hint 3 更新length; 用net_rx将mbuf传到stack
    rx_mbufs[rx_index]->len = rx_ring[rx_index].length;
    net_rx(rx_mbufs[rx_index]);
    //hing 4 用mvufalloc分一个新的mbuf替换
    rx_mbufs[rx_index] = mbufalloc(0);
    rx_ring[rx_index].addr = (uint64)rx_mbufs[rx_index]->head;
    rx_ring[rx_index].status = 0;
    //hint 5 更新E1000_RDT
    regs[E1000_RDT] = rx_index;
  }
}

这两个完全就是跟着hint写。