一般来讲, 在服务器上,如果有足够的资源,Winsock server,理论上可以支持成千的并发连接。而现实是,我们没有足够的资源可供使用,分配。本文主要来讨论一下内存资源之于Winsock server开发的重要性。
一)基本概念。
-> Pages,Locked Pages.
在现代操作系统中,内存管理会把主存(RAM)分成Pages来管理。 Paging(或者swapping)指的是主存与外存之间以Page为单位进行数据的交换。Locked Pages指的是被锁定在主存中的内存页,以保证一些内核组件,driver可以访问到它们。windows一定会保证一定数量的可交换的内存空间,防止一些非法程序锁定所有的物理内存,而致使系统崩溃。在windows NT, windows 2000上,可锁定的内存总的大小上限大概是物理内存的1/8(当然对于程序的开发人员,不应该对这个值进行任何的假设,这个值可能会随着操作系统本版的变化而变化)。在Winsock应用开发过程中,以overlapped方式读写IO操作,将会导致内存被锁定。
-> working set
在程序开始运行,并达到其稳定的运行状态(主要指的是其对内存的使用),在这个状态下,程序使用内存的数量一般小于其需要使用内存的总量。这样一个稳定的运行状态,我们可以称为working set: 被该程序频繁访问的内存页的集合。在windows上,你可以使用SetWorkingSetSize Win32 API来增加程序使用物理内存的数量。
-> non-paged pool
不可交换的内存。这主要指以non-paged的方式分配的内存,这些内存就像locked pages一样,是从来不会被交换出去的,用来存放一些由内核组件,driver访问的信息。 在Winsock应用开发过程中,以下的操作可能导致分配non-paged内存。
1) 调用系统一些系统的API,例如打开文件,create socket,等,都会导致从non-paged pool分配内存。
2) 一些driver可以显式地从该区域分配内存。
二) Winsock server上Locked Pages使用。
我们提到过,任何的overlapped IO操作,都会导致锁定内存页。这些内存页一旦被locked,就不会被交换出去。我们知道,windows操作系统对最大的可锁定内存页做了一个上限,如果超出这个上限,overlapped IO调用将会导致WSAENOBUFS错误。
考虑下面的情况,如果server在每个连接上会发出很多的overlapped receives操作,那么,随着连接数目的增多,很明显,被锁定的内存数量很有可能达到上限而导致WSAENOBUFS错误。在这种情况下,如果服务器预期会处理大数量的客户端连接,则需要服务器在每个连接上发出zero-byte buffer的overlapped接收请求(这种情况下,因为the size of buffer is zero,所以没有任何内存被锁定),一旦overlapped接收操作完成,server可以以non-blocking方式执行receive操作,以取得所有缓存在so_rcvbuf中的数据,直到返回WSAEWOULDBLOCK为止。
另外需要注意的是,windows在page的边界上对内存进行锁定,在x86平台上,它是4kb的整数倍。所以,假如你post了一个1 KB buffer,而系统真实锁定的是4 KB 的大小,为了避免这样的浪费,尽量用4kb的整数做overlapped IO操作。
三) Winsock server上non-paged pool使用。
同Locked Pages限制一样,windows对non-paged pool也有一个最大的限制。并且,当你的应用出现这个问题的时候,超出它的最大限制数,情况要远比Locked Pages复杂。这种情况下,后果是不确定的,有可能你的Winsock调用返回WSAENOBUFS错误,也有可能,在系统中,一个和你的应用毫无关联的driver由于申请不到non-paged内存而致使system crash。而这样的灾难,是没法恢复的。
考虑一个具体的例子:我们假设在windows2000上,系统有1 GB内存。这样的配置下,windows大概会预留1/4的空间用作non-paged pool(同样,对于程序的开发人员,不应该对这个值进行任何的假设),即:256MB。这样的配置下,保守估计,我们的Winsock server能够处理到大概50,000连接,或者更多。(每个accepted socket大概消耗1.5kb,每个连接上post一个overlapped操作,分配一个IRP,大概需要500 byte, 总计:(1500+500)*50,000 = 100 Mb) 。
无论是对于Locked Pages,还是对于non-paged pool使用,一旦超出了上限,Winsock调用仅仅会返回一般的WSAENOBUFS 或者ERROR_INSUFFICIENT_RESOURCES错误。为了处理这些错误,你可以试试以下的方法:
1) 需要首先调用SetWorkingSetSize,增加应用的可支配资源数,看能否解决。
2) 确信你的应用没有做出太多的overlapped IO操作。
3) 关闭一些连接数。
四) SOCKET的缓冲区设置问题。
Winsock在默认的情况下,每个socket都会与一个send和receive buffer相关联。你可以通过调用setsockopt来设置buffer的大小。
在缓冲区没有被关闭的情况下,我们看看overlapped send和revc是怎么工作的。
当上层的应用做出了send调用,而这时如果send buffer还有剩余的空间,那么数据将会从用户提交的buffer复制到send buffer中,然后调用返回成功。否则,假如这时send buffer已满,用户提交的buffer将会被锁定,并且调用返回WSA_IO_PENDING。当send buffer的数据被下层的tcp处理完成,winsock将直接处理用户提交的buffer里的数据,而不需要再复制。
同样,对于recv操作,如果数据已经被缓存在socket的receive buffer里,当发生recv调用的时候,数据将直接从socket的receive buffer复制到用户的buffer里,recv调用返回成功。否则,假如发生调用时receive buffer里没有数据,用户提交的buffer将会被锁定,recv调用返回WSA_IO_PENDING。当数据到达当前连接,将会被直接复制到用户提交的buffer里。
一个应用程序通过设定send buffer为0,把缓冲区关闭,然后发出一个阻塞send()调用。在这样的情况下,系统内核会把应用程序的缓冲区锁定,直到接收方确认收到了整个缓冲区后send()调用才返回。似乎这是一种判定你的数据是否已经为对方全部收到的简洁的方法,实际上却并非如此。想想看,即使远端tcp通知数据已经收到,其实也根本不代表数据已经成功送给客户端应用程序,比如对方可能发生资源不足的情况,导致afd.sys不能把数据拷贝给应用程序。另一个更要紧的问题是,在每个线程中每次只能进行一次发送调用,效率极其低下。
另外,希望通过关闭Winsock缓冲区,从而避免数据复制,达到优化性能的目的,也是不可取的。从上面,我们看到:只要应用保证适量的,足够的send, recv调用,这样的复制是完全可以避免的。
高性能的服务器应用程序可以关闭发送缓冲区,同时不会损失性能。不过,这样的应用程序必须十分小心,保证它总是发出多个重叠发送调用,而不是等待某个重叠发送结束了才发出下一个。如果应用程序是按一个发完再发下一个的顺序来操作,那浪费掉两次发送中间的空档时间,总之是要保证传输驱动程序在发送完一个缓冲区后,立刻可以转向另一个缓冲区。
如果关闭了recv buffer,在你的应用没有保证足够的recv操作前提下,任何进来数据,必须在TCP层进行缓存,最大缓存的数量将取决于tcp windows的大小(17Kb)。而最为严重的是这些缓存是从non-paged pool分配而来。如上所述,non-paged pool是非常珍贵,稀缺的内存。所以,从这个意义上来讲,关闭了recv buffer操作是不可取的。