6.S081 net lab

0. 序

算是填了一个坑吧。

1 qemu参数的解释

ifeq ($(LAB),net)
QEMUOPTS += -netdev user,id=net0,hostfwd=udp::$(FWDPORT)-:2000 -object filter-dump,id=net0,netdev=net0,file=packets.pcap
QEMUOPTS += -device e1000,netdev=net0,bus=pcie.0
endif

查看qemu的手册,在设备模拟上,有一个Device Front End和Device Back End的概念,这里的-device以及后面的参数指定了设备的前端,即操作系统可见的部分,这是一张e1000的网卡,然后连在pcie的第0根总线上。-netdev则对应设备的后端,指出qemu应该如何模拟这个设备,这里包括了使用user模式hostfwd指定了qemu如何对host和guest的udp,tcp连接进行转发。-object参数则要求qemu保存e1000网卡对应的网络流量,这些参数的含义都可以在qemu的invokation页面找到,然后nettests.c中连接对应的转发端口向主机的server.py发送消息,完成了guest和host的通信。至此,清楚了实验的搭建逻辑。

2 pci总线枚举

pci_init()
{
  // we'll place the e1000 registers at this address.
  // vm.c maps this range.
  uint64 e1000_regs = 0x40000000L;

  // qemu -machine virt puts PCIe config space here.
  // vm.c maps this range.
  uint32  *ecam = (uint32 *) 0x30000000L;
  
  // look at each possible PCI device on bus 0.
  for(int dev = 0; dev < 32; dev++){
    int bus = 0;
    int func = 0;
    int offset = 0;
    uint32 off = (bus << 16) | (dev << 11) | (func << 8) | (offset);
    volatile uint32 *base = ecam + off;
    uint32 id = base[0];
    
    // 100e:8086 is an e1000
    if(id == 0x100e8086){
      // command and status register.
      // bit 0 : I/O access enable
      // bit 1 : memory access enable
      // bit 2 : enable mastering
      base[1] = 7;
      __sync_synchronize();

      for(int i = 0; i < 6; i++){
        uint32 old = base[4+i];

        // writing all 1's to the BAR causes it to be
        // replaced with its size.
        base[4+i] = 0xffffffff;        
        __sync_synchronize();
        uint32 new = base[4 + i];
        uint32 sz = (~(new & 0xfffffff0)) + 1;
        printf("%d\n", sz); // 打印出pci设备需要的内存大小

        base[4+i] = old;
      }

      // tell the e1000 to reveal its registers at
      // physical address 0x40000000.
      base[4+0] = e1000_regs;

      e1000_init((uint32*)e1000_regs);
    }
  }
}

之前看xv6源码时很困惑的一个地方在于xv6中假定了所有设备的MMIO地址都是已知的(硬编码在内核中),但实际的操作系统为了应对多种的硬件,显然不能这样做。这里的pci设备枚举的代码清楚了操作系统如何找到pci设备以及对应的物理地址
这里的代码在干嘛可以参考pci配置空间,我想因为qemu参数中显式地指定了e1000是连接在pcie.0上,所以这里没有对总线进行枚举,并不清楚func域在pcie中的含义。通过在BAR寄存器中写入e1000_regs,系统就显式地指定了e1000网卡应该位于物理地址的哪一部分。

3 e1000网卡的初始化

这一段其实没什么好说的,参考e1000网卡手册的第14章就清楚了。特别说一下这个作为dma buffer的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
};

从手册的描述上看,一个buffer就是一个字符数组就行了,为什么这里还需要next, head, len这些数据结构?其实这些数据结构并不是用在dma里面的,而是用于后面协议栈向上传递使用的。比如next指针,把mbuf串为一个链表,作为udp套接字的buffer链。

4 lab实现

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);
  int pkt_index = regs[E1000_TDT] % TX_RING_SIZE;
  if ((tx_ring[(pkt_index + 1) % TX_RING_SIZE].status & E1000_TXD_STAT_DD) == 0)
  {
    release(&e1000_lock);
    return -1;
  }
  for (int i = 0; i < TX_RING_SIZE; ++i)
  {
    if ((tx_ring[i].status & E1000_TXD_STAT_DD) == 1 && tx_mbufs[i])
    {
      mbuffree(tx_mbufs[i]);
      tx_mbufs[i] = 0;
    }
  }
  tx_mbufs[pkt_index] = mbufalloc(0);
  memmove(tx_mbufs[pkt_index]->buf, m->head, m->len);
  tx_ring[pkt_index].addr = (uint64)tx_mbufs[pkt_index]->head;
  tx_ring[pkt_index].length = m->len;
  tx_ring[pkt_index].cmd = E1000_TXD_CMD_RS | E1000_TXD_CMD_EOP | 2;
  tx_ring[pkt_index].status = 0;
  regs[E1000_TDT] = (regs[E1000_TDT] + 1) % TX_RING_SIZE;
  release(&e1000_lock);

  return 0;
}

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()).
  //
  //acquire(&e1000_lock);
  while (1)
  {
    int pkt_index = (regs[E1000_RDT] + 1) % RX_RING_SIZE;
    struct mbuf *m = 0;
    if ((rx_ring[pkt_index].status & E1000_RXD_STAT_EOP) == 0)
      return;
    if (rx_ring[pkt_index].errors != 0)
      goto done;
    m = mbufalloc(0);
    if (!m)
      goto done;

    m->len = rx_ring[pkt_index].length;
    memmove(m->buf, rx_mbufs[pkt_index]->buf, rx_ring[pkt_index].length);

  done:
    rx_ring[pkt_index].errors = 0;
    rx_ring[pkt_index].status = 0;
    regs[E1000_RDT] = (regs[E1000_RDT] + 1) % RX_RING_SIZE;
    //release(&e1000_lock);
    if (m)
      net_rx(m);
  }
}

有几个细节需要注意

  • 环形队列一个很不好的地方在于会使buffer长度减少1,这里实际上最多只能有15个pending的buffer
  • 虽然在e1000_init函数中写延迟中断寄存器为0,并不代表真的每个packet都会触发一次中断,实际上一次中断还是可能带来多个packet。
  • recv函数中没有加锁,这一点我不太确定。我觉得涉及到一个cpu affinity问题,就是网卡收到包,向PLIC申请中断后,PLIC到底向哪个hart发起中断,我没有在PLIC-SPEC里面找到相应的描述。查看kernel/plic.c可以看到
void
plicinithart(void)
{
  int hart = cpuid();
  
  // set uart's enable bit for this hart's S-mode. 
  *(uint32*)PLIC_SENABLE(hart)= (1 << UART0_IRQ) | (1 << VIRTIO0_IRQ);

#ifdef LAB_NET
  // hack to get at next 32 IRQs for e1000
  *(uint32*)(PLIC_SENABLE(hart)+4) = 0xffffffff;
#endif
  
  // set this hart's S-mode priority threshold to 0.
  *(uint32*)PLIC_SPRIORITY(hart) = 0;
}

而在main函数的初始化中,每个hart都会调用这个函数,即每个hart都开启了S-mode下的外部中断使能。因此这个地方留作一个困惑吧

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值