epoll的原理和用法
设想一个场景:有100万用户同时与一个进程保持着TCP连接,而每一时刻只有几十个或几百个TCP连接是活跃的(接收到TCP包),也就是说,每一时刻进程只需要处理这100万连接中的极少一部分连接。那么如何才能高效地处理这种场景呢?进程是否在每次询问操作系统收集有事件发生的TCP连接时,把这100万个连接告诉操作系统,然后由操作系统找出其中有事件发生的几百个连接呢?实际上,在Linux2.4版本之前,那时的select
和poll
就是这么做的。
这里有个明显的问题,即在某一时刻,进程收集有时间发生的连接时,其实这100万连接中的大部分都是没有事件发生的。因此,如果每次收集事件时,都把这100万连接的套接字传给操作系统(这必然牵涉大量用户态到内核态的拷贝),而由操作系统内核寻找连接上有没有要处理的事件,将会是巨大的资源浪费。而select
和poll
就是这么做的,因此它们最多只能处理几千个并发连接。
但epoll
不这么做,它在Linux内核中申请一个简易的文件系统,把原先的select
或poll
调用分成3部分:调用epoll_create
建立一个epoll对象(在epoll文件系统中给这个句柄分配资源)、调用epoll_ctl
向epoll对象中添加这100万个连接、调用epoll_wait
收集发生事件的连接。这样,只需要在进程启动时建立1个epoll对象,并在需要的时候向它添加或删除连接就可以了。因此,在实际收集事件时,epoll_wait的效率就会非常高,因为调用epoll_wait
并不会将100万个连接全部传递过去,内核也不需要遍历全部的连接。
当一个进程调用epoll_create
时,Linux内核会创建一个eventpoll
结构体,这和结构中有两个成员与epoll的使用方式密切相关。
struct eventpoll{
struct rb_root rbr; // 红黑树的根节点
struct list_head rdllist; // 双向链表,保存有事件发生的连接,由epoll_wait返回
...
}
每个epoll对象都有一个独立的eventpoll结构体,这个结构体会在内核空间中创造独立的内存,用于存储使用epoll_ctl
方法向epoll对象添加进来的事件。这些事件都会挂到rbr
红黑树中,这样,重复添加的时间就能通过红黑树的性质高效地识别出来。
所有添加到epoll中的事件都会与设备驱动程序建立回调关系,也就是说,相应的事件发生时会调用关联的毁掉方法。这个回调方法在内核中叫做ep_poll_callback
,它会把这样的事件放到上面的rdllist
双向链表中。在epoll中,每一个事件都会创建一个epitem
结构体。
struct epitem{
struct rb_node rbn; // 红黑树节点
struct list_head rdllink; // 双向链表节点
struct epoll_filefd ffd; // 事件句柄等信息
struct eventpoll *ep; // 指向其所属的eventpoll对象
struct epoll_event event; // 期待的事件类型
...
}
这里包含着每个事件对应的信息。
当调用epoll_wait
检查是否有发生事件的连接时,只需要检查eventpoll
中rdllist
双向链表中是否有epitem
元素。如果rdllist
链表不为空,则把这里的事件复制到用户态内存中,同时把事件数量返回给用户。因此,epoll_wait
的效率非常高。epoll_wait
在向epoll对象中添加、修改、删除时,从红黑树中查找事件也非常快,也就是说,epoll是非常高效的,它可以轻易处理百万级别的并发连接。
如何使用epoll
epoll通过下面3个epoll系统调用来发挥作用
- epoll_create系统调用
int epoll_create(int size);
epoll_create
返回一个句柄,之后epoll的使用都将依靠这个句柄来标识。参数size
告诉epoll要处理的大致事件数目。不再使用epoll时,必须使用close
关闭这个句柄。
注意,
size
参数只是告诉内核大致的处理数目,而不是最大处理个数。
- epoll_ctl系统调用
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
epoll_ctl
向epoll对象中添加、修改、删除事件,返回0表示成功,否则返回-1,此时根据errno
变量分析具体错误类型。epoll_wait
方法返回的事件必然是通过epoll_ctl
添加到epoll中的。参数epfd
是epoll_create
返回的句柄。op
参数见下表。
op的取值 | 含义 |
---|---|
EPOLL_CTL_ADD | 添加新的事件到epoll中 |
EPOLL_CTL_MOD | 修改epoll中的事件 |
EPOLL_CTL_DEL | 删除epoll中的事件 |
第三个参数fd
是待检测的连接套接字,第四个参数event
告诉epoll对什么事件感兴趣,它使用了epoll_event
结构体,在上面介绍过的epoll机制中会为每个事件创建epitem
结构体,而在epitem
结构体中有个epoll_event
成员。epoll_event
定义如下。
struct epoll_event{
__uint32_t events;
epoll_data_t data;
}
events
取值见下表。
events取值 | 含义 |
---|---|
EPOLLIN | 表示对应连接上有数据可以读出(TCP连接的远端主动关闭连接,也相当于可读事件,因为需要处理到来的FIN包) |
RPOLLOUT | 表示对应的连接上可以写入数据发送 |
EPOLLRDHUP | 表示TCP连接的对端关闭或半关闭 |
EPOLLPRI | 表示对端有紧急数据发送 |
EPOLLERR | 表示连接发生错误 |
EPOLLHUP | 表示连接被挂起 |
EPOLLET | 设置触发方式为边缘触发(ET), 默认是水平触发(LT) |
EPOLLONESHOT | 表示对这个事件只处理一次,下次处理时需重新加入epoll |
而data
是一个epoll_data联合体。
typedef struct union epoll_data{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
- epoll_wait系统调用
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
收集在epoll监控中已经发生的事件,如果epoll中没有任何事件发生,则最多等待timeout
毫秒后返回。epoll_wait
的返回值表示当前发生的事件个数。如果返回0,则表示本次调用中没有事件发生;如果返回-1,表示出现错误,需要检查errno
变量。
第1个参数epfd
照例是epoll_create
返回的句柄。第2个参数events
则是分配好的epoll_event结构体数组,epoll会把发生的事件拷贝到events数组中(events不能为空,内核只负责吧数据复制到这个数组中,不负责在用户态分配内存)。第3个参数maxevents
表示本次可以返回的最大事件数目,通常和预分配完成的events数组长度相同。第4个参数timeout
表示在没有事件发生时等待的最长时间(毫秒), 如果为0,则表示epoll_wait不会等待,立即返回。
两种模式
epoll有LT和ET两种工作模式。默认LT,这时可以处理阻塞和非阻塞套接字。而上面events参数可以将模式调到ET。ET模式比LT效率更高,它只支持非阻塞套接字。
LT与ET模式的区别在于,当一个新事件到来时,ET模式下当然可以从epoll_wait
调用中获得这个事件,可是如果这次没有把此次事件对应的套接字缓冲区处理完,在这个套接字有新的事件到来之前,ET模式无法再次从epoll_wait
中获取这个事件,处理尚未处理完毕的缓冲区;
而LT模式则相反,只要事件对应的套接字缓冲区中有没处理干净的数据,通过epoll_wait
仍能获得这个事件。因此,LT模式下的epoll更简单,不易出错,而在ET模式下,如果没有一次彻底将缓冲区处理完,会导致缓冲区中的用户得不到响应。
默认情况下,redis使用LT模式,nginx使用ET模式。