select和epoll属于IO多路转接模型,用于实现对大量描述符进行就绪事件监控,可以让进程针对就绪描述符进行操作,避免因对非就绪的描述符进行操作而阻塞,让一个进程轮询对大量的就绪描述符进行操作,实现多个客户端与一个服务端之间的数据通信
IO多路转接模型 :select模型 poll模型 epoll模型
select模型
int select(int nfds,fd_set* readfds,fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
参数:
nfds:最大描述符+1;避免空遍历判断,提高性能
readfds/writefds/exceptfds:可读/可写/异常事件集合
timeout: 设置select等待时间:若为NULL–一直阻塞,直到某个文件描述符发生了事件;若为0,立即返回;若为特定的时间值,在指定的时间段没有发生事件,超时返回;
返回值:
大于0,返回的是就绪描述符的数量
等于0,等待超时
小于0,监控出错
用户自己定义一个描述符集合fd_set,然后将需要监控的描述符添加到集合中;将描述符集合拷贝到内核中;对集合中的描述符进行轮询就绪判断(若没有就绪,隔一点时间在进行轮询判断,若有描述符就绪,则select调用返回,返回就绪数目);调用返回前,将非就绪描述符移除;进程判断那些还在,进而对齐进行IO操作。
fd_set:描述符集合,其实就是一个整形数组,严格来说就是一个“位图”,大小默认是1024,取决于_FD_SETSIZE 这个宏
void FD_CLR(int fd,fd_set* set); //移除fd从set
int FD_ISSET(int fd,fd_set* set); //判断fd是否在set中
void FD_SET(int fd,fd_set* set); //将fd添加到set中
void FD_ZERO(fd_set* set); //清空set
select的优缺点:
1、select所能监控的描述符数量有最大上限–1024个
2、select每次都需要将描述符集合拷贝到内核去监控(用户态于内核态之间的数据拷贝)
3、select在内核中需要对所有的描述符进行轮询遍历判断是否就绪,数量的增多导致性能的降低
4、select就绪后会移除集合中非就绪描述符,修改集合,每次监控会重新添加描述符(编码复杂)
5、select返回的就绪的描述符,需要用户自己遍历,找出就绪的描述符对其进行操作,性能也会导致描述符的增多而降低
1、select遵循posix标准,可跨平台
2、select监控超时等待时间,可以精确到微妙
poll模型
int poll(struct pollfd* fds,nds_t nfds,int timeout);
返回值:
小于0, 表示出错;
等于0, 表示poll函数等待超时;
大于0, 表示poll由于监听的文件描述符就绪而返
用户为每一个关心的描述符定义事件结构,将描述符事件结构数组拷贝到内核,轮询遍历数组中的描述符,若没有描述符就绪用户关心的事件,每隔一会再遍历一次,若有,将就绪事件放到事件结构的revents中,并调用返回。当调用返回后,用户遍历事件结构数组,判断结构中的revents事件中是否有包含用户关心的事件,进而对其进行相应操作
poll的优缺点:
1、poll需要每次将所有事件结构信息拷贝到内核
2、在内核中任需要轮询遍历的判断,性能会导致描述符的增多而降低
3、描述符就绪后,通过修改事件结构体中的revents信息,返回后,还是未告知用户哪一个描述符就绪,还得轮询遍历判断那个是用户关心的事件
4、poll不能跨平台
1、采用事件结构的方式对描述符进行监控,简化了多个描述符集合的监控编码流程
2、没有描述符数量的上限限制
epoll模型
int epoll_create(int size);
创建一个epoll句柄,在内核中创建eventpoll结构体,
size:限制epoll所能监控的描述符上限,可以忽略,但要大于0
int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event);
注册要监听的事件类型
参数:
epfd: epoll_create的返回值,epoll的句柄
op:表示动作,有三个宏:
EPOLL_CTL_ADD :注册新的fd到epfd中;
EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
EPOLL_CTL_DEL :从epfd中删除一个fd
fd:需要监听的fd
struct 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; //用户关心的事件
epoll_data_t data; //data.fd就是用户要监控的描述符
}__EPOLL_PACKED;
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在epoll监控的事件中已经发送的事件.
参数:
参数events是分配好的epoll_event结构体数组.
epoll将会把发生的事件赋值到events数组(events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存).
maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size.
参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞).
如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函 数失败.
epoll的使用过程就是三部曲:
调用epoll_create创建一个epoll句柄;
调用epoll_ctl, 将要监控的文件描述符进行注册;
调用epoll_wait, 等待文件描述符就绪
epoll工作方式
举例子解释:
你正在吃鸡, 眼看进入了决赛圈, 你妈饭做好了, 喊你吃饭的时候有两种方式:
- 如果你妈喊你一次, 你没动, 那么你妈会继续喊你第二次, 第三次…(亲妈, 水平触发)
- 如果你妈喊你一次, 你没动, 你妈就不管你了(后妈, 边缘触发)
epoll有2种工作方式-水平触发(LT)和边缘触发(ET)
水平触发Level Triggered 工作模式
epoll默认状态下就是LT工作模式.
当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.
如上面的例子, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait 仍然会立刻返回并通知socket读事件就绪.
直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.
支持阻塞读写和非阻塞读写
边缘触发Edge Triggered工作模式
当epoll检测到socket上事件就绪时, 必须立刻处理.
如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 epoll_wait 的时候, epoll_wait 不会再返回了.
也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会.
ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll.
只支持非阻塞的读写
select和poll其实也是工作在LT模式下. epoll既可以支持LT, 也可以支持ET
LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序猿一次响应就绪过程中就把 所有的数据都处理完.
相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到 每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的.
另一方面, ET 的代码复杂程度更高了.
epoll的优缺点
接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文 件描述符, 也做到了输入输出参数分离开
数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不 频繁(而select/poll都是每次循环都要进行拷贝)
事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述 符数目很多, 效率也不会受到影响.
没有数量限制: 文件描述符数目无上限
epoll不能跨平台
监控等待时间只能精确到毫秒