NAPI 是 Linux 上采用的一种提高网络处理效率的技术,它的核心概念就是不采用中断的方式读取数据,而代之以 POLL 的方法来轮询数据,类似于底半方式(bottom-half 的处理模式);但是目前在 Linux 的 NAPI 工作效率比较差,本文在分析 NAPI 的同时,提供了一种高效的改善方式供大家参考。
NAPI 是 Linux 上采用的一种提高网络处理效率的技术,它的核心概念就是不采用中断的方式读取数据,而代之以首先采用中断唤醒数据接收的服务程序,然后 POLL 的方法来轮询数据,(类似于底半(bottom-half)处理模式);从我们在实验中所得到的数据来看,在随着网络的接收速度的增加,NIC 触发的中断能做到不断减少,目前 NAPI 技术已经在网卡驱动层和网络层得到了广泛的应用,驱动层次上已经有 E1000 系列网卡,RTL8139 系列网卡,3c50X 系列等主流的网络适配器都采用了这个技术,而在网络层次上,NAPI 技术已经完全被应用到了著名的 netif_rx 函数中间,并且提供了专门的 POLL 方法--process_backlog 来处理轮询的方法;根据实验数据表明采用NAPI技术可以大大改善短长度数据包接收的效率,减少中断触发的时间;由于 RTL8139CP 是一种应用比较广泛的网络适配器,所以本文以其为例,说明了NAPI技术在网络适配器上的应用和基本原理。
但是 NAPI 存在一些比较严重的缺陷:而对于上层的应用程序而言,系统不能在每个数据包接收到的时候都可以及时地去处理它,而且随着传输速度增加,累计的数据包将会耗费大量的内存,经过实验表明在 Linux 平台上这个问题会比在 FreeBSD 上要严重一些;另外采用 NAPI 所造成的另外一个问题是对于大的数据包处理比较困难,原因是大的数据包传送到网络层上的时候耗费的时间比短数据包长很多(即使是采用 DMA 方式),所以正如前面所说的那样,NAPI 技术适用于对高速率的短长度数据包的处理,在本文的末尾提出了 NAPI 的改善方法,和实验数据。
|
驱动可以继续使用老的 2.4 内核的网络驱动程序接口,NAPI 的加入并不会导致向前兼容性的丧失,但是 NAPI 的使用至少要得到下面的保证:
A. 要使用 DMA 的环形输入队列(也就是 ring_dma,这个在 2.4 驱动中关于 Ethernet 的部分有详细的介绍),或者是有足够的内存空间缓存驱动获得的包。
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 接收事件寄存器响应机制:
- COR 机制:当用户程序读状态/事件寄存器,读完成的时候寄存器和NIC的rx-ring中表示的状态队列将被清零,natsemi 和 sunbmac 的 NIC 会这样做,在这种情况下,必须把 NIC 所有以前的中断响应的处理部分都移动到 POLL 方法中去。
- 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 方法。
|
struct softnet_data 结构内的字段就是 NIC 和网络层之间处理队列,这个结构是全局的,它从 NIC中断和 POLL 方法之间传递数据信息。其中包含的字段有:
struct softnet_data |
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+ 的接收方式是一种全新的缓冲方式,能显著的降低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地址*/ };
static irqreturn_t |
在 8139CP 的中断程序可以看到 __netif_rx_schedule 的调用方式,它把 NIC 设备挂在softnet_data 结构中的 poll_list 队列上,以便及时的返回中断,让专门数据包处理 bottom-half部分来进行处理,我们先来看一下 __netif_rx_schedule 的内部工作流程。
static inline void __netif_rx_schedule(struct net_device *dev) |
由 __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) |
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) |
设备的 POLL 方法正如前所说的是被网络层上的软中断 net_rx_action 调用,我们现在来看具体的流程:
static int cp_rx_poll (struct net_device *dev, int *budget) |
其他的使用 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为下一个需要参与传输的缓冲区指针, |
在 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 |
上表中表示:
"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 |
从上图来看,数据传输的效率和波动性有很明显的改善,在高速率和低速率的时候所需要的POLL 次数的差距以前的 8139CP 驱动程序那么显著了,这样的而且最大的包接收能力也提高到了 553K/s,我们在 MIPS 系列以及 Xscale 系列平台上最大的包接收能力可以提高大约 15%-25%。
最后使用 NAPI 并不是改善网络效率的唯一途径,只能算是权益之策,根本的解决途径还是在于上层应用程序能够独占网络设备,或者是提供大量的缓冲资源,如果这样,根据我们的实验数据表明可以提高 100%-150% 以上的接收效率。