epoll是在 2.6内核时提出来的,是 select和 poll的增强版;
相较于 select 和 poll来说,epoll更加灵活,没有描述符限制;epoll使用一个文件描述符管理多个描述符,会去将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间内的 拷贝只需一次即可
epoll操作过程需要三个接口:
int epoll create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
(1). epoll_create 函数是一个系统函数,该函数会在内核空间内开辟出一块新的空间,可以理解为 epoll结构空间,返回值为 epoll的文件描述符编号,方便后续操作使用,size参数实际上就是去设置该 epoll能去监听的描述符数目
(2). epoll_ctl 是 epoll的事件注册函数,epoll_ctl 与 select不同,select函数是调用时指定需要监听的描述符和事件,epoll现将用户感兴趣的描述符事件注册到 epoll空间内,此函数是非阻塞函数,作用仅仅是去增删改 epoll空间内注册的描述符信息
参数一:指明哪一个 epoll,对应其编号,也是 epoll_create函数的返回值
参数二:op,表示当前请求的动作类型,由三个宏定义:(EPOLL_CTL_ADD:注册新的描述符到 epoll中去;EPOLL_CTL_MOD:修改已经注册的描述符的监听事件;EPOLL_CTL_DEL:从 epoll中删除已经注册的描述符)
参数三:fd,需要监听的文件描述符,一般指的是 socket_fd;
参数四:event,告诉内核对该 fd感兴趣的事件
struct epoll_event
{
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
这里的事件其实有多种:对应的描述符的可读、可写、错误发生、中断事件等...
(3). epoll_wait:等待事件的发生,返回需要处理的事件的数量,并且将需处理事件的描述符集合保存到 events内,便于后续遍历 events来处理事件
参数一:指定哪一个 epoll
参数二:事件集合,函数返回时,就已经就绪了的事件保存到其中了
参数四:超时时间,0不阻塞式调用,-1即阻塞直至事件发生
工作模式:
epoll对文件描述符的操作有两种模式:LT(水平触发) 和 ET(边缘触发);LT是默认模式,LT模式和 ET模式的区别如下:
LT(水平触发):事件就绪后,用户可以选择处理或者不处理,如果用户本次不去进行处理,那么下次调用 epoll_wait时,也会将未处理的事件打包给你
ET(边缘触发):事件就绪后,用户必须要去处理,因为内核已经不再进行兜底了,内核把就绪的事件打包给你后,就把对应的就绪事件清理掉了
ET模式在很大程度上减少了 epoll事件被触发的次数,因此效率要比 LT模式高
看个 epoll的demo:
Epoll相较于 select,进行的优化有哪些呢?
① 在创建 epoll对应的内核空间时,可以去指定监听的描述符数目,这个数目远大于 1024 (与机器内存相关,在 1G内存的机器上,大概 10万左右)
② epoll在内核开辟了空间,可以通过函数调用、工作模式的操作来去操作空间内注册了的描述符(增删改查),这样的话,就不需要再每次调用对应 wait方法时像 select函数一样涉及数据拷贝了,使用 epoll,只需要拷贝一次即可
③ 性能上的优势:select函数返回时,对应已发生事件描述符是在一个位图中,而这个位图中会存在已发生的、未发生的,即未完全纯粹将有事件产生的描述符筛选出来,因此需要手动去遍历每一位来判断对应的描述符是否有事件发生,这是低效的,事件复杂度 O(N);
而 epoll每次会将描述符集合中已发生事件的描述符添加到一个集合(即,就绪列表)中去,然后返回,然后我们直接便可以基于该集合进行操作了 (集合中的描述符都是已发生就绪事件的 QAQ&)