Select,Poll
五种IO模型
在介绍Select和Poll之前简单介绍一下IO
IO,即输入和输出,通过调用recv/read/send/write等系统调用将数据从缓冲区拷贝到另一个缓冲区,所以在IO的过程中,缓冲区是否为满以及数据是否就绪是非常重要的两个条件,因此在拷贝之前,OS通常需要等待资源就绪(如调用scanf时,OS需要等待我们向控制台中输入才能实现拷贝)
因此,IO的本质:等待 + 拷贝
其中分为五种IO模型:
- 阻塞IO
在内核将数据准备好之前,系统将会一直等待
- 非阻塞IO
如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码
- 信号驱动IO
内核将数据准备好之后,使用SIGIO信号通知进程进行IO操作
- IO多路转接(多路复用)
与阻塞IO类似,但不同点在于,IO多路转接可以同时等待多个fd的就绪状态
- 异步IO
在内核将数据拷贝完成之后通知进程(而信号驱动是通知进程何时拷贝数据)
我们在这里主要介绍的是IO多路转接这一模型中的内容
Select
系统提供select函数来实现多路转接的IO模型
Select函数
#include<sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数解释
- 参数nfds是需要监视所有文件描述符中数值最大的值+1
- readfds,writefds,exceptfds分别是可读文件描述符集合,可写文件描述符集合以及异常文件描述符集合
- timeout参数为结构体timeval,用来设置select()的等待时间
关于timeout取值
- NULL:表示select()没有timeout,则select将被一直阻塞,直到有文件描述符上发生了事件
- 0:只检测文件描述符的状态并立即返回,并不等待外部事件发生
- 特定时间值:表示select等待特定时间值,如果此时间内没有外部事件发生,则select将超时返回
关于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;
typedef long int __fd_mask;
实际上,fd_set是一个数组,表示一种位图结构,通过设置位图中对应位置的值来表示监视的文件描述符状态的变化
例如假设listensock的fd为3,当连接到来时,select将检测到读事件就绪,将readfds[3]上的0设置为1
提供了一系列操作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的全部位
函数返回值
- 成功则返回文件描述符状态已改变的个数
- 返回0表示select等待超过了timeout时间
- 错误返回-1
Select的特点
- 可监控文件描述符的个数由fd_set的大小决定,例如,如果sizeof(fd_set) = 512,则可监控文件描述符的个数为512*8=4096个(1字节=8bit)
- 在使用select时,通常需要一个数组来保存需要监控的fd
- 在select返回之后,数组中的fd可以作为源数据与FD_ISSET进行判断
- select返回之后会清空没有发生事件的fd,因此每次select之前都需要将数组中的fd重新逐一加入,并且可以扫描数组,得到maxfd并+1作为select的第一个参数
Select常见程序片段
int fd_array[sizeof(fd_set) * 8];
fd_set rfds; //以读事件为例
int maxfd = fdarray_[0].fd;
for(int i = 0;i < sizeof(fd_set) * 8;++i)
{
if(fd_array[i]为读事件)
FD_SET(fd_array[i], &rfds);
if (maxfd < fdarray_[i].fd)
maxfd = fdarray_[i].fd;
}
select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
for(int i = 0;i < sizeof(fd_set) * 8;++i)
{
if((FD_ISSET(fd_array_[i].fd, &rfds)) && (fd_array[i]为读事件))
{
//TODO
}
}
Select的缺点
- 每次调用select之前都需要手动设置fd集合,从接口使用角度来说很不方便
- 每次调用select之前都需要将fd集合从用户态拷贝到内核态,fd很多时开销比较大
- 同时每次调用select都需要在内核遍历传入的fd集合,也会造成开销大的问题
- select支持的文件描述符个数太少
为了将Select进行优化,因此在Select的基础上实现了Poll
Poll
Poll函数
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
//pollfd结构体
struct pollfd
{
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
参数解释
- fds是poll监听的pollfd结构体列表,其中包括所监听文件描述符,监听事件以及返回事件
- nfds表示fds的长度
- timeout为poll函数超时时间,单位为毫秒(ms)
关于poll中的事件
函数返回值
- 返回值大于0,表示poll监听的文件描述符就绪返回
- 返回值等于0,表示poll等待超时
- 返回值小于0,表示等待出错
Poll常见程序片段
struct pollfd fd_array[N]; //也可以动态管理
for (int i = 0; i < N; i++)
{
fd_array[i].fd = defaultfd;
fd_array[i].events = defaultevent;
fd_array[i].revents = defaultevent;
}
poll(fd_array, N, timeout);
for (int i = 0; i < N; i++)
{
if(fd_array[i].revents & POLLIN)//POLLIN表示读事件
{
//TODO
}
}
Poll的优点
- poll不需要通过fdset位图来设置文件描述符状态,只需要pollfd指针实现即可
- pollfd中包括了文件描述符以及事件类型,封装性更好
- poll没有最大数量限制
Poll的缺点
- 同select一样,poll需要轮询pollfd以获取文件描述符的准备状态
- 每次调用poll都需要将大量pollfd从用户态拷贝到内核态,增大了开销
因此,就有了另一种更优化方案 — Epoll。