select 函数重难点解析
select 函数重难点解析 select 函数是网络通信编程中非常常用的一个函数,因此应该熟练掌握它。虽然它是 BSD 标准之一的 Socket 函数之一,但在 Linux 和 Windows 平台,其行为表现还是有点区别的。我们先来看一下 Linux 平台上的 select 函数。 Linux 平台下的 select 函数 select 函数的作用是检测一组 socket 中某个或某几个是否有“事件”,这里的“**事件”**一般分为如下三类: 可读事件,一般意味着可以调用 recv 或 read 函数从该 socket 上读取数据;如果该 socket 是侦听 socket(即调用了 bind 函数绑定过 ip 地址和端口号,并调用了 listen 启动侦听的 socket),可读意味着此时可以有新的客户端连接到来,此时可调用 accept 函数接受新连接。 可写事件,一般意味着此时调用 send 或 write 函数可以将数据“发出去”。 异常事件,某个 socket 出现异常。 函数签名如下: int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 参数说明: 参数 nfds, Linux 下 socket 也称 fd,这个参数的值设置成所有需要使用 select 函数监听的 fd 中最大 fd 值加 1。 参数 readfds,需要监听可读事件的 fd 集合。 参数 writefds,需要监听可写事件的 fd 集合。 参数 exceptfds,需要监听异常事件 fd 集合。 readfds、writefds 和 exceptfds 类型都是 fd_set,这是一个结构体信息,其定义位于 /usr/include/sys/select.h 中: /* The fd_set member is required to be an array of longs. */ typedef long int __fd_mask; /* Some versions of <linux/posix_types.h> define this macros. */ #undef __NFDBITS /* It's easier to assume 8-bit bytes than to get CHAR_BIT. */ #define __NFDBITS (8 * (int) sizeof (__fd_mask)) #define __FD_ELT(d) ((d) / __NFDBITS) #define __FD_MASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS)) /* fd_set for select and pselect. */ typedef struct { /* XPG4.2 requires this member name. Otherwise avoid the name from the global namespace. */ #ifdef __USE_XOPEN __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS]; # define __FDS_BITS(set) ((set)->fds_bits) #else // 在我的centOS 7.0 系统中的值: // __FD_SETSIZE = 1024 //__NFDBITS = 64 __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS]; # define __FDS_BITS(set) ((set)->__fds_bits) #endif } fd_set; /* Maximum number of file descriptors in 'fd_set'. */ #define FD_SETSIZE __FD_SETSIZE 我们假设未定义宏 __USE_XOPEN,将上面的代码整理一下: typedef struct { long int __fds_bits[16]; } fd_set; 将一个 fd 添加到 fd_set 这个集合中需要使用 FD_SET 宏,其定义如下: void FD_SET(int fd, fd_set *set); 其实现如下: #define FD_SET(fd,fdsetp) __FD_SET(fd,fdsetp) FD_SET 在内部又是通过宏 __FD_SET 来实现的,__FD_SET 的定义如下(位于 /usr/include/bits/select.h 中): #if defined __GNUC__ && __GNUC__ >= 2 # if __WORDSIZE == 64 # define __FD_ZERO_STOS "stosq" # else # define __FD_ZERO_STOS "stosl" # endif # define __FD_ZERO(fdsp) \ do { \ int __d0, __d1; \ __asm__ __volatile__ ("cld; rep; " __FD_ZERO_STOS \ : "=c" (__d0), "=D" (__d1) \ : "a" (0), "0" (sizeof (fd_set) \ / sizeof (__fd_mask)), \ "1" (&__FDS_BITS (fdsp)[0]) \ : "memory"); \ } while (0) #else /* ! GNU CC */ /* We don't use `memset' because this would require a prototype and the array isn't too big. */ # define __FD_ZERO(set) \ do { \ unsigned int __i; \ fd_set *__arr = (set); \ for (__i = 0; __i < sizeof (fd_set) / sizeof (__fd_mask); ++__i) \ __FDS_BITS (__arr)[__i] = 0; \ } while (0) #endif /* GNU CC */ #define __FD_SET(d, set) \ ((void) (__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d))) #define __FD_CLR(d, set) \ ((void) (__FDS_BITS (set)[__FD_ELT (d)] &= ~__FD_MASK (d))) #define __FD_ISSET(d, set) \ ((__FDS_BITS (set)[__FD_ELT (d)] & __FD_MASK (d)) != 0) 重点看这一行: ((void) (__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d))) __FD_MASK 和 __FD_ELT 宏在上面的代码中已经给出定义: #define __FD_ELT(d) ((d) / __NFDBITS) #define __FD_MASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS)) __NFDBITS 的值是 64 (8 * 8),也就是说 __FD_MASK (d) 先计算 fd 与 64 的余数 n,然后执行 1 « n,这一操作实际上是将 fd 的值放在 0~63 这 64 的位置上去,这个位置索引就是 fd 与 64 取模的结果;同理 __FD_ELT(d) 就是计算位置索引值了。举个例子,假设现在 fd 的 值是 57,那么在这 64 个位置的 57 位,其值在 64 个长度的二进制中置位是: 0000 0010 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 这个值就是 1 « (57 % 64) 得到的数字。 但是前面 fd 数组的定义是: typedef struct { long int __fds_bits[16]; //可以看成是128 bit的数组 } fd_set; long int 占 8 个字节,一个 16 个 long int,如果换成二进制的位(bit)就是 8 * 16 = 128, 也就是这个数组只用了低 64 位, 高 64 位并没有使用。这说明在我的机器上,select 函数支持操作的最大 fd 数量是 64。 同理,如果我们需要从 fd_set 上删除一个 fd,我们可以调用 FD_CLR,其定义如下: void FD_CLR(int fd, fd_set *set); 原理和 FD_SET 相同,即将对应的标志位由1变0即可。 如果,我们需要将 fd_set 中所有的 fd 都清掉,则使用宏 FD_ZERO: void FD_ZERO(fd_set *set); 当 select 函数返回时, 我们使用 FD_ISSET 宏来判断某个 fd 是否有我们关心的事件,FD_ISSET 宏的定义如下: ...