在刚开始学习网络编程时,似乎莫名其妙地就会被某人/某资料告诉select函数是有fd(file descriptor)数量限制的。在最近的一次记忆里还有个人笑说select只支持64个fd。我甚至还写过一篇不负责任甚至错误的博客(突破select的FD_SETSIZE限制)。有人说,直接重新定义FD_SETSIZE就可以突破这个select的限制,也有人说除了重定义这个宏之外还的重新编译内核。
【不想看啰嗦的分析过程,直接看结论,请直接拉到文尾即可】
事实具体是怎样的?实际上,造成这些混乱的原因恰好是不同平台对select的实现不一样。
Windows的实现
MSDN.aspx)上对select的说明:
- int select(
- _In_ int nfds,
- _Inout_ fd_set *readfds,
- _Inout_ fd_set *writefds,
- _Inout_ fd_set *exceptfds,
- _In_ const struct timeval *timeout
- );
- nfds [in] Ignored. The nfds parameter is included only for compatibility with Berkeley sockets.
第一个参数MSDN只说没有使用,其存在仅仅是为了保持与Berkeley Socket的兼容。
The variable FD_SETSIZE determines the maximum number of descriptors in a set. (The default value of FD_SETSIZE is 64, which can be modified by defining FD_SETSIZE to another value before including Winsock2.h.) Internally, socket handles in an fd_set structure are not represented as bit flags as in Berkeley Unix.
Windows上select的实现不同于Berkeley Unix,后者使用位标志来表示socket。
在MSDN的评论中有人提到:
Unlike the Linux versions of these macros which use a single calculation to set/check the fd, the Winsock versions use a loop which goes through the entire set of fds each time you call FD_SET or FD_ISSET (check out winsock2.h and you’ll see). So you might want to consider an alternative if you have thousands of sockets!
不同于Linux下处理fd_set的那些宏(FD_CLR/FD_SET之类),Windows上这些宏的实现都使用了一个循环,看看这些宏的大致实现(Winsock2.h):
- #define FD_SET(fd, set) do { \
- u_int __i; \
- for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count; __i++) { \
- if (((fd_set FAR *)(set))->fd_array[__i] == (fd)) { \
- break; \
- } \
- } \
- if (__i == ((fd_set FAR *)(set))->fd_count) { \
- if (((fd_set FAR *)(set))->fd_count < FD_SETSIZE) { \
- ((fd_set FAR *)(set))->fd_array[__i] = (fd); \
- ((fd_set FAR *)(set))->fd_count++; \
- } \
- } \
- } while(0)
看下Winsock2.h中关于fd_set的定义:
- typedef struct fd_set {
- u_int fd_count;
- SOCKET fd_array[FD_SETSIZE];
- } fd_set;
再看一篇更重要的MSDN Maximum Number of Sockets Supported.aspx):
The Microsoft Winsock provider limits the maximum number of sockets supported only by available memory on the local computer.The maximum number of sockets that a Windows Sockets application can use is not affected by the manifest constant FD_SETSIZE.If an application is designed to be capable of working with more than 64 sockets using the select and WSAPoll functions, the implementor should define the manifest FD_SETSIZE in every source file before including the Winsock2.h header file.
Windows上select支持的socket数量并不受宏FD_SETSIZE的影响,而仅仅受内存的影响。如果应用程序想使用超过FD_SETSIZE的socket,仅需要重新定义FD_SETSIZE即可。
实际上稍微想想就可以明白,既然fd_set里面已经有一个socket的数量计数,那么select的实现完全可以使用这个计数,而不是FD_SETSIZE这个宏。那么结论是,select至少在Windows上并没有socket支持数量的限制。当然效率问题这里不谈。
这看起来推翻了我们一直以来没有深究的一个事实。
Linux的实现
在上面提到的MSDN中,其实已经提到了Windows与Berkeley Unix实现的不同。在select的API文档中也看到了第一个参数并没有说明其作用。看下Linux的man:
nfds is the highest-numbered file descriptor in any of the three sets, plus 1.
第一个参数简单来说就是最大描述符+1。
An fd_set is a fixed size buffer. Executing FD_CLR() or FD_SET() with a value of fd that is negative or is equal to or larger than FD_SETSIZE will result in undefined behavior.
明确说了,如果调用FD_SET之类的宏fd超过了FD_SETSIZE将导致undefined behavior。也有人专门做了测试:select system call limitation in Linux。也有现实遇到的问题:socket file descriptor (1063) is larger than FD_SETSIZE (1024), you probably need to rebuild Apache with a larger FD_SETSIZE
看起来在Linux上使用select确实有FD_SETSIZE的限制。有必要看下相关的实现 fd_set.h:
- typedef __uint32_t __fd_mask;
- /* 32 = 2 ^ 5 */
- #define __NFDBITS (32)
- #define __NFDSHIFT (5)
- #define __NFDMASK (__NFDBITS - 1)
- /*
- * Select uses bit fields of file descriptors. These macros manipulate
- * such bit fields. Note: FD_SETSIZE may be defined by the user.
- */
- #ifndef FD_SETSIZE
- #define FD_SETSIZE 256
- #endif
- #define __NFD_SIZE (((FD_SETSIZE) + (__NFDBITS - 1)) / __NFDBITS)
- typedef struct fd_set {
- __fd_mask fds_bits[__NFD_SIZE];
- } fd_set;
在这份实现中不同于Windows实现,它使用了位来表示fd。看下FD_SET系列宏的大致实现:
- #define FD_SET(n, p) \
- ((p)->fds_bits[(unsigned)(n) >> __NFDSHIFT] |= (1 << ((n) & __NFDMASK)))
添加一个fd到fd_set中也不是Windows的遍历,而是直接位运算。这里也有人对另一份类似实现做了剖析:linux的I/O多路转接select的fd_set数据结构和相应FD_宏的实现分析。在APUE中也提到fd_set:
这种数据类型(fd_set)为每一可能的描述符保持了一位。
既然fd_set中不包含其保存了多少个fd的计数,那么select的实现里要知道自己要处理多少个fd,那只能使用FD_SETSIZE宏去做判定,但Linux的实现选用了更好的方式,即通过第一个参数让应用层告诉select需要处理的最大fd(这里不是数量)。那么其实现大概为:
- for (int i = 0; i < nfds; ++i) {
- if (FD_ISSET...
- ...
- }
如此看来,Linux的select实现则是受限于FD_SETSIZE的大小。这里也看到,fd_set使用位数组来保存fd,那么fd本身作为一个int数,其值就不能超过FD_SETSIZE。这不仅仅是数量的限制,还是其取值的限制。实际上,Linux上fd的取值是保证了小于FD_SETSIZE的(但不是不变的)Is the value of a Linux file descriptor always smaller than the open file limits?:
Each process is further limited via the setrlimit(2) RLIMIT_NOFILE per-process limit on the number of open files. 1024 is a common RLIMIT_NOFILE limit. (It’s very easy to change this limit via /etc/security/limits.conf.)
fd的取值会小于RLIMIT_NOFILE,有很多方法可以改变这个值。这个值默认情况下和FD_SETSIZE应该是一样的。这个信息告诉我们,Linux下fd的取值应该是从0开始递增的(理论上,实际上还有stdin/stdout/stderr之类的fd)。这才能保证select的那些宏可以工作。
应用层使用
标准的select用法应该大致如下:
- while (true) {
- ...
- select(...)
- for-each socket {
- if (FD_ISSET(fd, set))
- ...
- }
- ...
- }
即遍历目前管理的fd,通过FD_ISSET去判定当前fd是否有IO事件。因为Windows的实现FD_ISSET都是一个循环,所以有了另一种不跨平台的用法:
- while (true) {
- ...
- select(. &read_sockets, &write_sockets..)
- for-each read_socket {
- use fd.fd_array[i)
- }
- ...
- }
总结
- Windows上select没有fd数量的限制,默认FD_SETSIZE=64,可通过修改FD_SETSIZE设置上限,但因为使用了循环来检查,所以效率相对较低;
- Linux上select不仅有FD_SETSIZE值限制,默认FD_SETSIZE=1024,并且还有fd值限制(直接做数组下标),就算是修改内核FD_SETSIZE值,也无法突破fd值限制,也因此其相对效率较高;
-
Windows select总体无限制,Linux select有限制,仅可通过poll/epoll替换才能解决问题。