I/O多路复用之select
select函数
函数原型:int select(int n,fd_set * readfds,fd_set * writefds,fd_set * exceptfds,struct timeval * timeout);
参数
其中第一个参数n:表示文件描述符+1
fd_set是一个结构体,源代码如下:
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
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
这个结构体是一个数组,更严格来说是一个“位图”,用每一个比特位来标识对应要监视的文件描述符。
那么,select函数中,中间三个参数分别表示需要检测的读文件描述符集合、写文件描述符集合、异常文件描述符集合。
最后一个参数:timeout,它的类型是一个结构体,该结构体源代码如下:
/* A time value that is accurate to the nearest
microsecond but also has a range of years. */
struct timeval
{
#ifdef __USE_TIME_BITS64
__time64_t tv_sec; /* Seconds. */
__suseconds64_t tv_usec; /* Microseconds. */
#else
__time_t tv_sec; /* Seconds. */
__suseconds_t tv_usec; /* Microseconds. */
#endif
};
这个结构体用于描述一段时间长度,它在select函数中的作用是超时时间。
关于它的取值:
NULL:表示阻塞,文件描述符对应事件就绪之前一直处于阻塞状态。
0:表示非阻塞,仅检测文件描述符对应事件是否就绪,然后直接返回,返回值(即select函数返回值)在后面介绍。
特定值:在这个特定值之前,如果没有事件发生就会一直处于阻塞状态,当等待事件超过这个特定值,就会变成非阻塞。
返回值
返回值有三种情况:
1、> 0:表示文件描述符事件就绪的个数。
2、== 0:表示文件描述符事件就绪前已经超过timeout时间了,直接返回。
3、< 0:表示有错误发生,错误码被设置。
错误码可能为:
EBADF:文件描述符无效或者该文件描述符已关闭。
EINTR:此调用被信号所中断。
EINVAL:参数n为负值。
ENOMEN:核心内存不足。
补充
另外,需要注意的点:
1、select函数中间三个参数在每次使用select函数前都需要置0,再逐个设置文件描述符到对应事件集合中。
关于一些fd_set的接口:
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位。 int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真。 void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位。 void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位。
2、timeout默认为0,如果不想设置为非阻塞,那么就自己指定值,当然,哪怕是想设置为非阻塞也最好还是写上。
3、第一个参数n是最大文件描述符+1,不要忘了+1(这一条准确来说是注意事项)。
示例
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>
int main()
{
// struct timeval *timeout = nullptr;
fd_set read_fd;
FD_ZERO(&read_fd);
FD_SET(0, &read_fd);
while (true)
{
printf(">");
fflush(stdout);
int res = select(1, &read_fd, nullptr, nullptr, nullptr); // 设置为阻塞
if (res < 0)
{
perror("select");
continue;
}
if (FD_ISSET(0, &read_fd))
{
char buf[1024] = {0};
int n = read(0, buf, sizeof(buf) - 1);
buf[n - 1] = 0;
printf("output:%s\n", buf);
}
FD_ZERO(&read_fd);
FD_SET(0, &read_fd);
}
return 0;
}
select的特点
1、最多可监控文件描述符的个数取决于sizeof(fd_set)
的值,我这里用虚拟机试了下是128Byte,而文件描述符在fd_set中是以位图结构来存储的,也就是说我这台虚拟机最多可监控128 * 8 = 1024个文件描述符,不同机器可能不一样。
2、在使用select函数之前,还需要一个数据结构来保存要放到fd_set对象中的文件描述符,因为在select函数返回后,需要对数据结构中的文件描述符进行判断,判断该文件描述符事件是否就绪(判断函数:FD_ISSET(),上面有它的简单介绍),还有一个原因就是select函数返回后,没有就绪的文件描述符会被置0,等下次使用需要重新放到fd_set对象中。
select的缺点
- 每次调用select时,都需要把想要监视的文件描述符添加到fd_set中。
- 每次调用select时,都需要把fd集合从用户态拷贝到内核态。
- 每次都需要在内核遍历所传进来的fd。
- select支持的文件描述符数量太少。