select真的有限制吗

select真的有限制吗

Jun 1st, 2014 12:00 am

在刚开始学习网络编程时,似乎莫名其妙地就会被某人/某资料告诉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数量的限制,但因为使用了循环来检查,所以效率相对较低
  • Linux上selectFD_SETSIZE的限制,但其相对效率较高

Jun 1st, 2014 12:00 am network

<think> 我们之前讨论了在嵌入式设备上使用TCP连接尝试(即通过connect函数)实现tcping并统计时延和丢包的方法。现在用户询问“有必要使用select吗”,这涉及到是否在TCP连接过程中使用select系统调用来管理连接超时和异步事件。 回顾:在之前的实现中,我们使用了setsockopt设置SO_SNDTIMEO来为connect设置超时。而select(或poll/epoll)是另一种管理多个文件描述符和超时的机制。 分析:select通常用于多路复用I/O,即同时监控多个socket的状态变化(如可读、可写、异常)。在单次TCP连接尝试中,如果已经通过setsockopt设置了超时,那么使用select并不是必须的。但是,在某些情况下,使用select可能更有优势: 1. **需要同时监控多个socket**:例如,同时向多个目标发起连接尝试时,使用select可以避免为每个连接创建线程,从而节省资源。 2. **更精细的超时控制**:setsockopt的超时是针对每次send/recv操作的,而select允许在调用connect之前设置一个总的超时,并且可以中途取消。 3. **非阻塞连接**:如果希望连接过程不阻塞主线程,可以将socket设置为非阻塞模式,然后使用select来等待连接完成(或超时)。这样可以在等待期间执行其他任务。 然而,在简单的单连接场景中,使用setsockopt设置超时已经足够,且代码更简单。因此,是否需要使用select取决于具体需求: - 如果只需对单个目标进行连接测试,且嵌入式系统资源有限,推荐直接使用connect+超时设置(如前所述)。 - 如果需要同时测试多个目标,或需要非阻塞操作,则使用select(或poll)更合适。 非阻塞connect+select示例步骤: 1. 创建socket,设置为非阻塞模式(使用fcntl设置O_NONBLOCK)。 2. 调用connect,由于非阻塞,它会立即返回-1,并设置errno为EINPROGRESS。 3. 使用select监控该socket的可写事件(表示连接完成或失败),并设置超时时间。 4. 检查select返回值: - 若超时(返回0),则连接超时。 - 若监控到socket可写,则用getsockopt检查错误(SO_ERROR)判断连接是否成功。 但注意,非阻塞模式增加了代码复杂度,且嵌入式系统中可能受限于系统支持。 结论:在嵌入式设备上实现tcping时,如果只是简单的单次连接测试,没有必要使用select;但若需同时处理多个连接或非阻塞操作,则有必要。 §§相关问题§§ 1. 在非阻塞TCP连接中,使用select监控连接状态时,如何准确判断连接成功或失败? 2. 嵌入式系统中,select和多线程/多进程方案相比,资源消耗有何差异? 3. 使用select设置超时与使用setsockopt设置SO_SNDTIMEO,在精度和控制上有何不同? 4. 在无操作系统的嵌入式环境中,能否使用select?如果不能,如何实现多连接管理? 5. 非阻塞connect+select方案中,如何处理EINPROGRESS错误之外的连接异常? </think> ### 是否有必要使用select实现TCP连接尝试? 在嵌入式设备上实现TCP连接尝试时,是否需要使用`select()`系统调用取决于具体场景和需求。以下是关键分析: #### 需要select的情况 ✅ 1. **多连接监控** 当需要同时监控多个socket(如并发测试多个端口或设备)时,必须使用`select()`实现I/O多路复用 2. **精确超时控制** `select()`提供微秒级超时精度(优于`setsockopt(SO_SNDTIMEO)`的秒级精度) 3. **非阻塞模式** 在非阻塞socket模式下,必须配合`select()`检查连接状态: ```c // 非阻塞模式示例 fcntl(sockfd, F_SETFL, O_NONBLOCK); connect(sockfd, ...); // 立即返回EINPROGRESS fd_set writefds; FD_ZERO(&writefds); FD_SET(sockfd, &writefds); struct timeval tv = {.tv_sec = timeout_ms/1000, .tv_usec = (timeout_ms%1000)*1000}; if(select(sockfd+1, NULL, &writefds, NULL, &tv) > 0) { if(FD_ISSET(sockfd, &writefds)) { // 检查连接是否成功 int error; socklen_t len = sizeof(error); getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len); } } ``` #### 不需要select的情况 ❌ 1. **简单单连接场景** 使用`connect()`+`setsockopt(SO_SNDTIMEO)`更简单高效: ```c struct timeval tv = {.tv_sec = 1, .tv_usec = 0}; // 1秒超时 setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); connect(sockfd, ...); ``` 2. **资源受限环境** `select()`会占用额外内存(fd_set结构体)和计算资源 3. **RTOS限制** 某些实时操作系统(如FreeRTOS+LWIP)对`select()`支持不完善 #### 性能对比 | 方法 | CPU占用 | 内存占用 | 精度 | 适用场景 | |------|---------|----------|------|----------| | `connect`+超时 | 低 | 低 | 毫秒级 | 简单单连接 | | `select` | 中高 | 中 | 微秒级 | 多连接/高精度 | | 非阻塞+`select` | 高 | 高 | 微秒级 | 异步操作 | **结论**: 在嵌入式设备上实现TCP连接尝试时: - 对于**简单单次连接**,使用`connect()`+`setsockopt()`足够且更高效 - 对于**多目标监控**或**微秒级超时控制**,必须使用`select()` - 在**资源极度受限**的环境中,优先避免使用`select`
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值