IO多路复用--epoll
1. epoll 简介
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。与poll/select不同,epoll不再是一个单独的系统调用,而是由epoll_create/epoll_ctl/epoll_wait三个系统调用组成。相对于select和poll来说,epoll更加灵活,没有描述符限制。
2. epoll函数
epoll操作过程需要三个接口,分别如下:
#include <sys/epoll.h> int epoll_create(int size);
/*epoll_create(2) creates a new epoll instance and returns a file descriptor referring to that instance.*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*Interest in particular file descriptors is then registered via epoll_ctl(2). The set of file descriptors
currently registered on an epoll instance is sometimes called an epoll set.*/
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
/*waits for I/O events, blocking the calling thread if no events are currently available.
The epoll_wait() system call waits for events on the epoll(7) instance referred to by the file descriptor epfd. The memory area pointed to by events will contain the events that will be available for the caller. Up to maxevents are returned by epoll_wait(). The maxevents argument must be greater than zero.
*/
(1) int epoll_create(int size);
创建一个epoll实例,并返回指向该实例的fd;当创建好epoll实例后,它会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
参数:
- size: 本来是用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值;只不过在2.6.27内核开始就不必要了,只要大于 零就行。相关代码片段如下:
SYSCALL_DEFINE1(epoll_create, int, size) { if (size <= 0) return -EINVAL; return sys_epoll_create1(0); }
返回值:
成功,返回一个非负数的fd;失败,返回-1.
(2) int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数。
参数:
- epfd : epoll实例的fd;
- op : 操作标志,下文会描述;
- fd : 监控对象的fd;
- event : 事件的内容,下文描述;
op可以有3个值,分别为:
- EPOLL_CTL_ADD : 添加监听的事件
- EPOLL_CTL_DEL : 删除监听的事件
- EPOLL_CTL_MOD : 修改监听的事件
event是一个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 events */ epoll_data_t data; /* User data variable */ };
//The event argument describes the object linked to the file descriptor fd.
其中,data是一个联合体,能够存储fd或其它数据,我们需要根据自己的需求定制。events表示监控的事件的集合,是一个状态值,通过状态位来表示,可以设置如下事件:
- EPOLLERR : 文件上发上了一个错误。这个事件是一直监控的,即使没有明确指定
- EPOLLHUP : 文件被挂断。这个事件是一直监控的,即使没有明确指定
- EPOLLRDHUP : 对端关闭连接或者shutdown写入半连接
- EPOLLET : 开启边缘触发,默认的是水平触发,所以我们并未看到EPOLLLT
- EPOLLONESHOT : 一个事件发生并读取后,文件自动不再监控
- EPOLLIN : 文件可读
- EPOLLPRI : 文件有紧急数据可读
- EPOLLOUT : 文件可写
- EPOLLWAKEUP : 如果EPOLLONESHOT和EPOLLET清除了,并且进程拥有CAP_BLOCK_SUSPEND权限,那么这个标志能够保证事件在挂起或者处理的时候,系统不会挂起或休眠
注意一下,EPOLLHUP并不代表对端结束了连接,这一点需要和EPOLLRDHUP区分。通常情况下EPOLLHUP表示的是本端挂断,造成这种事件出现的原因有很多,其中一种便是出现错误,更加细致的应该是和RST联系在一起,不过目前相关文档并不是很全面。
返回值:
成功,返回0;失败,返回-1,标志出现了问题,我们可以读取errno来定位错误,有如下errno会被设置:
- EBADF : epfd或者fd不是一个有效的文件描述符
- EEXIST : op为EPOLL_CTL_ADD,但fd已经被监控
- EINVAL : epfd是无效的epoll文件描述符
- ENOENT : op为EPOLL_CTL_MOD或者EPOLL_CTL_DEL,并且fd未被监控
- ENOMEM : 没有足够的内存完成当前操作
- ENOSPC : epoll实例超过了/proc/sys/fs/epoll/max_user_watches中限制的监听数量
(3) int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的发生。
参数:
- epfd : epoll实例的fd
- events : 储存事件的数组首地址,并在调用返回时保存就绪的事件;
- maxevents : epoll_wait() 最多可以返回的事件,故表示存储空间的大小,即events数组的大小;
- timeout : 等待的最长时间
返回值:
成功,返回获得的就绪事件的数量;失败,返回-1,表示出现了问题,我们可以读取errno来定位错误,有如下errno会被设置:
- EBADF : epfd不是一个有效的文件描述符
- EFAULT : events指向的内存无权访问
- EINTR : 在请求事件发生或者过期之前,调用被信号打断
- EINVAL : epfd是无效的epoll文件描述符
3 工作模式
水平触发(Level Trigger)和边缘触发(Edge Trigger):
那么为什么在这里突兀得提及ET和LT呢?是这样的,想必各位应该已经注意到EPOLLET了,这个就代表ET事件,而epoll默认采取的是LT,也就是说在能够正确使用epoll之前,我们必须弄明白ET和LT,尤其是准备直接使用nonblocking和ET的朋友。
LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
参考:
Linux Programmer's Manual --- EPOLL