对Windows TCP/IP协议栈的一种简化设计
扫描二维码
随时随地手机看文章
摘要:为了让Windows下的网络数据快速发送与接收,提高实时性能,采用对Windows TCP/IP协议栈进行一种简化设计,通过利用Windows提供的用户与设备对象的交互,减少Windows从用户态到内核态的分层,对Socket层进行简单的封装,并且利用零拷贝技术减少数据的拷贝次数以及设置多级优先级队列使数据按照优先级发送,从而使数据达到快速发送和接收的目的。实验结果验证了这种方法能达到预期效果。
关键词:网络体系结构;协议驱动;TCP/IP;零拷贝;优先级队列
随着现代信息技术的进一步发展,对信息传递的速度及对大信息量的传输都有进一步需求,在这样的情况下,无论是对于硬件要求还是软件方面的要求都在提升。而在相同的硬件条件下,如何提高通信的速度、实时性能,软件的优劣在很大程度上影响着这些方面。由于Windo ws的广泛使用和其方便完善的网络结构的支持,针对Windows的网络开发不断增加,然而由于Windows系统为了包容多种协议以及Windows分层驱动的特点,导致数据从用户应用程序到网卡经过的驱动层数很多,势必导致数据的延迟增加,这对那些要求通信实时性能较高的系统来说是无法容忍的。所以研究Windows网络体系架构,对TCP/IP协议栈进行优化,使之适合于对于特定要求的系统,有很重要的研究意义和价值。
1 Windows网络的多层结构
如图1所示,标准的Windows网络体系结构的最底层是网卡,网卡通过NDIS与网卡(NIC)驱动程序通信,网卡驱动程序又通过NDIS与协议驱动程序通信。在NIC驱动程序和协议驱动程序之间还可以插进去一个中间驱动程序。在协议驱动程序的上边,是内核模式TDI客户驱动程序,通过TDI接口同协议驱动程序交互。再往上,则是用户模式的动态连接库(提供WIN32 NETAPI)及网络应用程序。
从图1中可以看出,用户层编写网络程序与其他主机进行通信,发送数据需要经过Sockets接口,TDI客户,TDI传输驱动接口,MDIS协议驱动(TCP/IP协议栈),NIC驱动程序,网卡,可以看出数据从用户提交给网卡的分层很多,Windows操作系统利用这种分层设计的方法,有诸多好处,开发人员可以只关注整个结构中的某一层;分层可以降低层与层之间的依赖,既可以良好地保证未来的可扩展,在复用性上也是很好的优势。但是分层结构也不可避免具有一些缺陷,一方面,分层过多会导致系统性能的下降,因为不采用分层结构,很多业务可以直接造访数据库,以此获取相应的数据,如今却必须通过中间层来完成,其中需要处理数据通过各层的信息等操作,这些都降低了系统性能;另一方面是数据的拷贝次数增多,数据拷贝操作不单需要占用CPU时间片,同时也需要占用额外的内存带宽,这就增加了系统开销。这些消耗都会造成数据的时间延迟增加,这对于那些对实时性能有特别要求的而又需要利用Windows平台的系统来说,这是很难容忍的。
2 对Windows TCP/IP协议栈的简化设计
由于Windows网络结构分层较多,导致系统性能下降,网络数据的实时性能得不到体现,一种策略是简化现今Windows TCP/IP协议栈,减少分层;另一方面,利用零拷贝技术减少拷贝次数,减少系统性能消耗;可以采用多级优先级队列,让优先级较高的数据比优先级低的数据优先发送的基本策略,采用适当的调度算法进行处理。通过这几个方面的改进,可以一定程度上弥补Windows系统网络通信延迟较大的缺陷。
如图2所示,第1层为用户应用层;第2层是协议驱动,里面包括简化了的TCP/IP协议栈,只保留TCP,UDP,ARP,ICMP协议,并且对Wind ows中的AFD驱动模块简化,一些数据结构等就存放在设备扩展中;第3层是网卡驱动,第4层就是具体收发数据的网卡。在这几层里面第2层是最关键的部分,下面就具体如何设计进行比较详细的介绍。
2.1 采用零拷贝技术
简单一点来说.零拷贝就是一种避免CPU将数据从一块存储拷贝到另外一块存储的技术。零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率。而且,零拷贝技术减少了用户应用程序地址空间和操作系统内核地址空间之间因为上下文切换而带来的开销。在本文中对于TCP/IP协议栈,采用零拷贝技术,避免操作系统内核缓冲区之间进行数据拷贝操作,可以大大提高系统性能。
在接收发送数据时,用NDIS中的NDIS_PACKET包描述符,包描述符中包含了数据包的总长度,指向第一个缓冲描述符NDIS_BUFFER的指针,缓冲描述符NDIS_BUFFER里面的Start Virtual Address才是指向真正的数据所在的首地址以及包含了此缓冲中的数据长度。利用这个NDIS_PACKET包描述符,可以实现无需对数据进行拷贝,只要获得包描述符即可。
当用户数据提交给内核缓冲区时,采用直接I/O的方式,在内核中需要分配一个包首部的大小,用于TCP/IP协议层加上各层的首部,并且把该首部地址以及用户缓冲区地址用NDIS_PACKET包描述符封装。当发包线程把NDIS_PACKET包提交给协议栈处理时,不需要把包描述符中的数据拷贝到新的缓冲区中,可以直接利用NDIS提供的函数得到数据的首地址,以及数据包的总长度等。在协议栈中添加上各层首部以及其他操作后,就可以调用发包函数把NDIS_PACKET包描述符提交给网卡驱动,网卡驱动通过DMA把数据传送到网卡环形缓冲区中,再由网卡发送出去。
反之,在收包时,网卡通过DMA把数据传输到内核缓冲区中,网卡驱动程序中依然用包描述符来指明数据的地址,大小等信息。在收包处理线程中,对数据包的拆包等操作,同样的不需要拷贝到新的缓冲区中,利用包描述符提供的包地址,大小等信息进行处理即可。
2.2 设置多级优先级队列
在网络数据传输中,由于有些紧急数据希望尽快发送出去提交给目的主机,而曰前的Windows系统网络传输机制并没有提供这样的功能。可以通过采用多级优先级队列的方式来达到一定的实时效果,比如对于紧急数据,可以设置最高优先级值,而一般数据就可以设置最低优先级值。在用户应用程序中,对发送函数进行封装,新的发送函数有个优先级参数,通过指明优先级参数值灵活处理数据,当提交给内核时,就按照优先级值放到相对应的优先级队列中。相应的在内核收包、发包缓冲区中,设置多级优先级队列,按照多级反馈队列调度算法进行处理,每个队列的优先级不同,并且每个队列的被处理的时间不同,各个队列的时间片是随着优先级的减少而增加的,优先级越高的队列中它的被线程处理的时间也就越短。比如紧急数据放到最高优先级队列中,迟缓的数据可以放到最低优先级队列中,在内核的发包线程中,首先判断最高优先级队列是否为空,不为空则优先发送该队列中的数据包,当该队列的时间用完,如果该队列还有包没有处理完,则把这些包链接到低一级的队列尾部,然后判断低一级优先级队列是否为空,重复以上的操作依次进行下去,当对最低优先级队列处理完后,再循环处理。如果线程在处理第i队列的数据时,这时候有新的用户数据进入到比i队列优先级高的j队列中,则线程处理完该数据就立即去处理j队列,这个可以用一个掩码mask,每一位标识一个队列,当队列不为空,则该标识位置为1,否则置为0。
2.3 封装Socket层
创建Socket套接字,就是打开设备对象(第一次是创建,之后就是打开),而打开设备对象就会创建一个内核文件对象,这个内核文件对象其实就可以映射创建的Socket套接字。对于打开设备对象,就可以用CreateFile()函数,并且把返回的句柄定义为Socekt句柄,之后的操作就可以直接用这个Socket句柄进行操作,如send()函数,可以用WriteFile()函数封装实现;Receive()函数可以用ReadFile()函数封装实现;bind()函数、setsockopt()函数、getsockopt()函数都可以通过DeviceIoControl()函数封装实现。为了真正实现打开设备等操作,需要在协议驱动程序中埘各个用户应用程序下达的IRP请求进行响应,用派遣函数就可以实现。在图2中,用户程序可以通过新封装好的Socket层,使用原来同样的Socket编程语句,这样使用户使用起来感觉没有差别,对用户是透明的。
2.4 协议驱动
在应用程序中,对同一个线程环境下的文件句柄的读,写等,映射到内核中的IRP I/O堆栈的内核文件对象File()bject是同一个File()bject,这样可以用内核文件对象作为纽带作用。在协议驱动的设备扩展NDISPROT_OPEN_CONTEXT结构体内,建立一个File Port链表,如图3所示。链表的每个节点包含有内核文件对象、接收数据缓冲区、发送数据缓冲区、端口号、接收数据缓冲区大小、发送数据缓冲区大小等儿部分。内核文件对象用来标识是哪一个用户Socket句柄;接收、发送数据缓冲区用来存放Socket的接收、发送的数据;端口号的作用是让网络数据包可以知道提交到哪个内核文件对象下的接收缓冲区;接收、发送数据缓冲区大小指明接收、发送缓冲区最大的长度。如果缓冲区队列满,而这时候又有数据过来,则该数据应被丢弃。在协议驱动程序里面,利用这个FilePort链表,可以实现收发数据,设置接收、发送缓冲区的大小等操作。
需要注意的是在NDISPROT_OPEN_CONTEXT结构体内,需创建一个NPROT_LOCK类型的锁,用来对FilePort链表进行互斥访问。
2.4.1 端口号的绑定
在协议驱动设备扩展中需要建立一张表,里面存放已默认分配的端口号以及用户绑定的端口号,端口号是从小到大按序排列,表的作用是当用户应用程序绑定端口号操作时,首先会通过二叉查找法查找这张表,看该端口号是否存放在该表中,如果找到,则要返回给应用程序绑定失败,如果没有找到,则把该端口号插入到适当位置,并返回给应用程序绑定成功。用户应用程序通过调用bind()函数实现绑定Socket套接字,其含义就是用端口号来惟一标识用户线程下的Socket,让网络数据包提交给正确的Socket,在bind函数里面可以通过封装DeviceIo Control函数调用来实现。
2.4.2 发送数据过程
用户应用程序发送的IRP写请求(WriteFile()函数),传递到协议驱动程序后,调用派遣函数NdisProtWrite,通过IRP I/O堆栈里面的内核文件对象循环遍历FilePort链表找到对应的节点,然后把用户应用程序的数据通过缓冲区读写设备的方式拷贝到NDISPROT_OPEN_CONTEXT结构的相应的Priority SendQueue优先级队列中。如图3所示,发包线程的工作主要有,从Priority SendQueue优先级队列中提取数据,如何提取按照多级反馈队列调度算法处理,经过简化的TCP/IP协议栈,然后再调用NdisSendPackets函数发送给网卡驱动程序。在TCP/IP协议栈中,把该数据的优先级值赋值给IP首部的服务类型(TOS)字段中,使收包的时候根据此字段的优先级值把包放进相应的收包优先级队列中。
2.4.3 接收数据过程
协议驱动从网卡驱动程序接收网络数据包,这些数据包是打包封装好的,首先存放在NDISPROT_OPEN_CONTEXT结构的收包优先级队列Pri ority RecvQueue中,这样可以接收到高速上传过来的底层数据。如图3所示,需要建立一个收包处理线程,它的主要工作是,从收包优先级队列提取数据,具体算法根据上面的多级反馈队列调度算法,然后经由TCP/IP协议栈的处理,如果是UDP,TCP的数据包则通过包的目的端口号,遍历FilePort链表找到对应的节点,然后把剩下的净数据提交给节点(目标Socket)的收包缓冲区中。值得注意的是,因为NDIS封装数据用的是NDIS_PACKET结构,NDIS_PACKET结构里面包含一个NDIS_BUFFER结构的链表,在每个NDIS_BUFFER里面才真正指向数据的首地址,这里说的提交,并没有拷贝数据,只是把净数据的首地址再次链接到FilePort链表中。当用户应用程序通过Receive()函数接收数据的时候,会调用ReadFile()函数,发出读IRP请求,IRP到达协议驱动后,调用NdisProtRead()派遣函数处理,NdisProtRead()会通过IRP I/O堆栈里面的内核文件对象,遍历FilePort链表,找到相应的节点,再把节点接收缓冲区里面的数据拷贝到用户缓冲区里面。
3 测试与分析
3.1 测试环境和方法
测试时,使用2台主机分别作为客户机和服务器。硬件和操作系统都是相同的配置,操作系统是Microsoft Windows XP Professional Service Pack 3,Pentium(R)Dual-Core CPU,主频2.70 GHZ,内存2 GB,网卡Realtek RTL8102E/RTL8103E,交换机为朗讯Lucent Cajun P116T。测试的主要目的是分析简化后的网络相对于原来系统而言,在通讯延迟方面有何改进。测试的方法采用如下方案:在局域网内,采用UDP数据报进行通信测试,从客户机向服务器发送数据,数据长度为300 B,即ping-pong测试,客户端取1 000次的往返时间作为测试参数,对没有简化TCP/IP协议栈之前的客户端与简化之后的客户端进行相同的测试,记录次数15次。
3.2 结果分析
从图4中可以看出,经过对协议栈简化后,传输时间明显大大减少。经计算,简化前平均耗时1.241 s,而简化后的平均耗时为0.072 s,减少了94.198%,简化前耗时的样本标准差为0.038 2,简化后耗时的样本标准差为0.004 9,显然简化后的稳定性要更好。测试结果表明,经过简化后的这种设计,耗时和稳定性能都能到达很好的效果。
4 结语
本文讨论了一种简化现有的Windows系统TCP/IP协议栈的一种方法,减少了驱动结构层次,使用户应用程序的数据能较快提交给协议驱动程序,协议驱动程序通过封装Socket,并且设置多级优先级队列以及采用零拷贝技术。通过这些技术的使用,能使数据提交给网卡的系统开销减少,使数据发送时间延迟减少,能满足一定的实时性能需求。