NAPI的一些知识

NAPI Linux 上采用的一种提高网络处理效率的技术,它的核心概念就是不采用中断的方式读取数据,

而代之以首先采用中断唤醒数据接收的服务程序,然后 POLL 的方法来轮询数据,(类似于底半( bottom-half )处理模式);

从我们在实验中所得到的数据来看,在随着网络的接收速度的增加, NIC 触发的中断能做到不断减少,

目前 NAPI 技术已经在网卡驱动层和网络层得到了广泛的应用,

驱动层次上已经有 E1000 系列网卡, RTL8139 系列网卡, 3c50X 系列等主流的网络适配器都采用了这个技术,

而在网络层次上, NAPI 技术已经完全被应用到了著名的 netif_rx 函数中间,

并且提供了专门的 POLL 方法 --process_backlog 来处理轮询的方法;

根据实验资料表明采用 NAPI 技术可以大大改善短长度数据包接收的效率,减少中断触发的时间;

由于 RTL8139C 是一种应用比较广泛的网络适配器,所以本文以其为例,说明了 NAPI 技术在网络适配器上的应用和基本原理。

 

但是 NAPI 存在一些比较严重的缺陷:

而对于上层的应用程序而言,系统不能在每个数据包接收到的时候都可以及时地去处理它,而且随着传输速度增加,

累计的数据包将会耗费大量的内存,经过实验表明在 Linux 平台上这个问题会比在 FreeBSD 上要严重一些;

另外采用 NAPI 所造成的另外一个问题是对于大的数据包处理比较困难,

原因是大的数据包传送到网络层上的时候耗费的时间比短数据包长很多(即使是采用 DMA 方式),

所以正如前面所说的那样, NAPI 技术适用于对高速率的短长度数据包的处理,在本文的末尾提出了 NAPI 的改善方法,和实验数据。

 

使用 NAPI 先决条件:

驱动可以继续使用老的 2.4 内核的网络驱动程序接口, NAPI 的加入并不会导致向前兼容性的丧失,

但是 NAPI 的使用至少要得到下面的保证:

A. 要使用 DMA 的环形输入队列,或者是有足够的内存空间缓存驱动获得的包。

B. 在发送 / 接收数据包 ? 生中断的时候有能力关断 NIC 中断的事件处理,并且在关断 NIC 以后,

  并不影响数据包接收到网络设备的环形缓冲区 ( 以下简称 rx-ring ) 处理队列中。

  NAPI 对数据包到达的事件的处理采用轮询方法,在数据包达到的时候, NAPI 就会强制执行 dev->poll 方法。

  而和不象以前的驱动那样为了减少包到达时间的处理延迟,通常采用中断的方法来进行。

  应当注意的是,经过测试如果 DEC Tulip 系列( DE21x4x 芯片)以及 National Semi 的部分网卡芯片,

  的测试表明如果把从前中断处理的部分都改换用设备的 POLL 方法去执行,那么会造成轻微的延迟,

  因此在进行 MII (介质无关)的操作上就需要一些小小的诀窍 , 详见 mii_check_media 的函数处理流程,本文不做详细讨论。

 

  在下面显示的例子表示了在 8139 中如何把处理过程放在 dev poll 方法中,

  把所有的原来中断应该处理的过程放在了 POLL 方法里面,篇幅起见,我们只介绍接收的 POLL 方法。

 

  在下面的 8139CP 驱动程序介绍中表明了可以把在中断程序中所做的任何事情放在 POLL 方法中去做,

  当然不同的 NIC 在中断中所要处理的状态和事件是不一样的。

  对于所有的 NIC 设备,以下两种类型的 NIC 接收事件寄存器响应机制:

  1. COR 机制:当用户程序读状态 / 事件寄存器,读完成的时候寄存器和 NIC rx-ring 中表示的状态队列将被清零,

  natsemi sunbmac NIC 会这样做,在这种情况下,必须把 NIC 所有以前的中断响应的处理部分都移动到 POLL 方法中去。

  2. COW 机制:用户程序写状态寄存器的时候,必须对要写的位先写 1 0 ,如下面要介绍的 8139CP 就是这样的类型,

  大多数的 NIC 都属于这种类型,而且这种类型对 NAPI 响应得最好,它只需要把接收的资料包处理部分放置在 POLL 方法中,

  而接收事件的状态处理部分放在原先的中断控制程序中 , 我们等下将要介绍的 8139CP 类型网卡就是属于这种类型。

C 有防止 NIC 队列中排队的数据包冲突的能力。

  当关断发送 / 接收事件中断的时候, NAPI 将在 POLL 中被调用处理,由于 POLL 方法的时候, NIC 中断已经不能通知包到达,

  那么这个时候在如果在完成轮询,并且中断打开以后,会马上有一个 NIC 中断产生,从而触发一次 POLL 事件,这种在中断关断时刻到达的包我们称 "rotting"

  这样就会在 POLL 机制和 NIC 中断之间产生一个竞争,解决的方法就是利用网卡的接收状态位,继续接收环形队列缓冲 rx-ring 中的数据,直到没有数据接收以后,才使能中断。

 

  锁定和防冲突机制:

  - 1.SMP 的保证机制:保证同时只有一个处理器调用网络设备的 POLL 方法,

  因为我们将在下面看到同时只有一个处理器可以对调用 netif_rx_schedule 挂在 POLL 队列中的 NIC 设备调用 POLL 方法。

  - 2. 网络核心层( net core )调用设备驱动程序使用循环方式发送数据包,在设备驱动层接收数据包的时候完全无锁的接收,

  而网络核心层则同样要保证每次只有一个处理器可以使用软中断处理接收队列。

  - 3. 在多个处理器对 NIC rx-ring 访问的时刻只能发生在对循环队列调用关闭( close )和挂起( suspend )方法的时候

  (在这个时刻会试图清除接收循环队列)

  - 4. 数据同步的问题(对于接收循环队列来说),驱动程序是不需要考虑的网络层上的程序已经把这些事情做完了。

  - 5. 如果没有把全部的部分交给 POLL 方法处理,那么 NIC 中断仍然需要使能,

  接收链路状态发生变化和发送完成中断仍然和以前的处理步骤一样,这样处理的假设是接收中断是设备负载最大的的情况,

  当然并不能说这样一定正确。

 

下面的部分将详细介绍在接收事件中调用设备的 POLL 方法。

NAPI 提供的重要函数和数据结构和函数:

核心数据结构:

struct softnet_data 结构内的字段就是 NIC 和网络层之间处理队列 , 这个结构是全局的,

它从 NIC 中断和 POLL 方法之间传递数据信息。其中包含的字段有:

struct softnet_data

{

  int  throttle;   /* 1 表示当前队列的数据包被禁止 */

  int  cng_level;   /* 表示当前处理器的数据包处理拥塞程度 */

  int  avg_blog;   /* 某个处理器的平均拥塞度 */

  struct sk_buff_head input_pkt_queue; /* 接收缓冲区的 sk_buff 队列 */

  struct list_head poll_list;   /*POLL 设备队列头 */

  struct net_device output_queue;  /* 网络设备发送队列的队列头 */

  struct sk_buff  completion_queue; /* 完成发送的数据包等待释放的队列 */

  struct net_device backlog_dev;  /* 表示当前参与 POLL 处理的网络设备 */

};

核心 API:

1 netif_rx_schedule(dev)

    这个函数被中断服务程序调用,将设备的 POLL 方法添加到网络层次的 POLL 处理队列中去,

    排队并且准备接收数据包,在使用之前需要调用 netif_rx_reschedule_prep ,并且返回的数为 1

    并且触发一个 NET_RX_SOFTIRQ 的软中断通知网络层接收数据包。

2. netif_rx_schedule_prep(dev)

    确定设备处于运行,而且设备还没有被添加到网络层的 POLL 处理队列中,在调用 netif_rx_schedule 之前会调用这个函数。

3 netif_rx_complete(dev)

    把当前指定的设备从 POLL 队列中清除,通常被设备的 POLL 方法调用,

    注意如果在 POLL 队列处于工作状态的时候是不能把指定设备清除的,否则将会出错。

    POLL 方法的本质意义上来说就在于尽量减少中断的数目,特别在于大量的小长度的资料包的时候,

    减少中断,以达到不要让整个操作系统花费太多的时间在中断现场的保护和恢复上,

    以便把赢得的时间用来在我网络层上的处理数据的传输,例如在下面介绍的 8139CP 中断的处理过程中,

    目的就在于尽快把产生中断的设备挂在 poll_list, 并且关闭接收中断,最后直接调用设备的 POLL 方法来处理数据包的接收 ,

    直到收到数据包收无可收,或者是达到一个时间片内的调度完成。

 

RTL8139C 的数据接收环形缓冲队列:

RTL8139C 的接收方式是一种全新的缓冲方式,能显着的降低 CPU 接收数据造成的花费,

适合大型的服务器使用,适合 IP, TCP, UDP 等多种方式的数据下载,以及连接 IEEE802.1P, 802.1Q, VLAN 等网络形式;

8139CP 中分别有 64 个连续的接收 / 发送描述符单元,

对应三个不同的环形缓冲队列 --

一个是高优先级传输描述符队列,

一个是普通优先级传输符描述队列,

一个是接收符描述队列,

每个环形缓冲队列右 64 4 个双字的连续描述符组成,

每个描述符有 4 个连续的双字组成,每个描述符的开始地址在 256 个字节的位置对齐,

接收数据之前,软件需要预先分配一个 DMA 缓冲区,一般对于传输而言,

缓冲区最大为 8Kbyte 并且把物理地址链接在描述符的 DMA 地址描述单元,

另外还有两个双字的单元表示对应的 DMA 缓冲区的接收状态。

 

/driver/net/8139CP.C 中对于环形缓冲队列描述符的数据单元如下表示:

struct cp_desc {

  u32 opts1;/* 缓冲区状态控制符,包含缓冲区大小,缓冲区传输为动位 */

  u32 opts2;/* 专门用于 VLAN 部分 */

  u64 addr; /* 缓冲区的 DMA 地址 */ };

 

EX:BcmHalMapInterrupt(bcm63xx_enet_isr, (unsigned int)pDevCtrl, pDevCtrl->rxIrq);

static irqreturn_t bcm63xx_enet_isr(int irq, void * dev_id)

{

    BcmEnet_devctrl *pDevCtrl = dev_id;

 

    pDevCtrl->rxDma->intStat = DMA_DONE;  // clr interrupt

    netif_rx_schedule(pDevCtrl->dev);

 

    return IRQ_HANDLED;

}

 

8139CP NIC 中断:

static irqreturn_t

cp_interrupt (int irq, void *dev_instance, struct pt_regs *regs)

{

  struct net_device *dev = dev_instance;

  struct cp_private *cp = dev->priv;

  u16 status;

/* 检查 rx-ring 中是否有中断到达 */

  status = cpr16(IntrStatus);

  if (!status || (status == 0xFFFF))

  return IRQ_NONE;

 

  if (netif_msg_intr(cp))

  printk(KERN_DEBUG "%s: intr, status %04x cmd %02x cpcmd %04x/n",

          dev->name, status, cpr8(Cmd), cpr16(CpCmd));

/* 清除 NIC 中断控制器的内容 */

  cpw16(IntrStatus, status & ~cp_rx_intr_mask);

 

  spin_lock(&cp->lock);

/* 接收状态寄存器表示有资料包到达 */

  if (status & (RxOK | RxErr | RxEmpty | RxFIFOOvr)) {

/* 把当前的产生中断的 NIC 设备挂在 softnet_data 中的 POLL 队列上,等待网络上层上的应用程序处理 */

  if (netif_rx_schedule_prep(dev)) {

/* 关闭接收中断功能 */

   cpw16_f(IntrMask, cp_norx_intr_mask);

   __netif_rx_schedule(dev);

  }

  }

/* 发送中断的处理过程以及 8139C 的专门软中断的处理过程,这里我们不关心 */

  if (status & (TxOK | TxErr | TxEmpty | SWInt))

  cp_tx(cp);

/* 如果发生链路变化的情况,需要检查介质无关接口 (MII) 的载波状态同样也发生变化,否则就要准备重新启动 MII 接口 */

  if (status & LinkChg)

  mii_check_media(&cp->mii_if, netif_msg_link(cp), FALSE);

/* 如果 PCI 总线发生错误,需要对 8139C 的设备重新复位 */

  if (status & PciErr) {

  u16 pci_status;

 

  pci_read_config_word(cp->pdev, PCI_STATUS, &pci_status);

  pci_write_config_word(cp->pdev, PCI_STATUS, pci_status);

  printk(KERN_ERR "%s: PCI bus error, status=%04x, PCI status=%04x/n",

         dev->name, status, pci_status);

 

  /* TODO: reset hardware */

  }

 

  spin_unlock(&cp->lock);

  return IRQ_HANDLED;

}

NIC 挂在 POLL 队列 (poll_list) 上在 8139CP 的中断程序可以看到 __netif_rx_schedule 的调用方式,

它把 NIC 设备挂在 softnet_data 结构中的 poll_list 队列上,以便及时的返回中断,

让专门数据包处理 bottom-half 部分来进行处理,我们先来看一下 __netif_rx_schedule 的内部工作流程。

static inline void __netif_rx_schedule(struct net_device *dev)

{

  unsigned long flags;

 

  local_irq_save(flags);

  dev_hold(dev);

/* 把当前 NIC 设备挂在 POLL(poll_list) 队列中,等待唤醒软中断以后进行轮询 */

  list_add_tail(&dev->poll_list, &__get_cpu_var(softnet_data).poll_list);

/* 确定当前该设备所要准备接收的包大小 */

  if (dev->quota < 0)

  dev->quota += dev->weight;

  else

  dev->quota = dev->weight;

/* 为动软中断,在表示所有中断的状态字 irq_cpustat_t 中关于软中断字段 __softirq_pending 中,

    把关于网络轮循接收软中断位置 1, 等待调度时机来临时候运行该中断的句柄 net_rx_action. */

  __raise_softirq_irqoff(NET_RX_SOFTIRQ);

  local_irq_restore(flags);

}

 

__netif_rx_schedule 启动的软中断的处理过程分析软中断事件触发前已经在此设备子系统初始化时刻调用

subsys_initcall(net_dev_init) 在软中断控制台上被启动,挂在任务队列 tasklet 上准备在任务调度 schedule 的时刻运行它了,

这个里面最主要的部分是调用了 8139C 网络设备的 POLL 方法( dev->poll ),从网络设备的 rx-ring 队列中获得数据,

本来它应当放在网络设备中断服务程序中执行的,按照我们前面解释的那样,

POLL 方法以空间换取时间的机制把它放在软中断部分来执行轮循机制

(采用类似老的 Bottom-half 机制也可以达到同样效果,而且更加容易理解一些)

在每次进行进程调度的时候就会执行网络设备软中断,轮询 rx-ring NIC 进行数据的接收。

软中断的处理过程:

static void net_rx_action(struct softirq_action *h)

{

  struct softnet_data *queue = &__get_cpu_var(softnet_data);

  unsigned long start_time = jiffies;

  int budget = netdev_max_backlog;/* 表示队列的最大长度 */

/* 锁定当前线程,多处理器的情况之下不能被其它处理器中断处理 */

  preempt_disable();

  local_irq_disable();

/* 检查 POLL 队列 (poll_list) 上是否有设备在准备等待轮询取得资料 */

  while (!list_empty(&queue->poll_list)) {

  struct net_device *dev;

/* 这里保证执行当前的 POLL 过程的时间不超过一个时间片,这样不至于被软中断占用太多的时间,

这样在一次调度的时间内执行完毕当前的 POLL 过程, budget 表示一个时间片内最大数据传输的 " 块数 "

块的意思为每个 POLL 所完成 sk_buff 数量,每块中间的 sk_buff 数量 dev->quota 决定,在 8139CP 驱动中,

budget == 300, quota == 16 表示每给时间片最多可以接收到 4.8K sk_buff 数量 */

  if (budget <= 0 || jiffies - start_time > 1)

   goto softnet_break;

 

  local_irq_enable();

/* 从公共的 softnet_data 数据结构中的轮循队列上获得等待轮循的设备结构 */

  dev = list_entry(queue->poll_list.next,

      struct net_device, poll_list);

/* 调用设备的 POLL 方法从 NIC 上的 Ring Buffer 中读入数据 */

  if (dev->quota <= 0 || dev->poll(dev, &budget)) {

/* 完成一次 POLL 过程的数据的接收,重新定义设备接收数据的 " 配额 "

(事实上就是 sk_buff 缓冲区的数量,每次调用 POLL 方法的时候可以创建并且最多可以向上层提交的 sk_buff 缓冲区数目 ,

这个参数很重要在高速处理的时候有需要慎重优化这个数值,在有大量数据接收的情况下,需要增加该数值) */

   local_irq_disable();

   list_del(&dev->poll_list);

   list_add_tail(&dev->poll_list, &queue-

>poll_list);

   if (dev->quota < 0)

    dev->quota += dev->weight;

   else

    dev->quota = dev->weight;

  } else {

/* 发生了错误的数据接收状况,或者没有完成 " 规定 " 配额的数据接收,并且没有新的数据进来,

这个也可能表示已经完成了传输的过程,调用 __netif_rx_complete 把网络设备从 POLL 队列上清除

(介绍 POLL 过程的时候详细介绍) */

   dev_put(dev);

   local_irq_disable();

  }

  }

out:

  local_irq_enable();

  preempt_enable();

  return;

 

softnet_break:

  __get_cpu_var(netdev_rx_stat).time_squeeze++;

  __raise_softirq_irqoff(NET_RX_SOFTIRQ);

  goto out;

}

8139CP 驱动中的轮询方法

dev->poll 方法:

这个方法通常被网络层在向驱动的接收循环队列获取新的数据包时刻调用,

而驱动的接收循环队列中可以向网络层交付的包数量则在 dev->quota 字段中表示,我们来看 8139cp POLL 的原型:

static int cp_rx_poll (struct net_device *dev, int *budget)

参数 budget 的上层任务所需要底层传递的数据包的数量,这个数值不能超过 netdev_max_backlog 的值。

总而言之, POLL 方法被网络层调用,只负责按照网络层的要求值( " 预算 " 值)提交对应数量的数据包。

8139CP POLL 方法注册通常在设备驱动程序模块初始化 ( 调用 probe) 的时候进行 , 如下:

static int cp_init_one (struct pci_dev *pdev, const struct pci_device_id *ent)

{

dev->poll = cp_rx_poll;

}

设备的 POLL 方法正如前所说的是被网络层上的软中断 net_rx_action 调用,我们现在来看具体的流程:

static int cp_rx_poll (struct net_device *dev, int *budget)

{

  struct cp_private *cp = netdev_priv(dev);

  unsigned rx_tail = cp->rx_tail;

  /* 设定每次进行调度的时候从设备发送到网络层次最大的数据包的大小 */

unsigned rx_work = dev->quota;

  unsigned rx;

 

rx_status_loop:

  rx = 0;

/* 重新打开 NIC 中断,在 cp_interrupt 中断句柄中中断关闭了,现在 POLl 已经开始处理环行缓冲队列中的数据,所以中断可以打开,准备接收新的数据包 */

  cpw16(IntrStatus, cp_rx_intr_mask);

 

  while (1) {/*POLL 循环的开始 */

  u32 status, len;

  dma_addr_t mapping;

  struct sk_buff *skb, *new_skb;

  struct cp_desc *desc;

  unsigned buflen;

/* 从下标 rx_tail 的内存中的环行缓冲队列接收队列 rx_skb " 摘下 " 套接字缓冲区 */

  skb = cp->rx_skb[rx_tail].skb;

  if (!skb)

   BUG();

 

  desc = &cp->rx_ring[rx_tail];

/* 检查在 NIC 的环形队列( rx_ring )上的最后的数据接收状态,是否有出现接收或者 FIFO 的错误 */

  status = le32_to_cpu(desc->opts1);

  if (status & DescOwn)

   break;

 

  len = (status & 0x1fff) - 4;

  mapping = cp->rx_skb[rx_tail].mapping;

 

  if ((status & (FirstFrag | LastFrag)) != (FirstFrag | LastFrag)) {

   /* we don''''t support incoming fragmented frames.

    * instead, we attempt to ensure that the

    * pre-allocated RX skbs are properly sized such

    * that RX fragments are never encountered

    */

   cp_rx_err_acct(cp, rx_tail, status, len);

   cp->net_stats.rx_dropped++;

   cp->cp_stats.rx_frags++;

   goto rx_next;

  }

 

  if (status & (RxError | RxErrFIFO)) {

   cp_rx_err_acct(cp, rx_tail, status, len);

   goto rx_next;

  }

 

  if (netif_msg_rx_status(cp))

   printk(KERN_DEBUG "%s: rx slot %d status 0x%x len %d/n",

          cp->dev->name, rx_tail, status, len);

 

  buflen = cp->rx_buf_sz + RX_OFFSET;

/* 创建新的套接字缓冲区 */

  new_skb = dev_alloc_skb (buflen);

  if (!new_skb) {

   cp->net_stats.rx_dropped++;

   goto rx_next;

  }

 

  skb_reserve(new_skb, RX_OFFSET);

  new_skb->dev = cp->dev;

/* 解除原先映像的环行队列上的映像区域 */

  pci_unmap_single(cp->pdev, mapping,

     buflen, PCI_DMA_FROMDEVICE);

/* 检查套接字缓冲区 (sk_buff) 上得到的数据校验和是否正确 */

  /* Handle checksum offloading for incoming packets. */

  if (cp_rx_csum_ok(status))

   skb->ip_summed = CHECKSUM_UNNECESSARY;

  else

   skb->ip_summed = CHECKSUM_NONE;

/* 按照数据的实际大小重新定义套接字缓冲区的大小 */

  skb_put(skb, len);

 

  mapping =

  cp->rx_skb[rx_tail].mapping =

/* DMA 映射在前面新创建的套接字缓冲区虚拟地址 new_buf->tail 到实际的物理地址上,并且把这个物理地址绑在接收缓冲区的队列中 */

   pci_map_single(cp->pdev, new_skb->tail, buflen, PCI_DMA_FROMDEVICE);

/* 把新建立的缓冲区的虚拟地址绑在接收缓冲区的队列中,在下一次访问 rx_skb 数组的这个结构时候, POLL 方法会从这个虚拟地址读出接收到的数据包 */

  cp->rx_skb[rx_tail].skb = new_skb;

/* cp_rx_skb 调用 netif_rx_skb , 填充接收数据包队列,等待网络层在 Bottom half 队列中调用 ip_rcv 接收网络数据,这个函数替代了以前使用的 netif_rx */

  cp_rx_skb(cp, skb, desc);

  rx++;

 

rx_next:

/* 把前面映像的物理地址挂在 NIC 设备的环行队列上(也就是 rx_ring 上,它是在和 NIC 中物理存储区进行了 DMA 映射的,

而不是驱动在内存中动态建立的),准备提交给下层 (NIC) 进行数据传输 */

  cp->rx_ring[rx_tail].opts2 = 0;

  cp->rx_ring[rx_tail].addr = cpu_to_le64(mapping);

/* 在相应的传输寄存器中写入控制字,把 rx_ring 的控制权从驱动程序交还给 NIC 硬件 */

  if (rx_tail == (CP_RX_RING_SIZE - 1))

   desc->opts1 = cpu_to_le32(DescOwn | RingEnd | cp->rx_buf_sz);

  else

   desc->opts1 = cpu_to_le32(DescOwn | cp->rx_buf_sz);

/* 步进到下一个接收缓冲队列的下一个单元 */

  rx_tail = NEXT_RX(rx_tail);

 

  if (!rx_work--)

   break;

  }

 

  cp->rx_tail = rx_tail;

/* 递减配额值 quota ,一旦 quota 递减到 0 表示这次的 POLL 传输已经完成了使命,就等待有数据到来的时候再次唤醒软中断执行 POLL 方法 */

  dev->quota -= rx;

  *budget -= rx;

 

  /* if we did not reach work limit, then we''''re done with

  * this round of polling

  */

  if (rx_work) {

/* 如果仍然有数据达到,那 ? 返回 POLL 方法循环的开始,继续接收数据 */

  if (cpr16(IntrStatus) & cp_rx_intr_mask)

   goto rx_status_loop;

/* 这里表示资料已经接收完毕,而且没有新的接收中断 ? 生了,这个时候使能 NIC 的接收中断,

并且调用 __netif_rx_complete 把已经完成 POLL 的设备从 poll_list 上摘除,等待下一次中断产生的时候,

再次把设备挂上 poll_list 队列中。 */

  local_irq_disable();

  cpw16_f(IntrMask, cp_intr_mask);

  __netif_rx_complete(dev);

  local_irq_enable();

 

  return 0; /* done */

  }

 

  return 1;  /* not done */

}

其它的使用 NAPI 的驱动程序和 8139CP 大同小异,只是使用了网络层专门提供

POLL 方法 --proecess_backlog /net/dev.c , NIC 中断接收到了数据包后,

调用网络层上的 netif_rx /net/dev.c )将硬件中断中接收到数据帧存入 sk_buff

, 然后检查硬件帧头 , 识别帧类型 , 放入接收队列( softnet_data 结构中的

input_pkt_queue 队列上) , 启动接收软中断作进一步处理 . 软中断函数

(net_rx_action) 提取接收包,而 process_backlog( 也就是 POLL 方法 ) 向上层提交数据。

能让接收速度更快一些吗?

我们现在来思考一下如何提高 NAPI 效率的问题,在说到效率这个问题之前我

们先看一下在 linux 的文檔中 NAPI_HOWTO.txt 中提供一个模型用来构造自己

NIC POLL 方法,不过和 8139 有一些不一样,其中 NIC 设备描述中有一个

dirty_rx 字段是在 8139CP 中没有使用到的。

dirty_rx 就是已经开辟了 sk_buff 缓冲区指标和已经提交到 NIC rx_ring 参与

接收的缓冲,但是还没有完成传输的缓冲区和已经完成传输的缓冲区的数量总

和,与之相类似的是 cur_rx 这个表示的是下一个参与传输的缓冲区指标,我们

NAPI_HOWTO.txt 的举例中可以看到这个字段的一些具体使用方式:

/* cur_rx 下一个需要参与传输的缓冲区指标,

    如果 cur_rx 指标大于 dirty_rx 那表示已经有在 rx-ring 中开辟的 rx-ring 中的每个传输缓冲已经被耗尽了,

    这个时候需要调用 refill_rx_ring 把一些已经向网络层提交了数据的 rx-ring 接收单元开辟新的缓冲区,

    增加 dirty_rx 的数值,下一次资料接收做准备, */

        if (tp->cur_rx - tp->dirty_rx > RX_RING_SIZE/2 ||

            tp->rx_buffers[tp->dirty_rx % RX_RING_SIZE].skb == NULL)

                refill_rx_ring(dev);

 

/* 如果已经当前的 cur_rx dirty_rx 之间相差不超过总的 rx_ring 接收单元的一半,

而且剩下的一半中间有空的传输单元,那我们不必担心了,因还有足够的缓冲区可以使用,

就可以退出当前的程序,等待下一次软中断调用 POLL 来处理在这之间收到的数据,

NAPI_HOWTO.txt 中是重新动时钟计数,这样做是在没有使能 NIC 中断处理的情况下) */

         if (tp->rx_buffers[tp->dirty_rx % RX_RING_SIZE].skb == NULL)

                restart_timer();

/* 如果执行到这里了,那表示有几种情况可能发生,第一当前的 cur_rx dirty_rx 之间相

差不超过总的 rx_ring 接收单元的一半,调用 refill_rx_ring dirty_rx 并未增加,

(也许在 rx-ring 中大量的单元收到数据没有得到网络层函数的处理),

结果 dirty_rx 没有增加,而且也没有空闲的单元来接收新到的数据,这样就要重新调用 netif_rx_schedule 来唤醒软中断,

调用设备的 POLL 方法,采集在 rx-ring 的数据。 */

  else netif_rx_schedule(dev);  /* we are back on the poll list

*/

RTL-8169 的驱动程序中就使用了 dirty_rx 这个字段,但是在 8139CP 中并

未使用,其实这个并非 8139CP 驱动不成熟的表现,大家阅读

NAPI_HOWTO.txt 中可以知道,现在 8139CP 中并未严格按照 NAPI 所提出的

要求去做,如果大家有兴趣的话,可以比较一下 8139CP RTL-8169 这两个

驱动之间的不同,大家会发现虽然两者都没有在 NIC 中断处理中去完成数据从

驱动层向网络层上的转发,而都放在了软中断之中完成,但是在 8139 中利用

了自己的一些独特的硬件特性,使 NIC 在利用关断中断接收数据的同时利用资

料包到达位( RxOK )通知到达事件,然后采用 POLL 方法把数据从 NIC

接转发到上层;而 RTL8169 还需要借助 softnet_data 结构中的

input_pkt_queue (套接字缓冲( sk_buff )输入队列)来完成从 NIC 中断到软

中断之间的 sk_buff 数据调度;这样对于 8139CP 来说最大的好处就是不要了

dirty_rx 字段和 cur_rx 字段来让 POLL 方法以及 NIC 中断知道当前的传输单元

的状况,还包括不需要不时定期的调用 refill_rx_ring 来刷新 rx-ring 获得空闲的

传输单元;说到坏处我想就是重写了一个 POLL 方法,完全不能借用

/net/core/dev.c 中的 process_backlog 来作 ? 自己的 POLL 方法,不过这个代

价值得。

说了这 ? 多,似乎都和提高效率没有关系,其实正好相反,通过这些了解我们

softnet_data 中的一些字段的意思应该更加清晰了,下面所 ? 述的,提高效

率的方法就是在 8139CP 的基础上借用了 NAPI_ HOWTO.txt 中的一些方法,

从实际上的使用效果来看,在某些应用场合之下比 Linux 8139CP 的确是有

了一定的提高,我们首先看看在 Linux2.6.6 的内核使用 8139CP x86(PIII-

900Mhz) 平台上的数据包接收处理情况:比较表如下:

Psize    Ipps       Tput     Rxint            Done

----------------------------------------------------

60     490000      254560      21              10

128     358750      259946      27              11

256     334454      450034      34              18

512     234550      556670      201239          193455

1024    119061      995645      884526          882300

1440     74568      995645      995645          987154

上表中表示:

"Pszie" 表示包的大小  

"Ipps" 每秒钟系统可以接收的包数量  

"Tput" 每次 POLL 超过 1M 个资料包的总量  

"Rxint" 接收中断数量  

"Done" 加载 rx-ring 内数据所需要的 POLL 次数,这个数值也表示了我们需要清除 rx-ring 的次数。

从上表可以看出, 8139CP 中当接收速率达到 490K packets/s 的时候仅仅只有

21 个中断 ? 生,只需要 10 POLL 就可以完成资料从 rx_ring 的接收,然而对

于大数据包低速率的情况,接收中断就会急剧增加,直到最后每个数据包都需

要一次 POLL 的方法来进行处理,最后的结果就是每个中断都需要一次 POLL

的方法,最后造成效率的急剧下降,以至于系统的效率就会大大降低,所以

NAPI 适用于大量的数据包而且尽可能是小的数据包,但是对于大的数据包,

而且低速率的,反而会造成系统速度的下降。

如果要改善这种情况,我们可以考虑采用以下的方法,我们在 MIPS Xsacle

SA1100 平台上进行一系列的测试取得了良好的效果:

1 完全取消 NIC 中断,使用 RXOK 位置控制接收中断,

2 采用定时器中断 timer_list 的控制句柄,根据硬件平台设定一个合适的间

隔周期(间隔周期依据平台不同而异),对 rx-ring 直接进行 POLL 轮询,我们

MIPS Xscale 上直接使用了中断向量 0--irq0 ? rx-ring 进行轮询的

top-half (注意我们在上述两个平台上选择的 HZ 数值是 1000, 而通常这个数值

100 ,并且重新编写了 Wall-time 的记数程序,让 Wall-Time 的记数还是以

10MS ? 间隔),当然也可以根据自己的平台和应用程序的状况选择合适的定

时时间。

3 借助 softnet_data 中的 input_pkt_queue 队列,在时钟中断 bottom-half

完成 POLL 方法之后,并不直接把数据传输到网络层进行处理,而是把 sk_buff

挂在 input_pkt_queue 队列上,唤醒软中断在过后处理,当然可以想象,这样

需要付出一定的内存代价,而且实时性能也要差一些。

4 使用 dirty_rx 字段和 refill_rx_ring 函数,在调用完 POLL 方法以后,而且网

路层程序比较空闲的时候 ? 一些 rx-ring 中的单元建立新缓冲挂在环形缓冲队列

上,这样可以在新的数据包达到的时候节省时间,操作系统不必要手忙脚乱地

开辟新的空间去应付新来的数据。

5 最后请注意:我们上层的应用程序是以网络数据转发 ? 主的,并没有在应

用层面上有很多后台进程的复杂的应用,上述的 1 4 点中所做的显而易见是

以牺牲系统效率整体效率而单独改善了网络数据的处理。

 

我们再来看改善的 8139CP 驱动程序使用 8139CP x86(PIII-900Mhz) 平台上的接收情况:

 

Psize    Ipps       Tput     Rxint       Done

----------------------------------------------------

60     553500    354560   17          7

128     453000    350400   19         10

256     390050     324500   28          13

512     305600    456670   203        455

1024     123440    340020   772951     123005

1440     64568     344567   822394     130000

 

从上图来看,数据传输的效率和波动性有很明显的改善,

在高速率和低速率的时候所需要的 POLL 次数的差距以前的 8139CP 驱动程序那么显着了,

这样的而且最大的包接收能力也提高到了 553K/s

我们在 MIPS 系列以及 Xscale 系列平台上最大的包接收能力可以提高大约 15%-25%

 

最后使用 NAPI 并不是改善网络效率的唯一途径,只能算是权益之策,根本的解决途径还是在于上层应用程序能够独占网络设备,

或者是提供大量的缓冲资源,如果这样,根据我们的实验资料表明可以提高 100%-150% 以上的接收效率。

你可能感兴趣的:(NAPI的一些知识)