4.3.2 内存拷贝
正如第3节所解释的那样,数据包在到达用户程序前要经历两次器拷贝(图1):第一次,先从NIC缓冲区转移到核心缓存区(图2)。第二次则将它传输到用户应用程序缓存。图5展示了在测试平台的电脑上两次数据拷贝的时钟开销,及对应的数据包的大小。
根据NDIS规范,由函数NdisTransferData()实施并完成第一次传输。该函数的开销特别高,主要有两个原因。
1 .在拷贝过程前需要有一些附加的开销。考虑到NIC驱动程序在退出包驱动器控制权以后,整个数据包可能失效,故根据DDK文档[7],驱动器必须使用该功能函数。这样,该功能函数首先检查整个数据包是否已被NIC传输到内存;如果还没有,则等待直到传输完成。
2 .该功能函数所操作的数据对象并不在CPU缓存中。数据包通过控制总线从NIC板载存储器传输到主存(参见3.1)。
先前两点解释了图5所示的第一次拷贝高开销的原因。在处理过程中,存在有一个开销量底限基数的客观事实,它独立于处理数据量的大小(这就是为什么在数据包较小的情况下,处理每字节需要更高的代价)。而当数据包较大时,这些开销平摊于每个字节,故每字节开销量要小一些。
第二次拷贝使用的是标准C库函数(例如memcpy())。由于待拷贝到用户区的数据可能大都不在于CPU的cache中,故处理每字节的平均代价随着数据包的增大而增大,原因就是命中率下降了。另外处理每字节数据的开销量还与核心层存储区的大小有关。例如,如果核心存储区容量较小,那么从NIC存储区拷贝数据时,会出现有部分数据仍保留在CPU cache中。
总结,第一次拷贝每个包的时钟周期开销在540到10500之间,第二次拷贝在259到8550之间变化。实际上,考虑到每个包在存入核心存储区前都要插入一个含有20个字节的帧头(包含时间戳、包长度等其它信息),所以第二次拷贝的总开销在364到8664之间。
4.3.3 应用程序
所有应用程序与包驱动程序之间的交互作用都是通过系统调用来完成。Windows提供了 ReadFile()、WriteFile(),还有DeviceIO-Control()等系统调用来完成I/O操作。所有这些系统调用都要完成两部分内容"context switches"(在这里使用context switch这个词其实并不太准确。实际上从用户层到核心层这一过程是个优先特权转闸,而非上下文执行转闸):第一,将指令从用户层(应用程序)发送到核心层(驱动程序)。第二,返回用户应用程序的控制权。
众所周知,上下文转闸是相当复杂(它通常包含中断的产生和一些数据结构的初始化工作)并且开销很大的过程。如在我们的测试平台上,类似read()这样的系统调用需要33500个时钟周期。如此高的开销!若每次系统调用只拷贝单个数据包,那可想效率多低了。因此包捕获器在每次应用程序系统调用时传输整块的数据包。每次系统调用所传输的数据包数量的多少取决于核心层存储区的大小,并与CPU负载能力成正比。另外,它还取决于用户层应用程序的复杂性(如果应用程序对数据包的处理时间很长,那它从核心存储区重载数据的时间间隔就大些,那么核心缓冲区的数据就没能及时“排干”),而另一方面还受包捕获驱动器的影响(驱动器内的代码拥有较高的执行优先级,因此核心缓冲区常处于满载状态)。
由于读操作的频率,以及每次系统调用所处理的数据包的数量都是随机数(变化无常),故无法对每个包处理所需的时钟开销作出明确描述。在一台过载的机器上(即CPU的使用率达到100%),假定它接收到的是最小帧,同时捕获驱动器在每次系统调用时传输256k个字节(该值为目前WinPcap上可选择的界值。),这相当于3200个数据包(包括由驱动程序后来附增加的20个字节的帧头)。在这种情况下,处理每个包的上下文转闸(context switch)近似代价为10个时钟周期,但这相比于我们接下来要讲的其它组件的开销,这甚至可以被忽略。
4.3.4 其它处理部件
尽管我们一般都下意识地认为:在数据包过滤和传输拷贝过程中,捕获驱动器需耗费的执行时间是最多的(这也正是为什么几乎所有文献都将注意力集中于它以达到增进性能的效果)。但通过我们的测试,揭示了其它一些关系到包捕获代价的关键因素。在它们中间,又以timestamp最为显著。
NPF驱动程序通过KeQueryPerformanceCounter()win32功能函数获得数据包的时间戳,它是唯一能提供微秒级精度的核心函数。该函数与系统晶振相关联,故其开销十分巨大:在我们的测试平台上,大约需要1800个时钟周期(通过对若干单机器进行测试,得到该值)。该函数要耗费若干个微秒才能返回一个带有微秒级精确定时的结果,这不免有些荒谬,但事实正是如此。更有甚是,它必须持续运作以便在数据包到达时为之加上准确的时间戳。
额外的开销还包括与NDIS、与核心层的交互运作(交互运作大都需要使用系统调用函数,因此开销巨大)。另外还包括管理(内存映射与解映)核心层缓冲区、在NPF中向数据包增创帧。总之,除去过滤和拷贝的开销,其它还有近830个时钟周期的开销。
4.4 全部开销
图6以园盘切片形式形象展示了各相关组件的时钟开销。处理一个包的全部的费用是 5680 个钟周期。该测试结果生成的相关配置如下所示:流速为148Kfps,数据包大小为64个字节,适配器为3Com 3C996 Gigabit,过滤处理虚指令条数为21条。
显然,从图6可以看出,当数据包较小时,时间戳和NIC驱动的开销最大。又由于这两部分的开销大都取决于硬件,故进行软件优化意义不大。一些针对NIC驱动进行的小规模优化程序由于其开发商拒绝公开代码,使得它们得不到广泛的应用。然而在任何情况下,NIC驱动软件优化的效果远不如升级NIC卡上的芯片组。
最值得注意的是,图6说明了为什么大多数文献都将优化集中于拷贝和过滤这两个过程,以减少系统开销。但事实上,其结果却相当有限:仅可能带来性能15%的提升。
在我们所做的性能分析中,大都使用体积较小的数据包。需要解释的是,这并不是因为受到那方面的限制。举例来说,众多网络分析工具(尤其像sniffers和network monitors)都只提取包的起始部分,例如前98个字节,剩下的部分将被丢弃。这正是我们在测试中考虑使用小数据包的原因。
4.5 对测试结果的附加说明
尽管我们的测试结果取自特定平台上(Win32)的特定工具(WinPcap),但结果还是具有普遍意义的。WinPcap(也就是tap处理、第一及第二次拷贝、过滤)的开销类似于其它体系结构上的开销(例如,NPF与BPF相当类似)。同样的道理,的相关开销也都相差不多:NIC驱动、时间戳timestamp、上下文转闸。NIC驱动的开销可以通过将一些原先的软件功能在设计网卡时固化到芯片上的方式来减少,但这却需花费昂贵的成本。基于硬件的时间戳是最有效的优化之一:Endace's DAG卡的广泛使用[17]就是其中一个例子。基于的硬件由于在设计时缺少专业芯片,故没能提供任何简单的方法来获得精确时间戳,而只能通过内部逻辑(例如,借助于CPU计数器)。更糟的是,精确定时是从8253/8254芯片(或其它类似芯片)中获取,然而因为它们需通过系统总线IN/OUT操作,故其存取是相当慢的。
最后再提一点,可以忽略上下文转闸的影响,因为在不同的操作系统中其情况大都相差无几(例如,在调制解调器操作系统中,它被作为一项需谨慎设置的参数)。
5 .最优
针对先前几节内容讲述的这些突出瓶颈,本节将介绍并评估在NPF中所做的优化处理。
5.1 过滤部件
WinPcap使用的过滤系统是BSD包过滤器(BPF),1993[2]它被提议使用。当时文献记载的还有其它一些过滤系统[9][10][11][12],但是在普通操作环境下,它们的性能根本无法与BPF相比。
在对BPF的优化解决方案中,动态代码的产生(即,将包过滤代码编译为CPU执行指令)确保了出色的性能改进[11][10]。因此,Just In Time(JIT)engine引擎被整合入NPF,通过它将BPF过滤代码编译成80x86的二进制代码。图7所示,该优化措施带来了3.1到5的提升。这相当实现了对捕获机制总体性能8%的提升(假设为一个虚指令条数规模为21的过滤器)。
5.2 存储器拷贝
先前已经说了,第一次数据包拷贝(从NIC存储器到核心缓冲区)的开销比第2次的开销来得大。原因是由于它有个额外处理NdisTransferData()。然而,我们注意到几乎所有网络控制器(绝大多数网络适配器)在通报NIC驱动程序前已将一个数据包完整地传输到存储器中,因此NPF驱动程序可在一个邻接的缓冲区中得到它。既然这样,我们放弃旧的方法,而是通过一个标准C库函数来完成拷贝工作,结果如图8显示。由于数据包成块地传输,同时在CPU缓存中保持较高的命中率,这样第2次拷贝的速度也有些许提高。这回两次拷贝的开销相差不多且具有相同的变化趋势。
感谢这一优化措施。在数据包大小为64byte的满载机器上,第1次拷贝过程的开销从540个时钟周期降到了300,当然第二次拷贝过程的开销变化不大。这样,对于整个捕获机制带来了4%的性能提升。
5.3 时间戳
可以通过目前32位Intel处理器中的timestamp Counter(TSC)计数器来取代KeQueryPerformanceCounter(),并由它来得到微秒级精确时间戳。该高效计数器在每个处理器时钟周期都完成一次自增,因此它的精密近乎CPU时钟频率。而且x86汇编器提供一个快速(仅一个周期)指令-rdtsc来得到精确时间戳。