关于操作系统:xv6-源码窥探8网卡驱动

3次阅读

共计 5964 个字符,预计需要花费 15 分钟才能阅读完成。

前言

  • 本篇是对于 MIT 6.S081-2020-Lab11(networking) 的实现;
  • 如果发现内容上的纰漏,请不要悭吝您的键盘。

筹备工作

怎么说呢,这最初一个试验……十分得……乏味……(汗颜)

这个试验的测试分为两个局部:一是主机和 qemu 运行的 xv6 之间进行 APR 通信;二是 qemu 运行的 xv6 会向谷歌的 DNS server 发送一个查问,收到回复之后会打印输出。咱们的试验工作就是在设施驱动侧实现以太网数据帧的解决。

xv6 里的网络协议层很简略,自顶向下顺次有应用层、传输层、网络层和数据链路层。在这次试验中,传输层(Socket Layer)只有一个 UDP;网络层有 IP 和 ARP;数据链路层只关注将以太网帧载荷上传给网络层的局部。层与层之间都会有个 Packet Buffer,用来缓存层间上传和下传的 Packet,让层内的协定实体进行 PDU 的封装解封装。

各层内的协定实体的实现都是些独立的可并发调用的组件: 中断处理程序,IP Processing Thread,网络应用程序 。网卡将电信号或光信号转换成数字信号变成一个 Packet 放到一个网卡内置的内存队列里,而后会向 CPU 发送中断;中断处理程序会将网卡外部的 Packet 拷贝到 RAM 中的 Packet Buffer 中;IP Processing Thread 是个始终在运行的内核线程,能够解决 RAM 中 Packet 上传给 Socket Layer 的 Packet Buffer 中;最初 Socket Layer 再解决 Packet,最终递交到应用程序的 Packet Buffer 中。如果不算网卡内置的队列,在 RAM 中,仅仅是承受过程就有三个 Packet Buffer 进行交互。

队列的设计咱们曾经在先前的磁盘的中断处理程序见过了,过后是为理解耦中断处理程序和磁盘提出来队列的设计,当然这种设计很常见,并不是磁盘中断处理程序所独有的设计。到了网卡这边也是相似的作用,但还有两个非凡的语义:

  1. 若接收端存在短暂的大流量,硬件网卡的处理速度要比软件 IP Processing Thread 的处理速度快得多,因而为了对消这种速度差别,用了一个队列作为缓冲;
  2. 若网卡始终被占用,导致发送端队列中存在大量的 packet,就能够在网卡完预先在闲暇时将这些 packet 发走,从而进步网卡利用率。

很久以前没有利用 DMA 的时候,网卡接管到一个 packet 后会放到网卡的内置队列中,之后向 CPU 发送一个中断,之后 CPU 执行中断处理程序来将网卡内置队列中的数据逐字节拷贝到 RAM 的指定地位中,进而让 IP Processor Thread 读取。但实际上,CPU 拜访外设中的数据是个开销比拟大的动作,毕竟外设不像内存里 CPU 那么“近”。

但这次的试验要用的 E1000 型号的网卡要先进一点,网卡会将接管进来的 packet 通过 DMA 技术间接拷贝到 RAM 的指定地位中(通过网卡外部的 DMA Engine),而后才向 CPU 发送一个中断。响应中断后,中断处理程序不会再去拜访网卡,而是会间接拜访内存中的那个寄存 packet 的那个地位。所以这个地位是主机在初始化网卡的时候互相约定好的一个内存地址。具体地,咱们是用一个长为 16 的、大小为 16*1500 字节的 packet buffer 来在内存存储这些 packet 的 payload 的。而在拜访这些 packet 时,又是通过另外的长为 16、大小为 16*64 的数组来存储指向这些 packet 的指针的,这个数组被称为 DMA ring,是个循环数组。发送和接管都各有一个 DMA ring,对于发送来说叫 TX ring,对于承受来说叫 RX ring

这是一张路由器的性能图,咱们关注的是实心离散点的散布。路由器的工作就从输出网卡接管 Packet,再从另输入网卡把这个 Packet 打进来(同一个一张网卡既能输出也能输入 Packet,这里两个网卡调个个儿也是一样的)。 这里并没有使用 DMA 技术 ,所以 CPU 要拿到收到的 packet 时,是要拜访网卡而不是拜访 RAM。能够看到随着输出 pps(packets per second) 的减少,曲线是先回升、在降落。那么问题是,为什么先回升再降落、而不是始终回升、也不是先回升再不变?

CPU 在这里的工作只有两个:一是执行中断处理程序解决接收端网卡的中断,将 packet 拷贝到 RAM 中;二是执行某个内核线程将 packet 拷贝到发送端的网卡的缓冲中去。

之所以曲线回升到一段不再回升了,不是因为发送网卡的带宽太小,而是 CPU 利用率达到了 100%,以目前的 CPU 的性能曾经无奈再将解决更多的 Packet 了。当然,也不是说就肯定不是发送网卡带宽造成的瓶颈,只是大多状况网卡带宽绝对于 CPU 性能来说都是够用的。

因为每进来一个 packet 就会触发一次中断,解决这儿的中断可是一笔不低的 CPU 开销。如果网卡一直接管 packet,CPU 会始终在解决中断,而没工夫在把这些 packet 转移到另一个网卡,导致另一个网卡发送 packet 的速率反而降落了(这种景象被称为是 Live Lock,与 Dead Lock 绝对应,Dead Lock 放着不管它是不可能隐没的,而 Live Lock 则可能能够)。此时对 CPU 而言将接管进来的 packet 转交给另一个网卡才是更重要的工作,解决中断反而是主要的。所以索性 CPU 就关中断了,从中断模式转为轮询模式来解决接管网卡的承受的 packet( 被动解除 Live Lock,而不是被动地期待 Live Lock 隐没 )。如果能用 DMA 技术,就可能无效减缓这里的 Live Lock。

试验局部

试验的工作就是实现网卡驱动的顶半部的性能,一个读一个写,跟咱们在中断那一篇文章中介绍的 UART 是相似的。I/O 接口的初始化和底半部中断处理程序的代码曾经提前帮咱们写好了,所以整个驱动剩下都就是实现 int e1000_transmit(struct mbuf *m)void e1000_recv(void)。而试验领导书上的 hints 根本把读和写的流程具体的形容了一遍,基本上依照那个实现就行了。所以尽管是 hard,但其实要写的货色不算多,如果不看 hints 只看操作手册去实现,那才是真的难。

发送:

/* kernel/e1000.c */

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.
  //

  
// First ask the E1000 for the TX ring index 
// at which it's expecting the next packet, 
// by reading the E1000_TDT control register.

  acquire(&e1000_lock);
  uint64 tx_ring_index = regs[E1000_TDT];

// Then check if the the ring is overflowing. 
// If E1000_TXD_STAT_DD is not set in the descriptor 
// indexed by E1000_TDT, the E1000 hasn't finished 
// the corresponding previous transmission request, 
// so return an error.

  if ((tx_ring[tx_ring_index].status & E1000_TXD_STAT_DD) == 0) {release(&e1000_lock);
    return -1;
  }

// Otherwise, use mbuffree() to free the last mbuf 
// that was transmitted from that descriptor (if there was one).
  if (tx_mbufs[tx_ring_index])
    mbuffree(tx_mbufs[tx_ring_index]);

// Then fill in the descriptor. 
// m->head points to the packet's content in memory, 
// and m->len is the packet length. 
// Set the necessary cmd flags (look at Section 3.3 in the E1000 manual) 
// and stash away a pointer to the mbuf for later freeing.
  tx_ring[tx_ring_index].addr = (uint64)m->head;
  tx_ring[tx_ring_index].length = m->len;
  tx_ring[tx_ring_index].cmd = E1000_TXD_CMD_EOP | E1000_TXD_CMD_RS;
  tx_mbufs[tx_ring_index] = m;


// Finally, update the ring position by adding one to E1000_TDT modulo TX_RING_SIZE.
  regs[E1000_TDT] = (tx_ring_index + 1) % TX_RING_SIZE;
  release(&e1000_lock);
  
  return 0;
}

接管:

/* kernel/e1000.c */

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()).
  //

  // First ask the E1000 for the ring index 
  // at which the next waiting received packet (if any) is located,
  // by fetching the E1000_RDT control register and adding one modulo RX_RING_SIZE.
  while (1) {uint64 rx_ring_index = regs[E1000_RDT];
    rx_ring_index = (rx_ring_index + 1) % RX_RING_SIZE;

    // Then check if a new packet is available 
    // by checking for the E1000_RXD_STAT_DD bit 
    // in the status portion of the descriptor. If not, stop.
    if ((rx_ring[rx_ring_index].status & E1000_RXD_STAT_DD) == 0)
      break;

    // Otherwise, update the mbuf's m->len to the length reported in the descriptor.
    // Deliver the mbuf to the network stack using net_rx().
    rx_mbufs[rx_ring_index]->len = rx_ring[rx_ring_index].length;
    net_rx(rx_mbufs[rx_ring_index]);

    // Then allocate a new mbuf using mbufalloc() to replace the one just given to net_rx().
    // Program its data pointer (m->head) into the descriptor.
    // Clear the descriptor's status bits to zero.
    if ((rx_mbufs[rx_ring_index] = mbufalloc(0)) == 0)
      break;
    rx_ring[rx_ring_index].addr = (uint64)rx_mbufs[rx_ring_index]->head;
    rx_ring[rx_ring_index].status = 0;

    // Finally, update the E1000_RDT register to be the 
    // index of the last ring descriptor processed.
    regs[E1000_RDT] = rx_ring_index;

    // At some point the total number of packets
    // that have ever arrived will exceed the ring size (16); 
    // make sure your code can handle that.
  }
}

课程总结

这 11 个试验做下来感觉难度最大的还是页表和锁的试验,毕竟最终测试都还没通过……当前有工夫感觉 xv6 的货色有点忘了的时候再把这两个试验做一遍就行了,算是本人给本人挖的坑吧。此外,另外两个比拟重要的试验是过程调度、文件系统的试验,了解工作原理很重要。最初还有工夫的话再把 mmap 试验的 fork 升级成 cow fork。总之当初曾经把 6.S081 2020 的所有试验从头到尾都做了一遍,能够临时先告一段落了。

做这种国外公开的课程试验,其过程比拟好受的,因为它没有老师、没有同学,意味着遇到问题你只能在自研或是在互联网上求教陌生人(但 MIT 的这门课的传授上课讲的内容十分清晰,果然还是名校的老师);没有问题、没有学分、没有 DDL,意味着学上来纯靠主观能动性,不会有第三方的压力倒逼你本人。所以做这种课程试验最好是选测试充沛的(每个试验都要执行对应的 uint test 后执行 usertests 做回归测试)、有 auto grade score 的(make grade),要不然你本人都不晓得本人做的对不对。

最开始的时候齐全不相熟 xv6,甚至连操作系统的一些最基础理论常识都不了解,以致于第一和第二个试验是大量参考了他人现成的代码才勉强通过的。在第三个页表试验的时候开始独立地做了上来。这也是为什么前两个试验都没有对应的博文,页表和锁的试验的测试挂了还厚着脸皮写了博文。哪怕是问他人,也不要看他人写好的代码,否则没有意义。

不晓得求实的文章应该长什么样子,写博客文章这种货色的技术上限能够说是零。本人写这种博文的目标大都不是为了给他人看才写的。只是感觉学了货色,得找个中央把它记录下来,写的时候顺便一直去比照去校对认知,那就感觉这是求实的,因为输入的过程才是重要的。记录下来了之后,想着什么时候再去温故一下都是主要的,因为很多货色写进去之后我本人都不打算当前再去看一遍。

私是个每天都会被本人菜醒的生物,以上。

参考链接

  • Ring Buffer
正文完
 0