1、select、poll的些许缺点
先回忆下select和poll的接口
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
这两个多路复用实现的特点是:
- 每次调用select和poll都要把用户关心的事件集合(select为readfds,writefds,exceptfds集合,poll为fds结构体数组)从用户空间到内核空间。
- 如果某一时间段内,只有少部分事件是活跃的(用户关心的事件集合只有少部分事件会发生),会浪费cpu在对无效事件轮询上,使得效率较低,比如,用户关心1024个tcp socket的读事件,当是,每次调用select或poll时只有1个tcp链接是活跃的,那么对其他1023个事件的轮询是没有必要的。
select支持的文件描述符数量较小,一般只有1024,poll虽然没有这个限制,但基于上面两个原因,poll和select存在同样一个缺点,就是包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而且不论这些文件描述符是否就绪,每次都会轮询所有描述符的状态,使得他们的开销随着文件描述符数量的增加而线性增大。epoll针对这几个缺点进行了改进,不再像select和poll那样,每次调用select和poll都把描述符集合拷贝到内核空间,而是一次注册永久使用;另一方面,epoll也不会对每个描述符都轮询时间是否发生,而是只针对事件已经发生的文件描述符进行资源抢占(因为同一个描述符资源(如可读或可写)可能阻塞了多个进程,调用epoll的进程需要与这些进程抢占该相应资源)。下面记录一下自己对epoll的学习和理解。
2、epoll的几个接口
上面说到每次调用select和poll都把描述符集合拷贝到内核空间,这是因为select和poll注册事件和监听事件是绑定在一起的,为甚这么说呢,我们看select和poll的编程模式就明白了:
while(true){ select(maxfd+1,readfds,writefds,execpfds,timeout)/poll(pollfd,nfds,timeout); }
在I/O多路复用之select中说到了select的实现,调用select时就会进行一次用户空间到内核空间的拷贝。epoll的改进其实就是把注册事件和监听事件分开了,epoll使用了一个特殊的文件来管理用户关心的事件集合,这个文件存在于内核之中,由特殊的数据结构和一组操作构成,这样的话,用户就可以提前告知内核自己关心的事件,然后再进行监听,因此,就只需要一次用户空间到内核空间的拷贝了。其中管理事件集合的文件通过epoll_create创建,注册用户行为通过epoll_ctl实现,监听通过epoll_wait实现。那么编程模型大概是这个样子:
epoll_fd=epoll_create(size); epoll_ctl(epoll_fd,operation,fd,event); while(true){ epoll_wait(epoll_fd,events,max_events,timeout); }
2.1、epoll_create接口
#include <sys/epoll.h>
int epoll_create(int size);
epoll_create创建epoll文件,其返回epoll的句柄,size用来告诉内核监听文件描述符的最大数目,这个参数不同于select()中的第一个参数(给出最大监听的fd+1的值)。需要注意的是,当创建好epoll句柄后,它会占用一个fd值,在linux下如果查看/proc/进程id/fd/,能够看到这个fd,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。(摘自epoll精髓)
epoll_create会在内核初始化完成epoll所需的数据结构,其中一个关键的结构就是rdlist,表示就绪的文件描述符链表,epoll_wait函数就是直接检查该链表,从而抢占准备好的事件;另一个关键的结构是一颗红黑树,这棵树专门用于管理用户关心的文件描述符集合。
注:关于epoll文件的核心数据结构以及epoll_create的源码请参考这两份资料
2.2、epoll_ctl接口
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll_ctl用于用户告知内核自己关心哪个描述符(fd)的什么事件(event),
- epfd,使用epoll_create函数创建的epoll句柄,epfd文件描述符对应的结构中,有一颗红黑树,专门用于管理用户关心的事件集合。
- op,用于指定用户行为,op参数有三种取值:fd,用户关心的文件描述符
- EPOLL_CTL_ADD,注册新的fd到epfd中;
- EPOLL_CTL_MOD,修改已注册fd的事件;
- EPOLL_CTL_DEL,从epfd中删除一个fd;
- event,用户关心的事件(读,写)
参数event的结构如下:
struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable,内核会修改该属性 */ };
events可以是以下几个宏的集合:
- EPOLLIN ,表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
- EPOLLOUT,表示对应的文件描述符可以写;
- EPOLLPRI,表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
- EPOLLERR,表示对应的文件描述符发生错误;
- EPOLLHUP,表示对应的文件描述符被挂起;
- EPOLLET,将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
- EPOLLONESHOT,只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
2.2.1、EPOLL_CTL_ADD
重点说一下这个取值,当op=EPOLL_CTL_ADD时,epoll_ctl主要做了四件事:
- 把当前文件描述符及其对应的事件(fd,epoll_event)加入红黑树,便于内核管理
- 注册设备驱动poll的回调函数ep_ptable_queue_proc,当调用f_op->poll()时,最终会调用该回调函数ep_ptable_queue_proc()
- 在ep_ptable_queue_proc回调函数中,注册回调函数ep_poll_callback,ep_poll_callback表示当描述符fd上相应的事件发生时该如何告知进程。
- 在ep_ptable_queue_proc回调函数中,检测是文件描述符fd对应的设备的epoll_event事件是否发生,如果发生则把fd及其epoll_event加入上面提到的就绪队列rdlist中
注:关于epoll_ctl、ep_ptable_queue_proc、ep_poll_callback的原理及源码请参考这两份资料
2.3、epoll_wait接口
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
- epfd,使用epoll_create函数创建的epoll句柄,epfd文件描述符对应的结构中,有一颗红黑树,专门用于管理用户关心的事件集合。
- events,传出参数,表示发生的事件
- maxevents,传入参数,表示events数组的最大容量,其值不能超过epoll_create函数的参数size
- timeout,0,不阻塞;整数,阻塞timeout时间;负数,无限阻塞
epoll_wait函数的原理就是去检查上面提到的rdlist链表中每个结点,rdlist的每一个结点能够索引到监听的文件描述符,就可以调用该文件描述符对应设备的poll驱动函数f_op->poll,用以检查该设备是否可用。这里有个问题需要思考一下,既然rdlist就表示就绪的事件,也就是设备对应的资源可用了,为什么还要进行检查?这是因为设备的某个资源可能被多个进程等待,当设备资源准备好后,设备会唤醒阻塞在这个资源上的所有进程,当前调用epoll_wait的进程未必能抢占这个资源,所以需要再调用检查一次资源是否可用,以防止被其他进程抢占而导致再次不可用,检查的方法就是调用fd设备的驱动f_op->poll。
这也是为什么epoll效率可能比较高的原因,epoll每次只检查已经就绪的设备,不像select、poll,不管有没有就绪,都去检查。
注:关于epoll_wait的原理及源码请参考这两份资料
2、epoll的两种触发模式ET<
二者的差异在于level-trigger模式下只要某个socket处于readable/writable状态,无论什么时候进行epoll_wait都会返回该socket;而edge-trigger模式下只有某个socket从unreadable变为readable或从unwritable变为writable时,epoll_wait才会返回该socket。下面两幅图清晰反映了二者区别,这两幅图摘自Epoll在LT和ET模式下的读写方式
参考资料: