I/O复用--select()
select
man手册:select(2) - Linux manual page (man7.org)
参考:https://www.cnblogs.com/alantu2018/p/8612722.html
函数原型
#include <sys/select.h>
int select(int nfds, fd_set *restrict readfds,
fd_set *restrict writefds,
fd_set *restrict exceptfds,
struct timeval *restrict timeout);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
第一个参数:nfds必须是当前最大文件描述符+1
- 函数限制:
select() can monitor only file descriptors numbers that are less than FD_SETSIZE (1024)—an unreasonably low limit for many modern applications—and this limitation will not change. All modern applications should instead use poll(2) or epoll(7), which do not suffer this limitation.
select()可以监听的文件描述符小于1024,如果需要监听更多的文件描述符,就用
poll和epoll
以下的代码来自centos7
- fd_set结构体,下面是类型定义
/* The fd_set member is required to be an array of longs. */
typedef long int __fd_mask;
/* Sometimes the fd_set member is assumed to have this type. */
typedef __fd_mask fd_mask;
/* Maximum number of file descriptors in `fd_set'. */
#define FD_SETSIZE __FD_SETSIZE
/* Number of descriptors that can fit in an `fd_set'. */
#define __FD_SETSIZE 1024
/* Number of bits per word of `fd_set' (some code assumes this is 32). */
# define NFDBITS __NFDBITS
/* It's easier to assume 8-bit bytes than to get CHAR_BIT. */
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
/* fd_set for select and pselect. */
typedef struct
{
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
- 简化版本:将以上的类型定义替换成最原始的数据类型,可以看出
fd_set就是一个long int 数组
/* fd_set for select and pselect. */
typedef struct
{
long int fds_bits[1024 / (8 * (int) sizeof (long int))];
} fd_set;
// sizeof(long int) = 8,对于centos7,也即是上面的数组大小=1024/8*8=16
- 对f
fd_set操作的宏。
/* It's easier to assume 8-bit bytes than to get CHAR_BIT. */
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
// __NFDBITS = 8 * 8 = 64
#define __FD_ELT(d) ((d) / __NFDBITS)
// __FD_ELT(d) = (d / 64)
#define __FD_MASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS))
// __FD_MASK(d) = ((long int)1<<((d)%64))
/* Access macros for `fd_set'. */
#define FD_SET(fd, fdsetp) __FD_SET (fd, fdsetp)
#define FD_CLR(fd, fdsetp) __FD_CLR (fd, fdsetp)
#define FD_ISSET(fd, fdsetp) __FD_ISSET (fd, fdsetp)
#define FD_ZERO(fdsetp) __FD_ZERO (fdsetp)
/* 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 */
// __FDS_BITS (set)就是取fd_set的数组
// FD_SET(d, set)的操作为: [d/64] |= ((long int)1<<((d)%64))
// FD_CLR(d, set)的操作为: [d/64] &= ~((long int)1<<((d)%64))
// FD_ISSET(d, set)的操作为: [d/64] & ((long int)1<<((d)%64)) != 0
#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)
由函数限制可知,select监听的文件描述符的数字大小应该小于1024。则文件描述符d可以用10个bit表示。由上面的宏知道,这个10个bit的前4bit确定对应于数组的哪个数,而后6个bit用于确定对数组中的某个long int的哪一位进行操作。前4bit对应[d/64],后6bit对应(d%64)
- timeval结构体
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
函数使用
第一 .struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符(filedescriptor),即文件句柄,这可以是我们所说的普通意义的文件,当然 Unix下任何设备、管道、FIFO等都是文件形式,全部包括在内,所以毫无疑问一个socket就是一个文件,socket句柄就是一个文件描述符。
fd_set集合可以通过一些宏由人为来操作,比如:
FD_ZERO(fd_set *);清空集合
FD_SET(int, fd_set *);将一个给定的文件描述符加入集合之中
FD_CLR(int, fd_set*); 将一个给定的文件描述符从集合中删除
检查集合中指定的文件描述符是否可以读写FD_ISSET(int ,fd_set* )。一会儿举例说明。
第二 .struct timeval 是一个大家常用的结构,用来代表时间值,有两个成员,一个是秒数,另一个是微秒。 具体解释select的参数:
int maxfdp:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数的值无所谓,可以设置不正确。
fd_set* readfds:是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中 读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断 是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
fd_set* writefds:是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件 中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判 断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。
fd_set * errorfds:同上面两个参数的意图,用来监视文件错误异常。
struct timeval* timeout:是select的超时时间,这个参数至关重要,它可以使select处于三种状态:
第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
第二,若将时间值设为0秒0微秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
第三,timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
第三.select 返回值:
负值:select错误
正值:某些文件可读写或出错
0:等待超时,没有可读写或错误的文件
底层原理
https://my.oschina.net/fileoptions/blog/911091
使用例子
传入accept后得到的connfd,持续监听connfd是否有数据进入。
可见使用select函数是比较麻烦的,因为要传入最大描述符+1作为第一个参数,并且select之前的都要重新FD_SET,对于检测都要使用FD_ISSET.
void mon_connfd(int connfd) {
fd_set read_fds;
fd_set exception_fds;
FD_ZERO(&read_fds);
FD_ZERO(&exception_fds);
char buf[1024];
while (1)
{
memset(buf, '\0', sizeof(buf));
/**
* 每次调用select之前都要FD_SET,
* 因为事件发生后,文件描述符集合将被内核修改
*/
FD_SET(connfd, &read_fds);
FD_SET(connfd, &exception_fds);
int ret = select(connfd+1, &read_fds, NULL, &exception_fds, NULL);
if (ret < 0) {
printf("select() error!\n");
break;
}
// 对于可读事件,用recv函数读取数据
if(FD_ISSET(connfd, &read_fds)) {
ret = recv(connfd, buf, sizeof(buf)-1, 0);
if(ret <= 0) {
break;
}
printf("get %d bytes of normal data : %s\n", ret, buf);
}
// 对于可读事件,用带MSG_OOB标记的recv函数读取数据
else if(FD_ISSET(connfd, &exception_fds)) {
ret = recv(connfd, buf, sizeof(buf)-1, MSG_OOB);
if(ret <= 0) {
break;
}
printf("get %d bytes of oob data : %s\n", ret, buf);
}
}
close(connfd);
}

浙公网安备 33010602011771号