前言
本文介绍select()、poll()、epoll()多路复用的相关知识。
一、select()多路复用
select监视并等待多个文件描述符的属性发生变化,它监视的属性分3类:readfds(文件描述符有数据到来可读)、writefds(文件描述符可写)、和exceptfds(文件描述符异常)。
调用select函数会阻塞,直到有数据可读、可写、或者有错误异常、超时时函数才返回。函数返回后,通过遍历 fdset,找到就绪的文件描述符,进行相应处理。
int select(int max_fd, fd_set *readest, fd_set *writeset, fd_set *exceptest, struct timeval *timeout);
- max_fd: 待测试的fd的总数,待测试的最大文件描述符j加1(0~max_fd-1,而即使要监听的为3,4,5,实际也要监听0-2,总数为6);若有数据出现事件(读、写、异常)将会返回。
- readset、writeset和exceptset:指定要让内核测试读、写和异常条件的fd集合,不需要测试的可以设置为NULL
- timeout:select的超时时间,设置为NULL则永不超时
- 返回值: 描述符的数目,超时时返回0,出错返回-1
struct timeval
{
long tv_sec; //秒
long tv_usec; //毫秒
}
Linux内核的参数__FD_SETSIZE定义了每个FD_SET的句柄个数,意味着select所用到的FD_SET是有限的,select默认只能同时处理1024个客户端的连接请求
- FD_ZERO( fd_set* fds ) // 清空集合
- FD_SET( int fd, fd_set* fds ) // 加入集合
- FD_ISSET( int fd, fd_set* fds ) // 判断是否在集合中
- FD_CLR( int fd, fd_set* fds ) // 从文件中删除指定描述符
二、poll()多路复用
poll()的机制与 select() 类似,在本质上没有多大差别,但是poll() 没有最大文件描述符数量的限制(数量过大性能会下降)。
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd
{
int fd; //文件描述符
short events; //等待的事件
short revents; //实际发生的事件
}
events标志以及测试revents标志的一些常值
常量 | 说明 | 能否作events输入 | 能否作revents返回 |
---|---|---|---|
POLLIN | 普通或优先级带数据可读 | √ | √ |
POLLRDNORM | 普通数据可读 | √ | √ |
POLLRDBABAND | 优先级带数据可读 | √ | √ |
POLLPRI | 高优先级数据可读 | √ | √ |
POLLOUT | 普通数据可写 | √ | √ |
POLLWRNORM | 普通数据可写 | √ | √ |
POLLWRBAND | 优先级带数据可写 | √ | √ |
POLLERR | 发生错误 | √ | √ |
POLLHUP | 挂起 | √ | |
POLLNVAL | 描述字不是一个打开的文件 | √ |
POLLIN | POLLPRI相当于select()的读事件
POLLOUT | POLLWRBAND相当于select()的写事件
POLLIN相当于POLLRDNORM | POLLRDBAND
POLLOUT相当于POLLWRNORM
- fds: 用来指向一个struct pollfd类型的数组,每一个pollfd结构体指定了一个被监视的文件描述符,指示poll()监视多个文件描述符。events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域,events域中请求的任何事件都可能在revents域中返回。
- nfds: 指定数组中的监听个数
- timeout: 指定等待的毫秒数,无论I/O是否准备好,poll都会返回。
timeout<0 :无限超时,使poll()一直挂起直到一个指定事件发生
timeout=0 :poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。
poll()函数成功调用时,返回结构体中revents域不为0的文件描述符个数;若在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下列值之一:
- EBADF : 一个或多个结构体中指定的文件描述符无效
- EFAULTfds : 指针指向的地址超出进程的地址空间
- EINTR : 请求事件之前产生一个信号,调用可以重新发起
- EINVALnfds : 参数超出PLIMIT_NOFILE值
- ENOMEM : 可用内存不足,无法完成请求
三、epoll()多路复用
epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统,把原先的select/poll调用分成了3个部分:
- 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
- 调用epoll_ctl向epoll对象中添加连接的套接字
- 调用epoll_wait收集发生的事件的连接
epoll_create():创建epoll实例
int epoll_create( int size );
epoll_ctl():修改epoll的兴趣列表
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
- epfd : epoll_create()的返回值
- op : 需要执行的操作
EPOLL_CTL_ADD(将fd加入兴趣列表,已存在会出现EEXIST错误)
EPOLL_CTL_MOD(修改描述符上设定的事件,不存在会出现ENOENT错误)
EPOLL_CTL_DEC(将fd从列表删除,不存在会出现ENOENT错误) - fd : 要修改兴趣列表中的哪一个文件描述符的设定(这里fd不能作为普通文件或目录的文件描述符)
- ev : 指向结构体epoll_event的指针,为文件描述符所做的设置
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event
{
uint32_t events; //待检查的描述符fd上所感兴趣的事件集合
epoll_data_t data; //fd稍后称为就绪态时,联合的成员可用来指定传回给调用进程的信息
}
epoll_wait():事件等待
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
- epfd : epoll_create()的返回值
- evlist : 有关就绪态文件描述符的信息,数组evlist的空间由调用者负责申请
- maxevents : evlist数组里包含的元素个数
- timeout : 确定epoll_wait()的阻塞行为
timeout = -1 : 调用一直阻塞,直到兴趣列表中的文件描述符上有事件产生或者直到捕获到一个信号为止
timeout=0:执行一次非阻塞式的检查,看兴趣列表中的描述符上产生了哪个事件
timeout>0:调用将阻塞至多timeout毫秒,直到文件描述符上有事件发生,或者直到捕获到一个信号为止
调用epoll_ctl()时可以在ev.events中指定的位掩码以及由epoll_wait()返回的evlist[].events中的值如下:
常量 | 说明 | 作epoll_ctl()输入 | 作epoll_create()的返回 |
---|---|---|---|
EPOLLIN | 可读取非高优先级数据 | √ | √ |
EPOLLPRI | 可读取高优先级数据 | √ | √ |
EPOLLRDHUP | socket对端关闭 | √ | √ |
EPOLLOUT | 普通数据可写 | √ | √ |
EPOLLET | 采用边沿触发事件通知 | √ | |
EPOLLONESHOT | 完成事件通知后禁用检查 | √ | |
EPOLLERR | 发生错误 | √ | |
EPOLLHUP | 出现挂断 | √ |
四、区别
- select:有最大连接数的限制,当事件发生时需要轮询一遍所有的fd才能知道是哪一个发生了变化
- poll:没有最大连接数的限制,但事件发生时也需要轮询所有fd
- epoll:没有最大连接数的限制,也不需要轮询