首先认识一下epoll的几个基础函数
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...);
listen(s, ...);
int epfd = epoll_create(...)
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中
while(1) {
int n = epoll_wait(...);
for(接受到数据的socket) {
//处理数据
}
}
这段代码涉及到几个与epoll相关的函数方法:
epoll_create: 创建一个文件句柄
epoll_ctl: 向epoll对象添加/修改/删除要管理的连接
epoll_wait: 等待其管理的连接上的IO事件
int epoll_create(int size);
功能描述:用于生成一个epoll专用的文件描述符
参数size:因为epoll底层使用红黑树来保存文件描述符,红黑树的查找时间复杂度为O(logN),这个size并没有必要限制大小。size可以设置为大于0的任何数。
返回值:如果成功,返回epoll专用的文件描述符,如果失败,则返回-1。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event * event);
epoll中的事件注册函数,用于注册要监听的事件类型
参数:
epfd: epoll专用的文件描述符,epoll_create()的返回值
op: 表示动作,用三个宏来表示:
EPOLL_CTL_ADD: 注册新的fd到epfd中
EPOLL_CTL_MOD: 修改已经注册的fd的监听事件
EPOLL_CTL_DEL: 从epfd中删除一个fd
fd: 需要被监听的文件描述符
event: 告诉内核需要监听什么事件
返回值:0表示成功,-1表示失败
epoll_event结构体如文档描述:
#include
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
};
typedef union epoll_data epoll_data_t;
events可以是以下几个宏的集合:
EPOLLIN: 表示对应的文件描述符可以读
EPOLLOUT:表示对应的文件描述符可以写
EPOLLPRI:表示对应的文件描述符有紧急的数据可以读
EPOLLERR: 表示对应的文件描述符发生错误
EPOLLHUP:表示对应的文件描述符被挂断
EPOLLET: 将EPOLL设为边缘触发模式,不设置为水平触发模式
EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要监听这次事件,则需要把socket重新加入到EPOLL中。
int epoll_wait(int epfd, struct epoll_event * event,
int maxevents, int timeout);
功能:本函数用于监控事件的发生,当epoll监控的事件中存在已经发送的事件,那么就可以将此事件收集起来。
参数:
epfd:epoll自身产生的文件描述符,调用epoll_create的返回值
event:事先必须将空间分配好的结构体数组,epoll将即将发生的事件赋值到event数组中。
maxevents:告诉内核共有多少个event数组的大小
timeout:超时时间,单位为毫秒,设为-1时,该函数状态为阻塞。
返回值:如果为-1,表示失败;如果为0,表示已经超时;如果成功,返回需要处理的事件数目。
epoll整体流程图如下所示:
我们在之前文章中讨论过,select和poll存在三个缺陷,epoll方式很好的解决了这些问题:
epoll在内核中使用红黑树这个数据结构来保存文件描述符,红黑树这个数据结构的增删改的时间复杂度为O(logn),select/poll每次监听套接字,都需要将套接字列表整个复制到内核态,而epoll使用epoll_ctl,每次将一个监听套接字复制到内核态,这明显减少了用户空间到内核态的大量数据拷贝和内存分配。
epoll使用异步回调的机制,内核态维护一个就绪队列,如果某个socket已经准备好,那么就会通过回调函数触发内核将socket事件加入到就绪事件列表。用户调用epoll_wait函数,就会返回有事件发生的文件描述符的个数。这个过程中,没有像select/poll一样对socket集合进行了O(N)时间复杂度的轮询。提高了检测的效率。
epoll支持两种事件触发模式:
边缘触发:当被监控的Socket描述符有可读事件发生时,服务器端在调用epoll_wait这个函数时,只会苏醒一次,然后从内核缓冲区中读取所有的数据
水平触发:当被监控的Socket描述符有可读事件发生时,服务器端在调用epoll_wait这个函数时,会不断的苏醒,直到内核中没有数据可读才停止苏醒。