Windows NT和Windows 2000的套接字架构
对于开发大响应规模的Winsock应用程序而言,对Windows NT和Windows 2000的套接字架构有基本的了解是很有帮助的。
与其他操作系统不同的是,WinNT和Win2000的传输协议层并不直接给应用程序提供socket风格的接口,不接受应用程序的直接访问。而是实现了更多的通用API,称为传输驱动接口(Transport Driver Interface,TDI).这些API把WinNT的子系统从各种各样的网络编程接口中分离出来。然后,通过Winsock内核模式驱动提供了sockets方法(在AFD.SYS里实现)。这个驱动负责连接和缓冲管理,对应用程序提供socket风格的编程接口。AFD.SYS则通过TDI和传输协议驱动层交流数据。
1:谁来负责管理缓冲区?
如上所说,对于使用socket接口和传输协议层交流的应用程序来说,AFD.SYS负责缓冲区的管理。也就是说,当一个程序调用send或WSASend函数发送数据的时候,数据被复制到AFD.SYS的内部缓冲里(大小根据SO_SNDBUF设置),然后send和WSASend立刻返回。之后数据由AFD.SYS负责发送到网络上,与应用程序无关。当然,如果应用程序希望发送比SO_SNDBUF设置的缓冲区还大的数据,WSASend函数将会被堵塞,直到所有数据均被发送完毕为止。
同样,当从远地客户端接受数据的时候,如果应用程序没有提交receive请求,而且线上数据没有超出SO_RCVBUF设置的缓冲大小,那么AFD.SYS就把网络上的数据复制到自己的内部缓冲保存。当应用程序调用recv或WSARecv函数的时候,数据即从AFD.SYS的缓冲复制到应用程序提供的缓冲区里。
在大多数情况下,这个体系工作的很好。尤其是应用程序使用一般的发送接受例程不牵涉使用Overlapped的时候。开发人员可以通过使用setsockopt API函数把SO_SNDBUF和SO_RCVBUF这两个设置的值改为0关闭AFD.SYS的内部缓冲。但是,这样做会带来一些后果:
举例来说,一个应用程序把SO_SNDBUF为0把缓冲区(指AFD.SYS里的缓冲)关闭,然后发出一个同步堵塞send()调用。在这样的情况下,系统内核会把应用程序的缓冲区锁定,直到接收方确认收到了整个缓冲区后send()调用才返回。似乎这是一种判定你的数据是否已经为对方全部收到的简洁的方法,实际上却并非如此,这是很糟糕的。问题在于,即使远端TCP通知数据已经收到,其实也根本不代表数据已经成功送给客户端应用程序,比如对方可能发生资源不足的情况,导致AFD.SYS不能把数据拷贝给应用程序。另一个更要紧的问题是,在每个线程中每次只能进行一次发送调用,效率极其低下。
如果关闭接受缓冲(设置SO_RCVBUF的值为0),也不能真正的提高效率。接受缓冲为0迫使接受的数据在比winsock内核层更底层的地方被缓冲,同样在调用recv的时候进行才进行缓冲复制,这样你关闭AFD缓冲的根本意图(避免缓冲复制)就落空了。关闭接收缓冲是没有必要的,只要应用程序经常有意识的在一个连接上调用重叠WSARecvs操作,这样就避免了AFD老是要缓冲大量的到来数据。
到这里,我们应该清楚关闭缓冲的方法对绝大多数应用程序来说没有太多好处的了。只要要应用程序注意随时在某个连接上保持几个WSARecvs重叠调用,那么通常没有必要关闭接收缓冲区。如果AFD.SYS总是有由应用程序提供的缓冲区可用,那么它将没有必要使用内部缓冲区。
一个高性能的服务程序可以关闭发送缓冲,而不影响性能。这样的程序必须确保它在同时执行多个重叠发送调用,而不是等待某个重叠发
送结束之后才执行另一个。这样如果一个数据缓冲区数据已经被提交,那么传输层就可以立刻使用该数据缓冲区。如果程序“串行”的执行Overlapped发送,就会浪费一个发送提交之后另一个发送执行之前那段时间。
2:资源的限制条件
强健性是每一个服务程序的一个主要设计目标。就是说,服务程序应该可以对付任何的突发问题,比如,客户端请求的高峰,可用内存的暂时贫缺,以及其他可靠性问题。为了平和的解决这些问题,开发人员必须了解典型的WindowsNT和Windows2000平台上的资源约束。
最基本的问题是网络带宽。通常,使用用户数据报协议(UDP)的应用程序都可能会比较注意带宽方面的限制,以最大限度地减少包的丢失。
然而在使用TCP连接,服务器必须十分小心地制好注意不要滥用网络资源。否则,TCP连接中将会出现大量重发和连接取消事件。具体的带宽控制是跟具体程序相关的,超出了本文的讨论范围。
虚拟内存的使用也必须很小心地管理。通过谨慎地申请和释放内存,或者应用lookaside lists(一个记录申请并使用过的“空闲”内存的缓冲区)这样可以使服务程序避免过多的反复申请内存,并且保证系统中一直有尽可能多的空余内存。(应用程序还可以使用SetWorkingSetSize这个Win32API函数来向系统请求增加该程序可用的物理内存。)
使用Winsock时还可能碰到另外两个非直接的资源不足情况。
第一个是页面锁定限制。无论应用程序发起send还是receive操作,也不管AFD.SYS的缓冲是否被禁止,数据所在的缓冲都会被锁定在物理内存里。因为内核驱动要访问该内存的数据,在访问期间该内存区域都不能交换出去(解锁)。在大部分情况下,这不会产生任何问题。但是操作系统必须确认还有可用的可分页内存来提供给其他程序。这样做的目的是防止一个有错误操作的程序请求锁定所有的物理RAM,而导致系统崩溃。这意味着,应用程序必须有意识的避免导致过多页面锁定,使该数量达到或超过系统限制。
在WinNT和Win2000中,系统允许的总共的内存锁定的限制大概是物理内存的1/8。这只是粗略的估计,不能作为一个准确的计算数据。只是需要知道,有时重叠IO操作会发生ERROR_INSUFFICIENT_RESOURCE失败,这是因为可能同时有太多的send/receives操作在进行中。程序应该注意避免这种情况。
另一个的资源限制情况是,程序运行时,系统达到非分页内存池的限制。所谓非分页池是一块永远不被交换出去的内存区域。WinNT和Win2000的驱动从指定的非分页内存池中申请内存。这个区域里分配的内存不会被扇出,因为它包含了多个不同的内核对象可能需要访问的数据,而有些内核对象是不能访问已经扇出的内存的。一旦系统创建了一个socket (或打开一个文件),一定数目的非分页内存就被分配了。另外,绑定(binding)和连接socket也会导致额外的非分页内存池的分配。更进一步的说,一个I/O请求,比如send或receive,也是分配了很少的一点非分页内存池的(为了跟踪I/O操作的进行,包含必须信息的一个很小的结构体被分配了)。积少成多,最后还是可能导致问题。因此操作系统限制了非分页内存的数量。在winNT和win2000平台上,每个连接分配的非分页内存的准确数量是不相同的,在未来的windows版本上也可能保持差异。如果你想延长你的程序的寿命,就不要打算在你的程序中精确的计算和控制你的非分页内存的数量。
虽然不能准确计算,但是程序在策略上要注意避免冲击非分页限制。当系统的非分页池内存枯竭,一个跟你的程序完全无关的的驱动都有可能出问题,因为它无法正常的申请到非分页内存。最坏的情况下,会导致整个系统崩溃。比如那些第三方设备或系统本身的驱动。切记:在同一台计算机上,可能还有其他的服务程序在运行,同样在消耗非分页内存。开发人员应该用最保守的策略估算资源,并基于此策略开发程序。
资源约束的解决方案是很复杂的,因为事实上,当资源不足的情况发生时,可能不会有特定的错误代码返回到程序。程序在调用函数时可能可以得到类似WSAENOBUFS或
ERROR_INSUFFICIENT_RESOURCES的这种一般的返回代码。如何处理这些错误呢,首先,合理的增加程序的工作环境设置(Working set,如果想获得更多信息,请参考MSDN里John Robbins关于 Bugslayer的一章)。如果仍然不能解决问题,那么你可能遇上了非分页内存池限制。那么最好是立刻关闭部分连接,并期待情况恢复正常。